積雲が映像制作したMV『RANGEFINDER』公開中
専門88IO

ProcessingでPONGゲーム!!

専門

学校でプログラミングの課題が出ました。ProcessingによるPONGゲームの作成です。

グローバル変数を多用してsetup()とdraw()に殴り書きすることも多いですが、今回はJavaの復習も兼ねてなるべくクラスを使って読みやすいようにしてみました。

完成図

setup()とdraw()

なるべく簡潔に済ませます。

最初にグローバルスコープで変数定義、setup()で各クラスを初期化し、draw()でメンバの更新・オブジェクトの描画を行います。

LPaddle lp;
RPaddle rp;
Ball ball;
Screen sc;

void setup() {
  size(960, 540);
  background(0);
  smooth();
  frameRate(30);

  lp = new LPaddle(width / 20, height / 2);
  rp = new RPaddle(19 * width / 20, height / 2);
  ball = new Ball();
  sc = new Screen();
}

void draw() {
  sc.update(ball);
  rp.update();
  lp.update();
  ball.update(rp, lp);

  sc.draw();
  rp.draw();
  lp.draw();
  ball.draw();
}

ここで用いた4つのクラス

  • LPaddle
  • RPaddle
  • Ball
  • Screen

はすべてupdate()とdraw()を保有しており、update()で座標等のパラメータを更新、draw()でオブジェクトを描画します。

以下では、各クラスの解説をしていきます。

パドルの作成

Paddle

仕様上、左のプレイヤーはキー移動で右のプレイヤーはマウス移動です。

公平性を期すために、移動速度等の情報を保持する親クラスを作成します。

class Paddle {
  int x, y;
  final int w;
  final float speed;

  Paddle(int _x, int _y) {
    x = _x;
    y = _y;
    w = 60;
    speed = 10;
  }

  void update() {
    if (y - w/2 < 0) {
      y = w/2;
    } else if (y + w/2 > height) {
      y = height - w/2;
    }
  }

  void draw() {
    fill(255);
    stroke(255);
    strokeWeight(10);
    line(x, y - w/2, x, y + w/2);
  }
}

Paddleクラスはパドルの位置座標と移動速度、また幅(widthとしたかったが、すでに割り当てられているのでwとした)を保持します。

update()でパドルが画面外に出ないようにチェックします。

draw()はパドルの描画処理です。

LPaddle

次に左側のプレイヤー固有の処理を記述していきます。

Paddleクラスを継承し、update()の部分に座標移動処理を書き加えます。左側はキーボード入力で上下移動です。

class LPaddle extends Paddle {
  LPaddle(int _x, int _y) {
    super(_x, _y);
  }

  void update() {
    if (keyPressed) {
      switch (key) {
        case 'w':
          y -= speed;
          break;
        case 's':
          y += speed;
          break;
      }
    }
    super.update();
  }
}

今回はwキーで上移動、sキーで下移動にしました。

RPaddle

左と来たら右です。右側はマウスの位置で上下移動させます。

class RPaddle extends Paddle {
  RPaddle(int _x, int _y) {
    super(_x, _y);
  }

  void update() {
    if (abs(mouseY - y) < speed) { 
      y = mouseY;
    } else if (mouseY > y) {
      y += speed;
    } else if (mouseY < y) {
      y -= speed;
    }

    super.update();
  }
}

ここで気をつけなければならないのは、if (abs(mouseY - y) < speed)  { y = mouseY; }の部分です。この処理を省くと、マウスを動かさなくてもパドルが上下に振動してしまいます。上下移動量はspeedに依存しているので、ぴったりmouseYと重なることが少ないのが原因です。

Ball

このゲームの要です。コードは全体で230行程になりましたが、約半分をBallクラスが占めています。

Ballクラスでは、

  • ボールの発射処理
  • ボールの移動処理
  • 上下壁・左右パドルの衝突判定
  • 衝突時の移動方向の反転
  • 衝突時の加速処理
  • ゴール処理

を行います。(多いっ、設計ミス)

class Ball {
  int x, y;
  int nx, ny;
  int size;
  float speed;
  float direction;
  final float defaultSpeed, acceleration;
  boolean isPush;
  boolean isLGoal, isRGoal;

  Ball() {
    ellipseMode(RADIUS);
    x = nx =  width/ 2;
    y = ny = height / 2;
    size = 10;
    speed = defaultSpeed = 10;
    direction = 0;
    acceleration = 1.05;
    isPush = false;
    isLGoal = false;
    isRGoal = false;
  }

