strength in numbers

We are going to build a little game using OOP representing the concept of "strength in numbers". The game will work as follows:

defining a basic Soldier

To define a Soldier, we first need to think of what kind of information it should hold. The two basic actions a Soldier can perform are drawing itself and moving. For displaying, it should have a position, a size, and a color. To add some visual interest to the sketch, we will make the Soldier's size relative to its strength; a stronger Soldier will be larger than a weaker one. For moving, it should also have a speed and a direction. Finally, it should also have a constructor which takes two position parameters to place the Soldier where the mouse is clicked/dragged.

class Soldier {
  // --------------------------------------------------------------------
  // CONSTANTS
  // --------------------------------------------------------------------
  int MIN_SIZE = 10;

  // --------------------------------------------------------------------
  // VARIABLES
  // --------------------------------------------------------------------
  float xPos, yPos;      
  float dX, dY;
  int strength;
  color colour;

  // --------------------------------------------------------------------
  // CONSTRUCTOR
  // --------------------------------------------------------------------
  Soldier(float x, float y) {
    xPos = x;
    yPos = y;
    strength = 1; 
    colour = color(random(100, 255), random(100, 255), random(100, 255)); 

    // randomly set a start direction and speed
    dX = random(3);
    dY = random(3);
    if (random(1) < .5) dX *= -1;
    if (random(1) < .5) dY *= -1;
  }

  // --------------------------------------------------------------------
  // METHODS
  // --------------------------------------------------------------------
  /* moves the soldier and make it bounce off the walls */
  void move() {
    float radius = (strength+MIN_SIZE)/2.0;

    // if the soldier hit a wall, bounce back
    if (yPos+radius >= height) {
      // bottom wall
      dY *= -1;
    } else if (yPos-radius <= 0) {
      // top wall
      dY *= -1;
    }

    if (xPos+radius >= width) {
      // right wall
      dX *= -1;
    } else if (xPos-radius <= 0) {
      // left wall
      dX *= -1;
    }

    xPos += dX;
    yPos += dY;
  }

  /* draws the soldier */
  void draw() {
    // draw the shape
    fill(colour);
    ellipse(xPos, yPos, strength+MIN_SIZE, strength+MIN_SIZE);
  }
}

We can now build a main application to test our Soldier. This application is very similar to the Pulse example from the previous notes, so it should look quite familiar. The only major difference is that we will put all the calculation and analysis code in a new function called step(). For now, all step() does is move all the Soldiers.

// ----------------------------------------------------------------------
// GLOBAL CONSTANTS
// ----------------------------------------------------------------------
int MAX_SOLDIERS = 200;

// ----------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------
int numSoldiers = 0;
Soldier army[] = new Soldier[MAX_SOLDIERS];

// ----------------------------------------------------------------------
// BUILT-IN FUNCTIONS
// ----------------------------------------------------------------------
void setup() {
  size(400, 400);
  smooth();
  noStroke();
}

void draw() {
  background(0);
  step();

  // draw all the soldiers
  for (int i=0; i < numSoldiers; i++) {
    army[i].draw(); 
  }
}

void mousePressed() {
  addSoldier(mouseX, mouseY); 
}

void mouseDragged() {
  addSoldier(mouseX, mouseY); 
}

void keyPressed() {
  if (key == ' ') {
    // clear all
    numSoldiers = 0;
  } 
}

// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------
/* adds a new soldier to the display */
void addSoldier(int newX, int newY) {
  if (numSoldiers < MAX_SOLDIERS) {
    army[numSoldiers] = new Soldier(newX, newY);

    numSoldiers++;
  }
}

/* moves all the soldiers */
void step() {
  for (int i=0; i < numSoldiers; i++) {
    army[i].move();
  }
}				

adding the Soldier's strength

We should now add the Soldier's strength to its draw() method. To keep things simple, we'll make all Soldiers use the same font to draw their strength, so we can set up the font once in the main application (instead of once per Soldier), and speed things up.

// ...

// ----------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------
int numSoldiers = 0;
Soldier army[] = new Soldier[MAX_SOLDIERS];

PFont font;

// ----------------------------------------------------------------------
// BUILT-IN FUNCTIONS
// ----------------------------------------------------------------------
void setup() {
  size(400, 400);
  smooth();
  noStroke();

  // set up the font
  font = loadFont("Georgia.vlw");
  textFont(font, 12);
  textAlign(CENTER, CENTER);
}

// ...

class Soldier {
  // ...

