Triangular Pong for three players [game]

For no good reason, I want to make a triangular version of Pong so three walls, three paddles, one ball bouncing from the three paddles or going past a paddle and scoring. Should be fairly simple but enjoyably challenging to code but before I start, I wanted to check if it has already been done?

The major difference between three person pong and two person pong will be determining who has scored. In two person pong, it’s the other person. In three person pong, it’s the player who most recently hit the ball.

If it has not already been done, who wants to join in the fun?

1 Like

OK. I have started with the ball bouncing on one of the sides of the equilateral triangle. I am hoping @quark with his greater expertise in maths will jump in here…

float incidenceAngle = 10;
float surfaceAngle = 150;
float aR;

void setup() {
  size(640, 640);
  background(255);
  noLoop();
}

void draw() {
  angleReflect(incidenceAngle, surfaceAngle);

  print(aR);


  translate(320, 320);

  strokeWeight(8);
  float va = radians(surfaceAngle);
  line(sin(va) * -150, cos(va) * -150, sin(va) * 150, cos(va) * 150); //the surface

  stroke(0, 127, 0);
  strokeWeight(4);
  float vb = radians(incidenceAngle);
  line(sin(vb) * -150, cos(vb) * -150, 0, 0); //incidence

  stroke(0, 0, 127);
  strokeWeight(4);
  float vc = radians(aR);
  line(0, 0, sin(vc) * 150, cos(vc) * 150); //reflection



}

void angleReflect(float iA, float sA) {

  aR = sA * 2 - iA;

  if (aR >= 360) {
    aR = aR - 360;
  } else if (aR < 0) {
    aR = aR + 360;
  }
}
1 Like

Effectively you have a ball bouncing off a flat target that may or may not be axis aligned i.e. at any arbitrary angle. The solution is to use vectors (PVector) to represent

  • the start and end positions of the target
  • the current position of the ball
  • the velocity of the ball (velocity combines direction and speed)

We are not interested any of the actual angles all we need is a vector representing the velocity after reflection and this can be done with vector maths.

2 Likes

This sketch demonstrates the calculation of a reflected vector.

PVector start, end, mid; // screen positions
PVector normal; // unit vector for target normal
PVector fromXY, toXY; // screen positions
PVector toVec; // Reflected vector relative to hit position on target

void setup() {
  size(400, 400);
  textSize(16);
  textAlign(LEFT, CENTER);
  initTarget();
}

void draw() {
  background(240);
  fill(0);
  noStroke();
  text("[M] to move target to random", 20, 36);
  text("Click mouse to choose position for in vector start", 20, 18);
  drawTarget();
  if (fromXY != null)
    drawReflection();
}

void drawReflection() {
  push();
  stroke(255, 0, 255);
  strokeWeight(1);
  line(fromXY.x, fromXY.y, mid.x, mid.y);
  line(mid.x, mid.y, toXY.x, toXY.y);
  pop();
}

void mouseClicked() {
  fromXY = new PVector(mouseX, mouseY);
  toVec = getReflection(mid, fromXY, normal);
  toXY = PVector.add(toVec, mid);
}

void keyTyped() {
  if (key == 'm') {
    initTarget();
    fromXY = null;
  }
}

void drawTarget() {
  push();
  stroke(0);
  strokeWeight(2);
  line(start.x, start.y, end.x, end.y);
  noStroke();
  fill(0, 160, 0);
  ellipse(start.x, start.y, 8, 8);
  fill(160, 0, 0);
  ellipse(end.x, end.y, 8, 8);
  stroke(0, 0, 200);
  strokeWeight(1.1f);
  line(mid.x, mid.y, mid.x + 50 * normal.x, mid.y + 50 * normal.y);
  pop();
}

// hitSpot and fromSpot represent screen coordinates
// targetNorm is the target's normal vector (must be normalised)
PVector getReflection(PVector hitSpot, PVector fromSpot, PVector targetNorm) {
  PVector inVec = PVector.sub(hitSpot, fromSpot);
  float dot = inVec.dot(targetNorm);
  float nx = inVec.x -2 * dot * targetNorm.x;
  float ny = inVec.y -2 * dot * targetNorm.y;
  return new PVector(nx, ny);
}