  void update(RPaddle rp, LPaddle lp) {
    //animation before push, press space key
    if (!isPush) {
      if (keyPressed && key == ' ')  isPush = true;
      x = width / 2;
      y = height / 2;
      isLGoal = false;
      isRGoal = false;
      speed = defaultSpeed;
      direction = PI/30 * (frameCount % 60);
      return;
    }

    if (keyPressed && key == 'C') {
      isPush = false;
      return;
    }

    // Next (x, y) position
    nx = x + int(speed * cos(direction));
    ny = y + int(speed * sin(direction));

    // bound y
    if (ny <= size) { 
      direction *= -1;
      x = nx; 
      y = 2 * size - ny;
      speed *= sqrt(1 + (sq(acceleration) - 1)*sq(sin(direction)));
      return;
    } else if (ny >= height - size) {
      direction *= -1;
      x = nx;
      y = 2 * height - 2 * size - ny;
      speed *= sqrt(1 + (sq(acceleration) - 1)*sq(sin(direction)));
      return;
    }
    // bound x
    if (nx <= lp.x + size) {
      if (ny >= lp.y - lp.w/2 && ny <= lp.y + lp.w/2) {
        direction = 3*PI - direction;
        x = 2 * lp.x - nx + 2 * size;
        y = ny;
        speed *= sqrt(1 + (sq(acceleration) - 1)*sq(cos(direction)));
        return; 
      } 
    } else if (nx >= rp.x - size) {
      if (ny >= rp.y - rp.w/2 && ny <= rp.y + rp.w/2) {
        direction = 3*PI - direction;
        x = 2 * rp.x - nx - 2 * size;
        y = ny;
        speed *= sqrt(1 + (sq(acceleration) - 1)*sq(cos(direction)));
        return;
      }
    }

    x = nx;
    y = ny;

    if (x < 0) { 
      isPush = false; 
      isRGoal = true; 
    } else if (x > width) {
      isPush = false;
      isLGoal = true;
    }
  }

  void draw() {
    fill(255);
    strokeWeight(2);
    ellipse(x, y, size, size);
    stroke(150);
    arc(x, y, size*0.8, size*0.8, direction-PI/6, direction+PI/6);
  }
}

ボールは中心座標で一定速度で回転しており、Spaceキーで方向を確定、発射されます。そのため、発射される方向が分かりやすいようにボールに模様をつけました。

ボールの移動量は絶対値で定義します。移動方向はラジアンで指定し、x, y方向の移動量を算出しています。

加速は、衝突した壁の法線方向の移動量を加速度分乗算することで処理しています。

speed_x = speed * cos( direction )
speed_y = speed * sin( direction )

とした上で、

上下: speed = sqrt( sq( speed_x ) + sq( acceleration * speed_y ) ) 
左右: speed = sqrt( sq( acceleration * speed_x ) + sq( speed_y ) ) 

と更新しています。(コード内では整理して計算量を減らしてはいます)

当たり判定は移動予定の座標をnx, nyに格納し、その座標が壁に重なるかどうかで判定します。また、当たっている場合はめり込んだ分を反転してx, y を更新しています。パドルとの当たり判定にパドルの位置座標が必要となるので、update()の引数にLPaddleとRPaddleを渡しています。

最後にゴール判定です。単純にボールが画面の左右に飛び出たときにゴールしていると判断します。isPushを初期化することでボールは発射前のアニメーションに戻り、isLGoal, isRGoalのフラグでスクリーンの色や点数を更新します。(このメンバ変数はScreenで利用します。)

Screen

Screenクラスでは、各プレイヤーの得点の保持と表示、ゴール時の画面フラッシュを行います。

class Screen {
  final color defaultBgColor, lcolor, rcolor;
  color bgcolor;
  int lpoint, rpoint;

  Screen() {
    textAlign(CENTER);
    textSize(100);
    defaultBgColor = bgcolor = #000000;
    lcolor = #ff0000;
    rcolor = #0000ff;
    lpoint = 0;
    rpoint = 0;
  }

  void update(Ball ball) {
    if (ball.isLGoal) {
      bgcolor = lcolor;
      lpoint++;
    } else if (ball.isRGoal) {
      bgcolor = rcolor;
      rpoint++;
    } else {
      bgcolor = defaultBgColor;
    }
  }

  void draw() {
    background(bgcolor);
    fill(lcolor);
    text(lpoint, width / 4, height / 4);
    fill(rcolor);
    text(rpoint, width * 3 / 4, height / 4);
  }
}

update()でBallクラスを受け取り、ゴール判定を取得して、得点の更新等を行っています。

他のクラスよりは分かりやすいかと。

ただひとつ注意として、Screenのdraw()はbackground()を含み画面の初期化を担っているので、他クラスのdraw()よりも先に実行する必要があります。

PONGゲーム ソースコード全体

以下、今回作成したPONGゲームのコードになります。(本文はここで終わり)

LPaddle lp;
RPaddle rp;
Ball ball;
Screen sc;

void setup() {
  size(960, 540);
  background(0);
  smooth();
  frameRate(30);

  lp = new LPaddle(width / 20, height / 2);
  rp = new RPaddle(19 * width / 20, height / 2);
  ball = new Ball();
  sc = new Screen();
}

