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

 

技術者コラム

 
フォーム
 
第30回目:Processingでシューティング(Part2)
2014-10-05
筆者:村田
 
こんにちは。
 
前回、図形を一定方向に動かしたりくるくる回したりしました。これを応用して敵やプレイヤーの弾にしましょう。画面上の複数の弾を簡単に扱いたいので、配列やリストなどのコレクションを使いたいです。リファレンスを見てみましょう。固定配列のArrayと可変配列のArrayListがありました。今回は画面上の弾数が増減するのでArrayListを使います。弾のXY座標、進行角度などのデータをセットにして扱いたい時は構造体、クラス、ハッシュ、タプルといったユーザー定義のデータ型を作れる仕組みが必要です。Processingではクラスでデータ型を作ります。それでは弾(Bullet)クラスを作りましょう。
 
class Bullet {
}
 
XY座標と進行角度をBulletクラスの属性として持たせます。他にも進行角度の変化量である角速度や弾の移動速度も属性に追加しました。
 
class Bullet {
  float x;          // X座標
  float y;          // Y座標
  float angle;      // 進行角度
  float speed;      // 移動速度
  float angleSpeed; // 角速度
}
 
次に初期値を渡せるコンストラクタを作ります。Javaと同じく自インスタンスの参照にはthisを使います。なお所々...で記述を省略します。
 
class Bullet {
  ...
  Bullet (float x, float y, float angle, float speed, float angleSpeed) {
    this.x = x; 
    this.y = y;
    this.angle = angle;
    this.speed = speed;
    this.angleSpeed = angleSpeed;
  }
}
 
前回draw関数の中に書いた図形の移動処理をBulletクラスのメソッドにします。前回の処理とちょっと違うのは三角関数で得られたxとyの移動量をspeedで定数倍してスピードアップしている点です。
 
class Bullet {
  ...
  void move() {
    angle = (angle + angleSpeed) % 360; // 進行角度が角速度で変化する
    x += cos(radians(angle)) * speed;   // 進行角度のx成分のスピード倍
    y += sin(radians(angle)) * speed;   // 進行角度のy成分のスピード倍
  }
}
 
moveは移動だけで描画されません。描画するメソッドを別に作りましょう。
 
class Bullet {
  ...
  void draw() {
    ellipse(x, y, 10, 10); // XY座標を中心に幅と高さ10の円を描く
  }
}
 
これで弾が扱い易くなりました。試しに複数の弾をsetup関数で作成しdraw関数で描画してみましょう。
 
ArrayList<Bullet> bulletList; // 弾リスト(グローバル変数)
 
void setup() {
  size(500, 500);
  noStroke();
  
  bulletList = new ArrayList<Bullet>(); // 弾リスト生成
 
  // 画面の中央(cx,cy)
  float cx = width / 2;
  float cy = height / 2;
  
  // 0〜350°まで10°ずつずらして弾を生成
  for (float angle = 0; angle < 360; angle += 10) {
    Bullet bullet = new Bullet(cx, cy, angle, 2, 0);
    bulletList.add(bullet);
  }
}
 
void draw() {
  fill(0, 0, 0, 20);
  rect(0, 0, width, height);
 
  fill(255, 0, 0); // 弾の色(赤)
  for (Bullet bullet : bulletList) {
    bullet.move(); // 弾を移動
    bullet.draw(); // 弾を描画
  }
}
 
どうですか?参考画像1のように画面中央から放射状に弾が動けば成功です。次はこの放射弾を発射する敵を作りましょう。これもEnemyクラスとしてまとめます。なお今回は敵1体だけのゲームにします。Enemy vs Playerのゲーム。
 
class Enemy {
  float x = width / 2;  // 敵の初期位置(xは真ん中)
  float y = height / 3; // 敵の初期位置(yはちょっと上)
  int angle = 0;
  
  void move() {
    angle = (angle + 1) % 360;           // 角速度1
    x += cos(radians(angle)) * 2;        // 移動速度2
    y += sin(radians(angle*2 + 90)) * 3; // 移動速度3 縦は横の2倍の周期で動く
  }
  
  void draw() {
    rect(x-10, y-10, 20, 20); // 敵は四角図形。幅と高さは20
  }
}
 
Bulletクラスに似ています。どうせ敵1体だけなのでパラメータ付きのコンストラクタは用意していません。初期位置は画面中央のちょっと上に設定しました。まずdrawメソッドを見ていきます。敵は四角で描画することにしました。rect関数の第1,2引数は四角形の左上隅の座標を指定します。したがって(x,y)を四角形の中心とすると、幅と高さの半分を引いた位置が左上隅となります。
 
