Object oriented programming... call an object within its own method?

Hi all … Last week I got great help here with my beginner steps with object oriented programming, and my app works well now.

Now I am adding the famous “game of life” simulation to my program.

For this, I have a class called Dot, which handles each cell in a simple grid. These dots have properties like _lit which determines if that cell is “on” or “off” … Then I have a 2x2 matrix of them, which is dots… for example, I have a method “toggle”… dots[4][8].toggle() will toggle _lit on or off.

My problem arose when I tried to make a method called countneighbors(). The hope is that if I call dots[5][4].countneighbors() then it will check all of the 8 cells that are adjacent to that one cell… and simply return the count. For example if that cell is totally surrounded on all 8 sides, it will return 8.

But to write this, I have the countneighbors() method call another method, namely dots.look(). (look() just returns whether that one is lit or not.)

→ When I compile this, it highlights the place where countneighbors() calls look(), and it says:
error: ‘dots’ was not declared in this scope

Is it a problem to call one method from inside another? Can you tell me how I would correct this?

  • Below is my entire code (with chunks omitted that were only related to other little games in the program.) See the method countneighbors() … you can see that I have commented with ERROR ERROR ERROR one of the lines that cr
  • eates the error I’ve discussed…
  • Huge thanks for any help you can give! --Eric
/* huzzah and 240 x 320 ili9341... a match made in heaven */

#include <Adafruit_GFX.h>    // Include core graphics library
#include <Adafruit_ILI9341.h> // Include Adafruit_ILI9341 library to drive the display
// Declare pins for the display:
#define TFT_DC 16 // WAS ORIG 9
#define TFT_RST 12  // WAS ORIG 8~ // You can also connect this to the Arduino reset in which case, set this #define pin to -1!
#define TFT_CS 15  // WAS ORIG 10~
// The rest of the pins are pre-selected as the default hardware SPI for Arduino Uno (SCK = 13 and SDA = 11)
// Create display:
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
//#include <Fonts/FreeSerif24pt7b.h>  // Add a custom font

const int screenwidth = 240;
const int screenheight = 320;
//const int numdotrows = 40, numdotcols = 30;
//const int numdotrows = 4, numdotcols = 3;
const int numdotrows = 320/10, numdotcols = 240/10;
const int dotwidth = screenwidth / numdotcols;
const int dotheight = screenheight / numdotrows;
const int maxmenu = 5;
int menuitem=0;
bool firstrun = true;
String menunames[] = {"a poem          ", "rocket circuit          ", "dot circuit          ", "linear division          ","sparkle          ","ballbot       "};

uint16_t colorof(uint8_t r, uint8_t g, uint8_t b) { 
  return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3); 
}

// CLASS DOT

class Dot {
  public:
    //constructor
    Dot(int x, int y, bool lit, bool _tobelit);
    // methods
    void light(bool currentnotfuture);
    void douse(bool currentnotfuture);
    void toggle();
    void draw();
    bool look(bool currentnotfuture);
    void shift();
    int countneighbors();
  private:
    int _x;
    int _y;
    bool _lit;
    bool _tobelit;
};

// DOT METHODS
Dot::Dot(int x, int y, bool lit, bool tobelit) {
  _x = x;
  _y = y;
  _lit = lit;
  _tobelit = tobelit;
}

void Dot::light(bool currentnotfuture) {
  if (currentnotfuture) {
    _lit = true;
  }
  else {
    _tobelit = true;
  }
 
}

void Dot::douse(bool currentnotfuture) {
  if (currentnotfuture) {
    _lit = false;
  }
  else {
    _tobelit = false;
  }
}

void Dot::toggle() {
  _lit = not(_lit);
}

void Dot::draw() {
  uint16_t color;
  int drawx = (dotwidth) * _x;
  int drawy = (dotheight) * _y;
  if (_lit) {
    color=0xFFFF;
  }
  else {
    color=0x0000;
  }
  tft.fillRect(drawx,drawy,dotwidth,dotheight,color);
}