  /* draws the soldier */
  void draw() {
    // draw the shape first
    fill(colour);
    ellipse(xPos, yPos, radius*2+MIN_SIZE, radius*2+MIN_SIZE);

    // draw the number over it
    fill(0);
    text(radius, xPos, yPos);
  }

  // ...	
}

collision detection

We will now add a method collidesWith(Soldier other) to Soldier which will check if the given Soldier is touching the Soldier passed as a parameter, and return true or false. collidesWith(Soldier other) works by comparing the distance between the center points of the two Soldiers with the sum of their radiuses.

   

class Soldier {
  // ...

  /* checks if the current soldier collides with the passed other soldier */
  boolean collidesWith(Soldier other) {
    float distance = dist(xPos, yPos, other.xPos, other.yPos);
    float sumRadius = (strength+MIN_SIZE)/2.0 + (other.strength+MIN_SIZE)/2.0;

    if (distance < sumRadius) {
      return true;
    }
    return false;
  }
}

We can now use this functionality to detect if two Soldiers are touching. We will do this in the main application's step() function. Since a collision is commutative (if A collides with B, then B must collide with A), we will optimize our algorithm by making a Soldier check for collisions only with all the elements that are in a greater position in the array. If for example we have an array of 5 elements, we will make comparisons for 0-1, 0-2, 0-3, 0-4, 1-2, 1-3, 1-4, 2-3, 2-4, 3-4.

// ...

/* moves all the soldiers and look for collisions */
void step() {
  // move all the soldiers
  for (int i=0; i < numSoldiers; i++) {
    army[i].move();
  }

  // see if any two soldiers collide
  for (int i=0; i < numSoldiers-1; i++) {
    Soldier currSoldier = army[i];
    // go through all the current soldier's right neighbours
    for (int j=i+1; j < numSoldiers; j++) {
      Soldier otherSoldier = army[j];
      // if they collide...
      if (currSoldier.collidesWith(otherSoldier)) {
        // ...call a fight
      }
    } 
  }
}

Next, we need to simulate a fight. Remember that the winning Soldier gains the loser's strength and that the loser dies and disappears. This means that the loser must be removed from the army array. We don't want to modify the array too much because moving elements around is a very time consuming operation. A good way of doing things would be to handle fights in two steps:

The advantage of proceeding in this fashion is that we only need to modify army once instead of every time a Soldier dies.

We will do this by adding the isAlive attribute to Soldier, which will get set to false if the Soldier loses a fight. At the end of step(), we will go through the list of Soldiers and only keep the ones that are still alive, i.e. the ones with isAlive still set to true.

// ...

/* moves all the soldiers and if any collide, make them fight */
void step() {
  // move all the soldiers
  for (int i=0; i < numSoldiers; i++) {
    army[i].move();
  }

  // see if any two soldiers collide
  for (int i=0; i < numSoldiers-1; i++) {
    Soldier currSoldier = army[i];
    // go through all the current soldier's right neighbours
    for (int j=i+1; j < numSoldiers; j++) {
      Soldier otherSoldier = army[j];
      // if they collide...
      if (currSoldier.collidesWith(otherSoldier)) {
        // ...call a fight
        fight(currSoldier, otherSoldier);
      }
    } 
  }
  
  // array to hold all soldiers that will live to the next frame
  Soldier survivors[] = new Soldier[MAX_SOLDIERS];
  int numSurvivors = 0;
  
  // keep only the live soldiers for the next round
  for (int i=0; i < numSoldiers; i++) {
    if (army[i].isAlive) {
      survivors[numSurvivors] = army[i];
      numSurvivors++;
    }
  }

  army = survivors;
  numSoldiers = numSurvivors;
}

/* makes two soldiers fight, with the loser dying and the winner gaining his strength */
void fight(Soldier soldier1, Soldier soldier2) {
  Soldier winner;
  Soldier loser;

  // randomly generate the outcome of the fight
  if (random(1) < 0.5) {
    winner = soldier1;
    loser = soldier2;
  } else { 
    winner = soldier2;
    loser = soldier1;
  }

  // make the winner gain the loser's strength
  winner.strength += loser.strength;
  // kill the loser
  loser.isAlive = false;
}

class Soldier {
  // ...

  // --------------------------------------------------------------------
  // VARIABLES
  // --------------------------------------------------------------------
  float xPos, yPos;      
  float dX, dY;
  int strength;
  color colour;
  boolean isAlive;

