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:
- The window will contain soldiers, which are basically circles with a number in their center. This number represents their strength.
- Soldiers are created by clicking or dragging the mouse.
- When the soldiers are created, they start moving in a random direction at a random speed, bouncing off the walls.
- When two soldiers collide, they get in a fight. The winner of the fight gains the strength of the loser; the loser dies and disappears.
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:
- First, we look for collisions, call fights, and flag the losers for removal. The army is not modified in this step.
- Once we have called fights for all combinations of Soldiers, we can then look for all the losers and get rid of them in one shot.
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;
}