bool Dot::look(bool currentnotfuture) {
  if (currentnotfuture) {
    return _lit;
  }
  else {
    return _tobelit;
  }
}

void Dot::shift() {
  _lit = _tobelit;
}

int Dot::countneighbors() {   // COUNT THE EIGHT NEIGHBOR DOTS TO THIS DOT... THREE ABOVE, TWO ON SAME ROW, AND THREE BELOW.
  int hstart = max(_x, 0);
  int hend = max(_x+1, numdotcols);
  int count = 0;

  // the three TOP LAYER cells
  if (_y-1 > 0) {
    for (int xscan = hstart; xscan<hend+1; xscan++) {
      if (dots[scan][_y-1]->look(true)) {     // THIS IS ONE OF THE ERROR ERROR ERROR ERROR ERROR ERROR ONES
        count++;
      }
    }
  }

  // the two MIDDLE LAYER cells
  if (_x-1 >= 0) {
    if (dots[_x-1][_y]->look(true)) {
        count++;
    }
  }

  if (_x+1 < numdotcols) {
    if (dots[_x+1][_y]->look(true)) {
        count++;
    }
  }

  // the two BOTTOM LAYER cells
  if (_y+1 < numdotrows) {
    for (int xscan = hstart; xscan<hend+1; xscan++) {
      if (dots[scan][_y+1]->look(true)) {
        count++;
      }
    }
  }
  return count;
} // end of countneighbors

//INSTANTIATE DOTS

Dot *dots[numdotcols][numdotrows];


void waitbutton(int pin, bool shouldclear) {
  while (digitalRead(pin) != HIGH ) {
    delay(50);
  };
  if (shouldclear) {
    tft.fillScreen(0x0000);
  }
}



//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]
//  [[DELETED CODE RELATED TO OTHER GAMES ]]




void g3place(int x, int y, uint16_t color) {   // THIS PROCEDURE JUST PLAYING AROUND, NOT RELEVANT TO REAL GAME
  int drawx = (dotwidth) * x;
  int drawy = (dotheight) * y;
  tft.fillRect(drawx,drawy,dotwidth,dotheight,color);
} 

void g3pretty() {   // THIS PROCEDURE JUST PLAYING AROUND, NOT RELEVANT TO REAL GAME
  for (int xscan=0; xscan<numdotcols;xscan++) {
    for (int yscan=0; yscan<numdotrows;yscan++) {
      g3place(xscan,yscan,colorof((xscan+yscan % 2)*128+90, xscan*(255/numdotcols),yscan*(255/numdotrows) ));
    }
  }
}

void g3drawfield() {
  for (int xscan=0; xscan<numdotcols;xscan++) {
    for (int yscan=0; yscan<numdotrows;yscan++) {
      dots[xscan][yscan]->draw();
    }
  }
}


void g3shiftall() {
  for (int xscan=0; xscan<numdotcols;xscan++) {
    for (int yscan=0; yscan<numdotrows;yscan++) {
      dots[xscan][yscan]->shift();
    }
  }
}

void g3liferulepass() {
  for (int xscan=0; xscan<numdotcols;xscan++) {
    for (int yscan=0; yscan<numdotrows;yscan++) {
       int c = dots[xscan][yscan]->countneighbors();
       if (c<2 or c>3) {
         dots[xscan][yscan]->douse(false);
       }
       if (c=3) {
        dots[xscan][yscan]->light(false);
       }
    }
  }
}