// Create a random target.
void initTarget() {
  mid = new PVector(width/2 + random(-50, 50), height/2 + random(-50, 50));
  float a = random(TWO_PI), dx = 50 * cos(a), dy = 50 * sin(a);
  start = new PVector(mid.x-dx, mid.y-dy);
  end = new PVector(mid.x+dx, mid.y+dy);
  normal = new PVector(start.y-end.y, end.x-start.x);
  normal.normalize();
}
3 Likes

This is excellent, thank you. I am guessing there are only three possible angles of the target. These correspond to the three sides of an equilateral triangle. The paddles will be parallel to the sides. I am wondering how the angle of incidence changes. In two person Pong, if the ball is released so it moves horizontally, will its angle ever change?? I am just thinking about this before moving to the next part of the project!!
Wondering whether the direction of movement of the paddle upon impact will contribute to the change of angle of the ball…

1 Like

Good questions. Replace the getReflections method with this code


/**
 Calculate the reflection vector such that angle of reflection equals 
 angle of incidence.
 (Method overloading technique)
 */
PVector getReflection(PVector hitSpot, PVector fromSpot, PVector targetNorm) {
  return getReflection(hitSpot, fromSpot, targetNorm, 2);
}

/**
 Get the reflection vector given the initial point, reflection point on 
 target and the target surface normal.
 
 The fourth parameter f modifies the angle of reflection
 f < 2 >> angle of reflection > angle of incidence
 f = 2 >> angle of reflection = angle of incidence
 f > 2 >> angle of reflection < angle of incidence
 
 Experimentation shows values in the range 1.3 to 3.5 produce reflections
 that 
 
 Note: incident and reflected angles are measured from the surface normal vector.
 Note: hitSpot and fromSpot are in screen coordinates
 
 @parameter hitSpot the reflection point
 @parameter fromSpot the reflection point
 @parameter targetNorm is the target's normal vector (must be normalised)
 @parameter f reflection angle modifier
 */
PVector getReflection(PVector hitSpot, PVector fromSpot, PVector targetNorm,
            float f) {
  PVector inVec = PVector.sub(hitSpot, fromSpot);
  float dot = inVec.dot(targetNorm);
  float nx = inVec.x -f * dot * targetNorm.x;
  float ny = inVec.y -f * dot * targetNorm.y;
  return new PVector(nx, ny);
}

and in mouseClicked change the second line to

toVec = getReflection(mid, fromXY, normal, f);

Changing the value of f changes the reflection angle. It means you could have f =2 in the paddle centre and 1.3 near the ends.

You can modify the getReflection method to suit your needs, for instance modifying f based on paddle movement and speed etc. Experimentation is the name of the game.

1 Like

the ball can start at an random angle.

The paddle could be straight in the middle section and outward (10%) be round, so the ball reflects differently.

That be cool, increase the angle a bit or give it a spin

2 Likes

Some things to think about?

  1. How do we get simultaneous input from several players?
    It seems to me that a single device e.g. desktop / laptop / tablet can only realistically support single players The solution would be to have the game on a server and each player would have their own client device which would place limitations on others wanting to set up their own game environment. BTW I am prepared to be shot down on this because it is not by area of expertise.
  2. Limitations on game area geometry.
    What follows is based on my personal experience of trying to get a ball bouncing inside a concave polygon. In a concave polygon some of the internal angles are \lt90^\circ whereas in a convex polygon they are \ge90^\circ and \lt180^\circ. In a 3 player game the game area would be an equilateral triangle i.e. a concave polygon with three internal angles of 60^\circ.
    Now the problem is that a ball bouncing between two vectors with an internal angle \lt90^\circ angle would be forced towards the intersection of the two vectors and will eventually escape outside the polygon. :disappointed: One solution would be to vary the reflection angle depending on the speed and travel direction of the paddle but the reflections would probably look unrealistic in this situation.

