ニューラル株式会社|ハイブリッドOS|File System|ARM|Android|Java|制御システム|オープンシステム

 

技術者コラム

 
フォーム
 
第31回目:Processingでシューティング(Part3)
2014-10-12
筆者:村田
 
こんにちは。
 
今回はプレイヤーを表示させるところから始めましょう。プレイヤーも敵と同じようにクラスにまとめます。
 
class Player {
  float x = width / 2;   // 初期位置は画面下中央
  float y = height - 10;
  
  void draw() {
    triangle(x, y-10, x-10, y+10, x+10, y+10);
  }
}
 
Playerのdrawメソッドで三角形を描いています。triangleの引数は3つの頂点のx,y座標です。三角形の中心をx,yとし、幅、高さは20とした場合の3つの頂点を考えます。1つ目の頂点は(x,y-10)となります。Processingの座標系はY軸が画面上がマイナスであることに注意して下さい。残りの2点は(x-10,y+10)、(x+10,y+10)となります。
 
次は上下左右キーでプレイヤーを移動できるようにしたいと思います。マウス位置はグローバル変数mouseX,mouseYに入っていましたが、キーボードの値はグローバル変数keyCodeを参照して取得します。それと、キーを押している間だけプレイヤーが移動し、離したら止まるようにしたいですね。これもイベントハンドラを書く必要はなく、グローバル変数keyPressedを参照しtrue/falseで判断できます。楽ですね〜。ではPlayerクラスのmoveメソッドを実装しましょう。
 
class Player {
  ...
  void move() {
    if (keyPressed) {  // キーが押されているときだけ・・・
      switch (keyCode) {
        case UP:    y -= 2; break; // UP(38) ()内は実際のkeyCode定数値 (参考までに)
        case DOWN:  y += 2; break; // DOWN(40)
        case LEFT:  x -= 2; break; // LEFT(37)
        case RIGHT: x += 2; break; // RIGHT(39)
      }
    }
    // 画面端から三角形がはみ出ないようにチェックする
    if (x-10 < 0)      x = 10;
    if (x+10 > width)  x = width-10;
    if (y-10 < 0)      y = 10;
    if (y+10 > height) y = height-10;
  }
  ...
}
 
ではPlayerオブジェクトを作って画面に表示させましょう。
 
Player player; // グローバル変数
 
void setup() {
  ...
  player = new Player();
}
 
void draw() {
  ...
  fill(0, 255, 0); // プレイヤーは緑色の三角形
  player.move();
  player.draw();
}
 
実行すると参考画像1のようにプレイヤーを上下左右キーで移動できるようになりました。次はプレイヤーもレーザー弾を発射できるようにします。今回は弾は自動発射にしようと思います。プレイヤーの弾も速度を与えて一定方向に移動するので、前回の弾クラス(Bullet)がそのまま流用できそうです。ただし、図形をちょっとレーザーっぽくしましょう。というわけで、Bulletクラスを継承してLaserクラスを作ります。
 
class Laser extends Bullet {
  float w;
  float h;
 
  Laser (float x, float y, float angle, float w, float h) {
    super(x, y, angle, 3, 0); // 弾速:3 角速度:0に固定
    this.w = w; // レーザー弾は幅と高さをコンストラクタでもらう
    this.h = h;
  }
 
  void draw() {
    rect(x-w/2, y-h/2, w, h); // レーザー弾は長方形
  }
}
 
継承はJavaと同じでextendsで書きます。それではLaserコンストラクタを見ていきます。弾速度と角速度を固定値にしてBulletクラスのコンストラクタを呼びます。スーパークラス(Bullet)はsuperで参照できます。Laserは長方形の図形にしますので、幅と高さはコンストラクタのパラメータでもらいましょう。
 
次にレーザー弾もリストで管理しましょう。Bulletと同じように画面外に出たら削除します。以下のコードは前回のBulletリストのコードをコピペして作りました。
 
ArrayList<Laser> laserList; // グローバル変数
 
void setup() {
  ...
  laserList = new ArrayList<Laser> ();
  ...
}
 
void draw() {
  ...
  fill(0, 0, 255); // レーザー弾は青色
  for (int i = laserList.size()-1; i >= 0; i--) {
    Laser laser = laserList.get(i);
    laser.move();
    laser.draw();
    if (laser.needRemove()) laserList.remove(i); // 画面外のレーザーは削除
  }
  ...
}
 