void game3() {  //=====three=====three=====three=====three=====three=====three=====three=====three=====three=====three
  bool quitter = false;
  Serial.println("*** STARTING GAME3");
  
  g3pretty();
  waitbutton(4,true);

  g3drawfield(); // should show all white
  waitbutton(4,true);

  for (int xscan=0; xscan<numdotcols;xscan++) {  // fill nextbuffer with random
      for (int yscan=0; yscan<numdotrows;yscan++) {
        int randall = random(0,2);
        if (randall == 1) {
          dots[xscan][yscan]->light(false);
        }
        else {
          dots[xscan][yscan]->douse(false);
        }
      }
  }
  
  g3drawfield(); // should still show all white
  waitbutton(4,true);

  g3shiftall(); // shift phase bringing next phase into reality
  
  g3drawfield(); // should show the randoms AND WE ARE SEEDED AND READY TO START
  waitbutton(4,true);

  while (not quitter) {
    g3liferulepass();
    g3shiftall();
    g3drawfield();

    
    if (digitalRead(5) == HIGH ){  // BUTTON 4 QUITS
      quitter = true;
    }
    waitbutton(4,true);
    
  }

}

void setup()  // Start of setup
{
  Serial.begin(9600);
  randomSeed(analogRead(0)); 

  //SET UP GRID OF DOTS
  for(int y=0;y<numdotrows;y++) {
    for (int x=0;x<numdotcols;x++) {
        dots[x][y] = new Dot(x,y,true, true); 
    }
  }
  
  pinMode(4,INPUT);
  pinMode(5,INPUT);
  
  // Display setup:
  tft.begin();  // Initialize display
  tft.fillScreen(0x0000);  // Fill screen with black
  //tft.setRotation(0);  // Set orientation of the display. Values are from 0 to 3. If not declared, orientation would be 0,
                         // which is portrait mode.
}  // End of setup

void loop() {
  if (firstrun) {
    firstrun = false;
    //game1(2,5,20,7,true);
    game3();
  }


//HAD TO DELETE OTHER CODE
//HAD TO DELETE OTHER CODE
//HAD TO DELETE OTHER CODE
    
  }
}

P.S. Sorry, I did notice that in the constructor I had an error by putting the underscore version of the variable _tobelit up in the parameters parentheses. Duh. I've fixed that in my code but it's not relevant to the issue I'm asking about. Thanks! --Eric

From an OO programming view, I would challenge the idea that the count_neighboors functions needs to be a method of your dot class. That would break a rule of independent entity, each dot would need to know how to talk to other dots directly through a coordinate system like you do and you loose the concept of something that has its own indépendance.

You could have another class that represents a collection of dots arranged in a grid, like a DotGrid class. Counting neighbors would then be a method that class as this is the one knowing how individual dots are laid out.

So dots could have a variable (could be a class variable if you only want one grid ever in your code) that tell them in which grid they belong and when a dot wants to count neighbors they would call the count_neighboors method on that grid. The grid class then can ask the calling dots for its (x,y) and the Grid class then is the one inspecting dots at (x-1,y), (x+1,y), (x-1,y-1), (x,y-1),… all the 8 adjacent dots

So A dot should just have an x,y attributes, a state (lit or off) and a grid reference if they belong in a grid

The grid class would have a 2D array of dots as instance variable

This is fantastic advice, thanks so much.

To be honest, I will probably take your advice as two steps. Your first paragraph -- "don't make countneighbors a method of the class" makes great sense and I think part of my brain had been considering it.

Your specific suggestion after that risks blowing my mind at the moment ... so I might try a humbler "non-method" approach first and see if I later evolve to the other plan...

Thanks again!!!
Eric

Sure you can have a dot class and just create the 2D array and some functions in the main program. That works too

Yep, that's what I did... and it works perfectly now (after I found a few more predictable bugs.)

Thanks! We'll see if I evolve to make one object for the whole grid. I wonder if it would have memory implications.

thx!

Eric

An instance of an object is not really more costly than the same structure coded in C - probably just a few bytes.

cool... that makes sense. Well, as I move slowly into being more object oriented in thinking. it is very rewarding. in some ways it makes programming a bit more fun. I just added a crude pong game into my "matrix of 40x53 dots" app... and realizing that I could get it to detect the paddle quite easily if I added one new boolean property to each dot object... it handled it gracefully.