void draw() {
  sc.update(ball);
  rp.update();
  lp.update();
  ball.update(rp, lp);

  sc.draw();
  rp.draw();
  lp.draw();
  ball.draw();
}

class Paddle {
  int x, y;
  final int w;
  final float speed;

  Paddle(int _x, int _y) {
    x = _x;
    y = _y;
    w = 60;
    speed = 10;
  }

  void update() {
    if (y - w/2 < 0) {
      y = w/2; 
    } else if (y + w/2 > height) {
      y = height - w/2;
    }
  }

  void draw() {
    fill(255);
    stroke(255);
    strokeWeight(10);
    line(x, y - w/2, x, y + w/2);
  }
}

class LPaddle extends Paddle {
  LPaddle(int _x, int _y) {
    super(_x, _y);
  }

  void update() {
    if (keyPressed) {
      switch (key) {
        case 'w':
          y -= speed;
          break;
        case 's':
          y += speed;
          break;
      }
    }
    super.update();
  }
}

class RPaddle extends Paddle {
  RPaddle(int _x, int _y) {
    super(_x, _y);
  }

  void update() {
    if (abs(mouseY - y) < speed) { 
      y = mouseY; 
    } else if (mouseY > y) {
      y += speed;
    } else if (mouseY < y) {
      y -= speed;
    }

    super.update();
  }
}

class Ball {
  int x, y;
  int nx, ny;
  int size;
  float speed;
  float direction;
  final float defaultSpeed, acceleration;
  boolean isPush;
  boolean isLGoal, isRGoal;

  Ball() {
    ellipseMode(RADIUS);
    x = nx =  width/ 2;
    y = ny = height / 2;
    size = 10;
    speed = defaultSpeed = 10;
    direction = 0;
    acceleration = 1.05;
    isPush = false;
    isLGoal = false;
    isRGoal = false;
  }

  void update(RPaddle rp, LPaddle lp) {
    //animation before push, press space key
    if (!isPush) {
      if (keyPressed && key == ' ')  isPush = true;
      x = width / 2;
      y = height / 2;
      isLGoal = false;
      isRGoal = false;
      speed = defaultSpeed;
      direction = PI/30 * (frameCount % 60);
      return;
    }

    if (keyPressed && key == 'C') {
      isPush = false;
      return;
    }

    // Next (x, y) position
    nx = x + int(speed * cos(direction));
    ny = y + int(speed * sin(direction));

    // bound y
    if (ny <= size) { 
      direction *= -1; 
      x = nx; y = 2 * size - ny; 
      speed *= sqrt(1 + (sq(acceleration) - 1)*sq(sin(direction))); 
      return;
    } else if (ny >= height - size) {
      direction *= -1;
      x = nx;
      y = 2 * height - 2 * size - ny;
      speed *= sqrt(1 + (sq(acceleration) - 1)*sq(sin(direction)));
      return;
    }
    // bound x
    if (nx <= lp.x + size) { 
      if (ny >= lp.y - lp.w/2 && ny <= lp.y + lp.w/2) { 
        direction = 3*PI - direction; 
        x = 2 * lp.x - nx + 2 * size;
        y = ny; speed *= sqrt(1 + (sq(acceleration) - 1)*sq(cos(direction)));
        return; 
      } 
    } else if (nx >= rp.x - size) {
      if (ny >= rp.y - rp.w/2 && ny <= rp.y + rp.w/2) {
        direction = 3*PI - direction;
        x = 2 * rp.x - nx - 2 * size;
        y = ny;
        speed *= sqrt(1 + (sq(acceleration) - 1)*sq(cos(direction)));
        return;
      }
    }

    x = nx;
    y = ny;

    if (x < 0) { 
      isPush = false; 
      isRGoal = true; 
    } else if (x > width) {
      isPush = false;
      isLGoal = true;
    }
  }

  void draw() {
    fill(255);
    strokeWeight(2);
    ellipse(x, y, size, size);
    stroke(150);
    arc(x, y, size*0.8, size*0.8, direction-PI/6, direction+PI/6);
  }
}

class Screen {
  final color defaultBgColor, lcolor, rcolor;
  color bgcolor;
  int lpoint, rpoint;

  Screen() {
    textAlign(CENTER);
    textSize(100);
    defaultBgColor = bgcolor = #000000;
    lcolor = #ff0000;
    rcolor = #0000ff;
    lpoint = 0;
    rpoint = 0;
  }

  void update(Ball ball) {
    if (ball.isLGoal) {
      bgcolor = lcolor;
      lpoint++;
    } else if (ball.isRGoal) {
      bgcolor = rcolor;
      rpoint++;
    } else {
      bgcolor = defaultBgColor;
    }
  }

  void draw() {
    background(bgcolor);
    fill(lcolor);
    text(lpoint, width / 4, height / 4);
    fill(rcolor);
    text(rpoint, width * 3 / 4, height / 4);
  }
}

コメント

タイトルとURLをコピーしました