これでレーザー弾の描画は行えるようになりました。あとはPlayerクラスが適当なタイミングでレーザー弾を発射できるようにしましょう。
 
class Player {
  ...
  void draw() {
    triangle(x, y-10, x-10, y+10, x+10, y+10);
    if (frameCount % 30 == 0) laserShot(); // 0.5秒毎に自動発射
  }
 
  void laserShot() {
    // レーザー弾は一度に4方向に発射する
    laserList.add(new Laser(x, y, -90, 2, 20)); // 上(-90°) 幅2/高20
    laserList.add(new Laser(x, y,  90, 2, 20)); // 下(90°)  幅2/高20
    laserList.add(new Laser(x, y, 180, 20, 2)); // 左(180°) 幅20/高2
    laserList.add(new Laser(x, y,   0, 20, 2)); // 右(0°)   幅20/高2
  }
}
 
これで参考画像2のようにレーザー弾が出るようになりました。次は当たり判定を実装しましょう。BulletとPlayer、LaserとEnemyと2つの当たり判定があるので、共通関数として衝突判定関数を作りましょう。
 
// 衝突判定(グローバル関数)
boolean collision(float x1, float y1, float w1, float h1,
                  float x2, float y2, float w2, float h2) {
  if (x1 + w1/2 < x2 - w2/2) return false;
  if (x2 + w2/2 < x1 - w1/2) return false;
  if (y1 + h1/2 < y2 - h2/2) return false;
  if (y2 + h2/2 < y1 - h1/2) return false;
  return true;
}
 
まず引数で2つの長方形(x,y,w,h)をもらいます。この2つの長方形が「重ならない」条件を考えていく方法が簡単です。ゆっくり見ていきましょう。最初のif条件を見ます。x1 + w1/2とは「中心+幅の半分」つまり長方形1の右端ですね。そしてx2 - w2/2は「中心ー幅の半分」つまり長方形2の左端です。「長方形1の右端 < 長方形2の左端」という状態をイメージできますか?このとき2つの長方形は絶対に重ならないのです。ということでfalseを返しましょう。2つ目のif条件は長方形1と長方形2の位置関係を入れ替えたものです。「長方形2の右端 < 長方形1の左端」ならば重なりません(false)。3つ目のif条件を見ます。y1 + h1/2とは「中心+高さの半分」つまり長方形1の下端です。そしてy2 - h2/2とは「中心ー高さの半分」つまり長方形2の上端です。ProcessingのY座標は下側がプラスであるという点に注意して「長方形1の下端 < 長方形2の上端」という状態をイメージして下さい。これも2つの長方形が絶対に重ならない状態です。4つ目のif条件は長方形1と2を入れ替えた条件ですね。最後にこの4つの条件を満たさない場合は「重なっている」のでtrueを返します。
 
そうだ忘れていました。弾が当たったら敵/プレイヤーそれぞれHPを減らすようにしましょう。EnemyとPlayerクラスにそれぞれHP属性を加えておきましょう。なお、いわゆるsetter/getterアクセサは作りません。
 
class Enemy {
  ...
  int hitPoint = 30;
  ...
}
 
class Player {
  ...
  int hitPoint = 10;
  ...
}
 
衝突関数を使って敵の弾が当たったかを判定する箇所ですが、draw関数の中で弾リストを表示しているところで判定しちゃいましょう。
 
void draw() {
  ...
  for (int i = bulletList.size()-1; i >= 0; i--) {
    Bullet bullet = bulletList.get(i);
    bullet.move();
    bullet.draw();
    // ここから追加
    if (collision(player.x, player.y, 3, 3, bullet.x, bullet.y, 5, 5)) {
      bullet.hit = true;
      player.hitPoint--;
    }
    // ここまで
    if (bullet.needRemove()) bulletList.remove(i);
  }
  ...
}
 
ループの中でplayerとbulletの衝突判定を行っています。なお、本記事最初でPlayerの緑三角形の幅と高さを20x20としていましたが、当たり判定に使う矩形を3x3と小さくすることで弾を避け易くしています。弾に当たったらbullet.hitをtrueにします。前回の記事の通り、bullet.needRemove()がtrueを返すようになりますので、playerに当たった弾は消えることになります。
 