(although with a certain memory cost. well, if there are 40x53 = 2120 dot objects, and I added one boolean property to each of those... at the silly absolute minimum that would be 2120 extra bits of memory consumed. but of course more overhead... any guess at how much memory that would consume in the end? I realized getting the answer is not as easy as watching the difference when the compiler tells me how much memory was used by variables... because I think the compiler doesn't really know how many objects my loops will create until after it compiles.)

every byte you add to your instance variables of your class will definitely use 1 byte per instance…

→ so if you have 2120 instances, then you consumer an extra 2120 bytes…

but one can be smart… :slight_smile:

say your dot needs to know its x and y position and and few a booleans, you had declared your class as

  private:
    int _x;
    int _y;
    bool _lit;
    bool _tobelit;
    bool _dotState;

Now lets assume you know your matrix is of size 40x53

40 and 53 are not very big numbers - so you don’t need a full int to represent them. actually 6 bits would be enough (0b111111(bin) is 63(dec)), so if you could allocate 6 bits for x and y you can store your position.

And booleans can fit on one bit (0 or 1), so your 3 other variables could be a total of 3 bits.

In total your memory needed is 6 + 6 + 3 = 15 bits → fits within two bytes.

So one way to save memory is to start thinking in bit fields:

Try this code:

struct dotDataSmall_t
{
  uint8_t _x : 6;       // max value 63
  uint8_t _y : 6;       // max value 63
  uint8_t _lit : 1;     // max value 1
  uint8_t _tobelit : 1; // max value 1
  uint8_t _state : 1;   // max value 1
} smallDot = {15, 32, 1, 0, 1};

struct dotDataFat_t
{
  int _x;
  int _y;
  bool _lit;
  bool _tobelit;
  bool _dotState;
} fatDot = {15, 32, 1, 0, 1};

void setup()
{
  Serial.begin(115200);
  Serial.println(F("WITH BIT FIELD"));
  Serial.print(F("Size of dotDataSmall_t = "));
  Serial.println(sizeof(smallDot));

  Serial.print(F("_x = "));
  Serial.println(smallDot._x);

  Serial.print(F("_y = "));
  Serial.println(smallDot._y);

  Serial.print(F("_tobelit = "));
  Serial.println(smallDot._tobelit);


  Serial.println(F("\nWITHOUT BIT FIELD"));
  Serial.print("Size of dotDataFat_t = ");
  Serial.println(sizeof(fatDot));

  Serial.print(F("_x = "));
  Serial.println(fatDot._x);

  Serial.print(F("_y = "));
  Serial.println(fatDot._y);

  Serial.print(F("_tobelit = "));
  Serial.println(fatDot._tobelit);
}

void loop() {}

the Serial console will show (on a UNO or similar)

[color=purple]
WITH BIT FIELD
[color=green]Size of dotDataSmall_t = 2[/color]
_x = 15
_y = 32
_tobelit = 0

WITHOUT BIT FIELD
[color=red]Size of dotDataFat_t = 7[/color]
_x = 15
_y = 32
_tobelit = 0
[/color]

→ you can see we saved 5 bytes per instance and access to the data works the same way… so if you have 2120 instances, you saved 2120 x 5 = 10600 bytes, more than 10kB of SRAM !!

and you even have 3 extra bits to play with. (3 more booleans or a value less than 7 - say if you want to count the number of hits on a dot before it disappears and max is 7)

That's great info -- again, I might have to stare at it before I try it.

But here's a question... a "bool" is also, I would think, only 1 bit.

so... Note that here I am only talking about the boolean properties you discussed, not the x and y...

if I only look at the boolean values, would:

uint8_t _lit : 1;

... take exactly the same amount of memory as:

bool _lit;

? ... or is "bool" less efficient than using the other way?

thanks
eric

Variables all use at least 1 byte, even a bool
When you use bit fields, the structure will be padded to a multiple of 8 bits so if you have nothing else in the structure, it’s of limited value.