:grin:

1 Like

For @quark’s 2nd point, I would strongly recommend using a slightly hexagonal playing field. Keep it mostly a triangle, but cut off the sharp corners so you have a hexagon with side lengths alternating something like 4, 1, 4, 1, 4, 1.

If the ball gets jammed into the corner of a triangle, when both players have their paddles there to catch it, the ball can start vibrating back and forth between them with increasing speed eventually resulting in multiple bounces per frame which your physics solution likely won’t handle (or won’t want to have to handle) causing the ball to phase through the paddle.

1 Like

With a desktop, each player can have two keys. One for each direction of the paddle. This is nice and retro…

Mini bluetooth, or USB, keyboards with two buttons.

On a tablet, different areas of the screen to touch with priority given when the ball is near to the player’s wall…maybe.

Because the input is so simple, (go that way, go the other way, or do nothing) it should be possible. Much of the game play is mentally calculating angles and timing.

Exactly! But maybe rounded corners using dodg’em car or pinball aesthetics so when the ball goes into a corner it bounces back into the middle of the pitch.

Spin would be very nice. If the bat is moving ‘backwards’ or towards the ball’s path …
Oh, let’s have a picture instead. Moving the bat one way subtracts a little from the angle (magenta) and the other way adds a little (cyan).
bounce

OR

bounce2

Do I give f a value?
float f = 1.2;

Give it any value you like if it provides the reflection you want. :grinning:

1 Like

It works wonderfully, and so now to experimentation…

1 Like

I made a triangle and 3 movable paddles


// Pong in a Triangle (2D) for 3 players
// USE q/w and g/h and k/l respectively

// see https://discourse.processing.org/t/triangular-pong-game/42548/16

// triangle data
PVector[] listCorners =  new PVector[3];

// Paddle data
Paddle[] listPaddles = new Paddle[3];

// we need this array to allow for multiple keys pressed at the same time
// cf. https://discourse.processing.org/t/keypressed-for-multiple-keys-pressed-at-the-same-time/18892
boolean[] moveKeys = new boolean[256];

// --------------------------------------------------------------------
// Core functions

void setup() {
  size( 800, 800);

  // make corners
  for (int i = 0; i < 3; i++) {
    float angle = i * (TWO_PI/3.0) + TWO_PI/20.0; // triangle a bit rotated 
    float x1= width/2+cos(angle) * ((height / 2) -60);
    float y1= height/2+sin(angle) * ((height / 2) -60);
    listCorners [i] = new PVector( x1, y1) ;
  }

  // Make the paddles / players: the corners between which the paddle moves and its 2 keys and color
  listPaddles[0] = new Paddle ( listCorners [0], listCorners [1], 'w', 'q', color(255, 0, 0)  ); // r 
  listPaddles[1] = new Paddle ( listCorners [1], listCorners [2], 'g', 'h', color(0, 255, 0)  ); // g
  listPaddles[2] = new Paddle ( listCorners [2], listCorners [0], 'k', 'l', color(0, 0, 255)  ); // b
} // setup

void draw() {
  background(255);

  // show game field
  showGameField();

  // manage paddles
  for (Paddle pd : listPaddles) {
    pd.checkKeys();
    pd.display();
  }//for
} // draw

// --------------------------------------------------------------------
// Input functions

void keyPressed() {
  if (key>32&&key<256)
    moveKeys[key] = true;
}//func

void keyReleased() {
  if (key>32&&key<256)
    moveKeys[key] = false;
}//func

// --------------------------------------------------------------------
// Other functions

void showGameField() {
  // show triangle

  stroke(0);
  noFill();
  triangle (listCorners[0].x, listCorners[0].y,
    listCorners[1].x, listCorners[1].y,
    listCorners[2].x, listCorners[2].y);

  fill(255, 2, 2);//red
  for (PVector pv : listCorners) {
    ellipse(pv.x, pv.y, 12, 12);
  }//for
}//func

// ====================================================================