さてmoveの中身を順番に見ていきましょう。まずangleは0°から始まります。1°ずつ増えて360°で1周します。次はx方向(横)の移動です。コサインカーブを思い出しましょう。1(cos0°)から始まって0(cos90°)、-1(cos180°)、0(cos270°)、1(cos360°)と動きます。まず右に動き始めangleが90°になったところで右から左向きに動くように変わり、angleが270°になったところでまた右に動き始めます。左右の往復運動がイメージできますでしょうか。次はy方向(縦)の移動です。angleを2倍にしているので、横の2倍の周期で縦が移動します。左右に1往復する間に上下に2往復するというわけです。なお位相を90°ずらしています。サインカーブを思い出しましょう。通常は0(sin0°)から始まって、0 -> 1 -> 0 -> -1 -> 0と変化しますが、位相を90°ずらしているので、1 -> 0 -> -1 -> 0 -> 1というカーブになります。スタート時(angle=0)、下方向(1)にすぐに移動を始めて欲しかったのでこうしました。ではEnemyを実際に描画してみましょう。
 
Enemy enemy; // グローバル変数
 
void setup() {
  ...
  enemy = new Enemy(); // 敵1体を生成
}
 
void draw() {
  ...
  fill(167, 87, 168); // 敵は紫っぽい色
  enemy.move();
  enemy.draw();
}
 
参考画像2のように無限マークに似た感じで動きます。ここまで書いておいてなんですが、こんなことは頭で考えるより実際に手でパラメータをいじくりまわした方が楽しいですし、面白い動きを発見できると思います。ちょっといじってはRUNして、またいじくって・・・と、どんどん面白いビジュアルを追求する楽しさがProcessingの醍醐味かもしれません。
 
今度は敵の動きと先程の放射弾を組み合わせていきましょう。その前にやることがあります。弾をどんどん発射するので、画面から外れた弾はArrayListから削除しましょう。それとまだ先の話ですが、プレイヤーに当たった弾も画面から消してArrayListからも削除したいです。これらを判定するメソッドをBulletクラスに追加しましょう。
 
class Bullet {
  ...
  boolean hit = false; // プレイヤーに当たったかを示すフラグ (まだ使わないけど)
 
  boolean needRemove() {
    // 画面からはみ出るか、プレイヤーに当たったら削除してよい。
    return x < 0 || x > width || y < 0 || y > height || hit;
  }
}
 
draw関数内の弾リスト描画ループを修正しましょう。
 
void draw() {
  ...
  for (Bullet bullet : bulletList) {
    bullet.move();
    bullet.draw();
    if (bullet.needRemove()) bulletList.remove(bullet); // 画面外の弾を削除(?)
  }
  ...
}
 
上記は実行時エラーとなってしまいます。Java同様、拡張for文内のイテレータは途中で要素が削除されることを想定していません。普通のfor文に書き換えましょう。お尻から回します。
 
void draw() {
  ...
  for (int i = bulletList.size()-1; i >= 0; i--) {
    Bullet bullet = bulletList.get(i);
    bullet.move();
    bullet.draw();
    if (bullet.needRemove()) bulletList.remove(i); // 画面外の弾を削除
  }
  ...
}
 
では敵に放射弾を発射させましょう。先程setup関数内で試したロジックをEnemyクラスに持ってくるだけです。
 
class Enemy {
  ...
  void circleShot() {
    // 放射弾。敵の現在位置(x,y)を中心に360°発射してやんよ
    for (float angle = 0; angle < 360; angle += 10) {
      Bullet bullet = new Bullet(x, y, angle, 2, 0); // 弾速2
      bulletList.add(bullet);
    }
  }
}
 
というわけでsetup関数からお試しコードは削除します。残ったコードは以下の通り。
 
void setup() {
  size(500, 500);
  noStroke();
  
  bulletList = new ArrayList<Bullet>();
  enemy = new Enemy();
}
 
次はcircleShotを定期的に呼ぶようにします。どうやってタイミングを計るか。こんなときはProcessingのframeCountグローバル変数を利用します。描画フレーム数がカウントアップされています。frameCountが60になると大体1秒です。Enemyのdraw関数内でframeCountを参照し、適当なタイミングでcircleShotを呼びます。
 
class Enemy {
  ...
  void draw() {
    rect(x-10, y-10, 20, 20);     
    if (frameCount % 90 == 0) circleShot(); // ほぼ1.5秒間隔で放射弾発射
  }
  ...
}
 
さて動かしてみましょう。参考画像3ではちょっと分かりづらいですが、敵が無限の軌跡で動きながら放射弾を撃ってきます。別の種類の弾も作って発射させてみましょう。スロー弾です(テキトー)。
 
class Enemy {
  ...
  void draw() {
    rect(x-10, y-10, 20, 20);     
    if (frameCount % 90 == 0) circleShot();
    if (frameCount % 10 == 0) slowCurveShot(); // 1秒間に6発
  }
  ...
  void slowCurveShot() {
    Bullet bullet = new Bullet(x, y, angle, 1, 0.2); // スピードが遅く(1)ちょっとだけ曲がる(0.2)
    bulletList.add(bullet);
  }
}
 
動かすと参考画像4のようになります。やはり分かりづらい・・・遅い弾を発射しビミョ〜に曲がります。避けにくいかもしれません:P 今回はここまで。
 
p.s. 最近Breaking Badという海外ドラマを見始めまして。久々に海外ドラマシリーズを見ているのですが、人気シリーズのようで面白いです。
 
以上。