  // --------------------------------------------------------------------
  // CONSTRUCTOR
  // --------------------------------------------------------------------
  public Soldier(float x, float y) {
    xPos = x;
    yPos = y;
    strength = 1; 
    colour = color(random(100, 255), random(100, 255), random(100, 255)); 
    isAlive = true;

    // randomly set a start direction and speed
    dX = random(3);
    dY = random(3);
    if (random(1) < .5) dX *= -1;
    if (random(1) < .5) dY *= -1;
  }

  // ...
}

Finally, since Soldiers can now die in the middle of a cycle and remain in the army, we'll need to add a check to make sure they are still alive before making them fight. If we don't do that, a dead Soldier may fight with a live Soldier and could actually win!

// ...				

/* moves all the soldiers and if any collide, make them fight */
void step() {
  // move all the soldiers
  for (int i=0; i < numSoldiers; i++) {
    army[i].move();
  }

  // see if any two soldiers collide
  for (int i=0; i < numSoldiers-1; i++) {
    Soldier currSoldier = army[i];
    // if the current soldier is alive...
    if (currSoldier.isAlive) {
      // ...go through all the current soldier's right neighbours
      for (int j=i+1; j < numSoldiers; j++) {
        Soldier otherSoldier = army[j];
        // if the neighbour is alive...
        if (otherSoldier.isAlive) {
          // ...and they collide...
          if (currSoldier.collidesWith(otherSoldier)) {
            // ...call a fight
            fight(currSoldier, otherSoldier);
          }
        }
      }
    } 
  }

  // array to hold all soldiers that will live to the next frame
  Soldier survivors[] = new Soldier[MAX_SOLDIERS];
  int numSurvivors = 0;

  // keep only the live soldiers for the next round
  for (int i=0; i < numSoldiers; i++) {
    if (army[i].isAlive) {
      survivors[numSurvivors] = army[i];
      numSurvivors++;
    }
  }

  army = survivors;
  numSoldiers = numSurvivors;
}

// ...

refining the game

We have fulfilled the requirements of the game, but we still need to perform a very important step: refining our work. This implies adding details and improving the functionality to make the game more interesting and thorough. We will do this by modifying fight() to truly represent the idea of "strength in numbers". Instead of randomly selecting the winner, we will base our calculations on odds in favor of the stronger Soldier. The greater the difference in strength between Soldiers, the better chances the stronger one has of winning.

/* makes two soldiers fight, with the loser dying and the winner gaining his strength */
/* the odds of winning are relative to the soldiers' strengths */
void fight(Soldier soldier1, Soldier soldier2) {
  Soldier smaller;
  Soldier greater;
  Soldier winner;
  Soldier loser;

  // check which soldier has a greater strength than the other
  if (soldier1.strength < soldier2.strength) {
    smaller = soldier1;
    greater = soldier2;
  } else {
    smaller = soldier2;
    greater = soldier1;
  }

  // randomly generate the outcome of the fight taking the ratio 
  // of the soldiers' strength into account
  float odds = smaller.strength/greater.strength;
  if (random(1) > odds) {
    winner = greater;
    loser = smaller;
  } else { 
    winner = smaller;
    loser = greater;
  }

  // make the winner gain the loser's strength
  winner.strength += loser.strength;
  // kill the loser
  loser.isAlive = false;
}

Finally, let's blend the loser's color into the winner's, using ratios based on their strengths to end up with the finished game. Since strength is an int, whenever we divide a smaller strength by a greater one, we'll perform integer division and end up with 0. The trick to fix this is to convert one of the ints using float(), which will make the result of the division also a float.

/* makes two soldiers fight, with the loser dying and the winner gaining his strength */
/* the odds of winning are relative to the soldiers' strengths */
void fight(Soldier soldier1, Soldier soldier2) {
  Soldier smaller;
  Soldier greater;
  Soldier winner;
  Soldier loser;

  // check which soldier has a greater strength than the other
  if (soldier1.strength < soldier2.strength) {
    smaller = soldier1;
    greater = soldier2;
  } else {
    smaller = soldier2;
    greater = soldier1;
  }

  // randomly generate the outcome of the fight taking the ratio 
  // of the soldiers' strength into account
  float odds = smaller.strength/greater.strength;
  if (random(1) > odds) {
    winner = greater;
    loser = smaller;
  } else { 
    winner = smaller;
    loser = greater;
  }

  // blend the loser's color into the winner's
  winner.colour = lerpColor(
    winner.colour, 
    loser.colour,  
    min(1.0, loser.strength/float(winner.strength))
  );
  // make the winner gain the loser's strength
  winner.strength += loser.strength;
  // kill the loser
  loser.isAlive = false;
}

Processing Workshop

Elie Zananiri
Alberta College of Art + Design
3-5 April 2008