Processingで3Dシューティングゲーム

Processingでの3Dゲーム

ProcessingのloadShape/shape関数を使うと簡単に3Dモデルを描画できます。迫りくる蚊を打ち落とすゲームを作ってみました。
mosquito1

  1. 地面と木の描画
  2. 上下左右キーによる視点移動
  3. 弾丸発射
  4. 一定間隔で蚊を追加
  5. GameOverやスコアの処理
    といった手順で実装を進るとよいでしょう。

視点の向きは水平方向の角度thetaHと、垂直方向の角度thetaVで管理しています。
mosquito2
これらの角度から注視点をもとめてカメラの向きを制御しています。

  float x = 100*cos(radians(thetaH));
  float z = 100*sin(radians(thetaH));
  float y = 100*sin(radians(thetaV));
  camera(0, 0, 0, x, y, z, 0, 1, 0);

蚊、木、ボールと描画対象はModelクラスで管理しています。posは座標、speedは1フレームに移動する量です。蚊は50フレーム毎に発生させていますが、中心に向かうようにspeedを設定する必要があります。その処理を以下のコードで行っています。

  float theta = random(360);
  float mx = cos(radians(theta)) * random(600, 800);
  float mz = sin(radians(theta)) * random(600, 800);
  PVector p = new PVector(mx, random(-100, 100), mz);
  PVector d = p.copy().normalize().mult(-1.5);
 mosquitos.add(new Model(p, d, mosquito));

mosquito3
ランダムな場所に蚊を発生させていますが、中心からある程度離れた位置にしたかったので、sin/cosを使っています。ランダムな角度thetaから座標(x,y)を求め、その座標に600~800のランダムな数を掛けることで発声する座標を求めています。また、一定スピードで原点に近づけるためのスピードは以下の手順で求めています。

  1. 正規化して長さ1のベクトルを求める
  2. ベクトルをマイナス1.5倍して向きを変える
    1.5という数字を変えるとスピードが、600~800の値を変えると最初に発生する場所が変わります。

3Dゲームの実装においてはベクトルや三角関数を使えると自由度が広がります。Future Codersではそのような分野もサポート可能です。高校や大学での数学が実際に役に立つ場面もあること、実感してもらえると嬉しいです。

ソースコード

class Model {
  PShape model;
  PVector pos, speed;
  boolean valid = true;

  Model(PVector pos, PVector speed, PShape model) {
    this.pos = pos;
    this.speed = speed;
    this.model = model;
  }

  void tick() {
    pos.add(speed);
  }

  void paint() {
    pushMatrix();
    translate(pos.x, pos.y, pos.z);
    float theta = atan2(pos.x, pos.z);
    rotateY(theta);
    if (model != null) {
      shape(model);
    } else {
      fill(255, 255, 0);
      sphere(10);
    }
    popMatrix();
  }
}

ArrayList<Model> balls = new ArrayList<Model>();
ArrayList<Model> trees = new ArrayList<Model>();
ArrayList<Model> mosquitos = new ArrayList<Model>();
PShape tree, mosquito;
float thetaH, thetaV, sH, sV;
boolean shot = false, gameOver = false;
int score = 0;

void setup() {
  size(512, 512, P3D);
  tree = loadShape("tree.obj");
  mosquito = loadShape("mosquito.obj");
  for (int i = 0; i < 10; i++) {
    PVector z = new PVector(0, 0, 0);
    PVector t = new PVector(random(-500, 500), 0, random(-500, 500));
    trees.add(new Model(t, z, tree));
  }
  noStroke();
  textSize(50);
  textAlign(CENTER);
}

void draw() {
  background(0);
  translate(width/2, height/2);

  float x = 100*cos(radians(thetaH));
  float z = 100*sin(radians(thetaH));
  float y = 100*sin(radians(thetaV));
  camera(0, 0, 0, x, y, z, 0, 1, 0);

  lights();
  fill(0, 0, 255);
  pushMatrix();
  translate(0, 100, 0);
  box(1500, 10, 1500); 
  for (Model m : trees) {
    m.paint();
  }
  popMatrix();

  for (Model m : mosquitos) {
    m.paint();
  }
  for (Model m : balls) {
    m.paint();
  }
  
  camera(width/2, height/2, 400, width/2, height/2, 0, 0, 1, 0);
  fill(255, 255, 0);
  text("SCORE:" + score, width/2, 100);
  if (gameOver) {
    text("GAME OVER", width/2, height/2);
    return;
  }

  if (keyPressed) {
    if (keyCode == DOWN) { 
      sV = 1;
    }
    if (keyCode == UP) {
      sV = -1;
    }
    if (keyCode == LEFT) {
      sH = -3;
    }
    if (keyCode == RIGHT) {
      sH = 3;
    }
    if (key == ' ' && shot == false) {
      shot = true;
      balls.add(new Model(new PVector(0, 0, 0), new PVector(x/10, y/10, z/10), null));
    }
  }

  thetaV = max(-45, min(thetaV + sV, 45));
  thetaH = thetaH + sH;
  sV *= 0.92;
  sH *= 0.92;

  if (frameCount % 50 == 0) {
    float theta = random(360);
    float mx = cos(radians(theta)) * random(600, 800);
    float mz = sin(radians(theta)) * random(600, 800);
    PVector p = new PVector(mx, random(-100, 100), mz);
    PVector d = p.copy().normalize().mult(-1.5);
    mosquitos.add(new Model(p, d, mosquito));
  }

  for (Model m : mosquitos) {
    m.tick();
  }
  for (Model b : balls) {
    b.valid = b.pos.mag() < 2500;
    b.tick();
  }

  for (Model m : mosquitos) {
    if (m.pos.mag() < 100) {
      gameOver = true;
    }
    for (Model b : balls) {
      if (m.pos.dist(b.pos) < 100 && m.valid) {
        b.valid = false;
        m.valid = false;
        score ++;
      }
    }
  }

  removeInvalid(balls);
  removeInvalid(mosquitos);
}

void removeInvalid(ArrayList<Model> list) {
  for (int i = list.size() - 1; i >= 0; i--) {
    Model b = list.get(i);
    if (!b.valid) {
      list.remove(i);
    }
  }
}

void keyReleased() {
  shot = false;
}