同じようにenemyとlaserの衝突判定も書きましょう。
 
void draw() {
  ...
  for (int i = laserList.size()-1; i >= 0; i--) {
    ...
    if (collision(enemy.x, enemy.y, 20, 20, laser.x, laser.y, laser.w, laser.h)) {
      laser.hit = true;
      enemy.hitPoint--;
    }
    ...
  }
  ...
}
 
敵さんの当たり判定サイズは20x20です。次はHitPointを画面に表示させましょう。
 
void draw() {
  ...
  fill(255, 255, 0); // HP表示は黄色
  text("Player:" + nf(player.hitPoint, 3) , 20, 20);
  text("Enemy:" + nf(enemy.hitPoint, 3)  , 20, 40); 
 
  if (player.hitPoint == 0 || enemy.hitPoint == 0)
    noLoop(); // ゲームオーバー
}
 
text関数を使って画面に文字列を表示することができます。第1引数に文字列を渡します。文字列はJavaのStringと同じです。Processingのnf関数で数値を3桁の文字列に変換しています。第2,3引数は表示する座標です。最後にプレイヤーか敵のHPが0になったらnoLoop関数で描画ループを止めてゲームを終了します。
 
しばらく遊んでみると敵の弾に当たらない安全地帯があることが分かりました。そこに入ってしまえば手を止めていてもいずれ勝ってしまうのです。うーん、もう少し頑張りましょうかね。敵がプレイヤーを狙い撃ちしてくるスナイパー弾を追加しましょう。うーんこのネーミングもイマイチ・・。狙い撃ちとは敵とプレイヤーを結ぶ線の角度で撃ってくる弾です。プレイヤーを直接狙った弾ってことです。このようにA地点とB地点を結ぶ線の角度を求める場合、三角関数のatan(アークタンジェント)を使うのが定石です。サイン・コサイン・タンジェントのタンジェントを思い出しましょう。例えばtan30°は覚えていますか?1/(ルート3) = 0.577です。tanは直角三角形の角度からxとyの比率が出せます。atan(アークタンジェント)はtanの逆関数ですので、xとyの比率から角度が出せます。つまりatan(1/(ルート3))は30°になります。えっと実際に使ってみた方が早いです。
 
class Enemy {
  ...
  void draw() {
    ...
    if (frameCount % 120 == 0) snipeShot(); // 2秒に1回スナイプショット
  }
  
  void snipeShot() {
    float dx = player.x - x;
    float dy = player.y - y;
    float degree = degrees(atan2(dy, dx));
    Bullet bullet = new Bullet(x, y, degree, 2, 0);
    bulletList.add(bullet);
  }
}
 
まず敵から見たプレイヤーの相対位置dx,dyを計算します。x - player.xではありませんので注意。そしてatan2(dy,dx)でxとyの比率から角度(ラジアン)を計算します。ラジアンから度数に変換するためdegrees関数を通してからBulletを生成します。atan2関数はxとyを2引数で渡しますが第1引数がy、第2引数がxであることに注意しましょう。なお、アークタンジェントを1引数で計算するatan関数を使う場合はatan(dy/dx)となります。
 
これで安全地帯は無くなり、じっとしていると敵のスナイプ弾を食らってしまうようになりました。今回のシューティングゲームはここまでになります。えーと少し遊んでみて速攻飽きました:P (参考画像3) なお、ソースコード全体は以下のGitHubを参照してください。
 
https://github.com/muratamuu/Processing-ShootingGame/blob/master/ShootingGame.pde
 
 
先程、Dice Newsというサイトの記事「5 Programming Languages Marked for Death」をちらっと見ました。死にゆく言語としてPerl/Ruby/VB.Net/ActionScript/Delphiの5言語が挙げられています。Rubyが入っていることに驚きました。Twitterの開発言語がRuby(RoR)からScalaに置き換えられた理由でもあるパフォーマンスの問題を指摘しています。これだけでRuby終わったと言ってしまうのも・・と思いましたが、まあそれはさておき、いずれもとても人気のあった言語です。新規プロジェクトで使われる機会が減っているのかもしれませんが、既存の資産としては結構な量のコードがあるのではないでしょうか。私自身も昨年VBとDelphiを業務で触ったのですが、まだまだ“Goodbye World!”には早いですよね。
 
以上。