class Paddle {

  final PVector from, to; // its two corner's data (begin and end of line)
  final char leftC, rightC; // its keys 

  float amt=0.5; // pos between 0 and 1
  final float speed1 = 0.01;
  final float amtHalfPaddleWidth  = .042;

  final color colorMy;

  int score=0;

  // -----------------

  //constr
  Paddle(PVector f_, PVector t_,
    char left_, char right_,
    color col_) {

    from =  f_;
    to   =  t_;

    leftC  = left_;
    rightC = right_;

    colorMy=col_;
  }//constr

  // -----------------

  void display() {
    // display Paddle

    //calc start and end of paddle line
    float x1=lerp(from.x, to.x, amt-amtHalfPaddleWidth);
    float y1=lerp(from.y, to.y, amt-amtHalfPaddleWidth);
    float x2=lerp(from.x, to.x, amt+amtHalfPaddleWidth);
    float y2=lerp(from.y, to.y, amt+amtHalfPaddleWidth);

    // draw paddle
    strokeWeight(8);
    stroke(colorMy);
    line (x1, y1,
      x2, y2 );
    //quad (x1, y1, x1+2, y1,
    //  x2, y2, x2+2, y2 );
    strokeWeight(1);
  }//func

  void checkKeys() {
    // check keys / move paddle

    if (moveKeys[leftC]) {
      amt-=speed1;
    } else if (moveKeys[rightC]) {
      amt+=speed1;
    }

    // check values
    if (amt-amtHalfPaddleWidth<0)
      amt=amtHalfPaddleWidth;
    if (amt+amtHalfPaddleWidth>1)
      amt=1-amtHalfPaddleWidth;
  }//func
  //
} //class
//

The code is wonderful! We now have a game that is only missing a ball, and a ball that is only missing a game. Before we introduce the two to each other, I am wondering whether you want to move this discussion to a different forum that is better for project planning and development. I am beginning to think we should produce and publish this Triangular Pong on a platform such as itch.io.
What do you think?

PS I have been researching user interaction and ‘the look’.

@Chrisir and @quark and @scudly

1 Like

I don’t have the math to proceed here. (Paddle / Ball / PVector / collision)

But you can use my stuff freely.

Chrisir

P.S.

maybe mine works better when you replace the function keyPressed with var in draw() like if(keyPressed)…

(there is no var to replace the keyReleased() function apparently though )

1 Like

OK!

I have made some minor tweaks (see code below).

  1. Changed the keys being used so the three players don’t get tangled up. q/w for one, o/p for another, and v/b for the third.

  2. Made the paddles rectangular and simpler.

  3. Reduced the colours to get closer to the aesthetics of a Cathode Ray Tube.

Next we need to avoid the paddles overlapping… any ideas?

Then we need to discuss how to add skill, variability and unpredictability to the shots. Skill, variability and unpredictability are probably all the same thing. I will list some options: The angle of reflection is proportional to the distance from the centre of the paddle, so the ball bounces more towards the ends of the paddle. The approach I like might not be possible, but let’s see…

bounce2

If the paddle is moving in the direction of the cyan arrow then the reflection of the ball is as cyan. If the paddle is moving in the direction of the magenta arrow then the reflection of the ball is as magenta. What do you think?

// Pong in a Triangle (2D) for 3 players
// USE q/w and v/b and o/p respectively

// see https://discourse.processing.org/t/triangular-pong-game/42548/16

// triangle data
PVector[] listCorners =  new PVector[3];

// Paddle data
Paddle[] listPaddles = new Paddle[3];

// we need this array to allow for multiple keys pressed at the same time
// cf. https://discourse.processing.org/t/keypressed-for-multiple-keys-pressed-at-the-same-time/18892
boolean[] moveKeys = new boolean[256];

// --------------------------------------------------------------------
// Core functions

void setup() {
  size( 800, 800);


  // make corners
  for (int i = 0; i < 3; i++) {
    float angle = i * (TWO_PI/3.0) + TWO_PI/20.0;
    float x1= width/2+cos(angle) * ((height / 2) -60);
    float y1= height /2+sin(angle) * ((height / 2) -60);
    listCorners [i] = new PVector( x1, y1) ;
  }

  // Make The paddles / players: the corners between the paddle moves and its 2 keys
  listPaddles[0] = new Paddle ( listCorners [0], listCorners [1], 'b', 'v', color(255, 255, 255)  );
  listPaddles[1] = new Paddle ( listCorners [1], listCorners [2], 'q', 'w', color(255, 255, 255)  );
  listPaddles[2] = new Paddle ( listCorners [2], listCorners [0], 'o', 'p', color(255, 255, 255)  );
} // setup

void draw() {
  background(0);

  // show game field
  showGameField();

  // manage paddles
  for (Paddle pd : listPaddles) {
    pd.checkKeys();
    pd.display();
  }//for
} // draw

// --------------------------------------------------------------------
// Input functions

void keyPressed() {
  if (key>32&&key<256)
    moveKeys[key] = true;
}//func

void keyReleased() {
  if (key>32&&key<256)
    moveKeys[key] = false;
}//func

// --------------------------------------------------------------------
// Other functions

void showGameField() {
  // show triangle

  stroke(0);
  noFill();
  triangle (listCorners[0].x, listCorners[0].y,
    listCorners[1].x, listCorners[1].y,
    listCorners[2].x, listCorners[2].y);

  fill(255, 2, 2);//red
  for (PVector pv : listCorners) {
    ellipse(pv.x, pv.y, 12, 12);
  }//for
}//func

// ====================================================================

class Paddle {

  final PVector from, to; // its 2 corner data
  final char leftC, rightC;

  float amt=0.5;
  final float speed1 = 0.01;
  final float amtHalfPaddleWidth  = .042;

  final color colorMy;

  int score=0;

  // -----------------

  //constr
  Paddle(PVector f_, PVector t_,
    char left_, char right_,
    color col_) {

    from =  f_;
    to   =  t_;

    leftC  = left_;
    rightC = right_;

    colorMy=col_;
  }//constr

  // -----------------

  void display() {
    // display Paddle

    //calc start and end of paddle line
    float x1=lerp(from.x, to.x, amt-amtHalfPaddleWidth);
    float y1=lerp(from.y, to.y, amt-amtHalfPaddleWidth);
    float x2=lerp(from.x, to.x, amt+amtHalfPaddleWidth);
    float y2=lerp(from.y, to.y, amt+amtHalfPaddleWidth);

    // draw paddle
    strokeWeight(12);
    strokeCap(PROJECT);
    stroke(colorMy);
    line (x1, y1,
      x2, y2 );
    //quad (x1, y1, x1+2, y1,
    //  x2, y2, x2+2, y2 );
    strokeWeight(1);
  }//func

  void checkKeys() {
    // check keys / move paddle

    if (moveKeys[leftC]) {
      amt-=speed1;
    } else if (moveKeys[rightC]) {
      amt+=speed1;
    }

    // check values
    if (amt-amtHalfPaddleWidth<0)
      amt=amtHalfPaddleWidth;
    if (amt+amtHalfPaddleWidth>1)
      amt=1-amtHalfPaddleWidth;
  }//func
  //
} //class
//

This is a little study to detect the direction of movement of a paddle. In this case it is paddle[0]. If key ‘b’ has been pressed twice, the paddle is moving in the direction of b. If key ‘v’ has been pressed twice, the paddle is moving in the direction of v. I am sure it can be greatly improved, but it is a start…

boolean goingRight = true;

void setUp() {
}

void draw() {
}


void keyPressed() {
  if ((key == 'b') && (goingRight == true)) {
    println("Paddle is moving towards b");
  } else if ((key == 'b') && (goingRight == false)) {
    goingRight = true;
  }



  if ((key == 'v') && (goingRight == false)) {
    println("Paddle is moving towards v");
  } else if ((key == 'v') && (goingRight == true)) {
    goingRight = false;
  }
}
1 Like