レポート6

インベータゲームの考察

今回はインベータゲームをメイン、プレイヤー、弾、敵、敵の弾、に分けて考察していきます。

1、プレイヤークラス

(ソース1) import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import javax.swing.ImageIcon; /** * プレイヤークラス */ public class Player { // 方向定数 private static final int LEFT = 0; private static final int RIGHT = 1; // 移動スピード private static final int SPEED = 5; // プレイヤーの位置(x座標) private int x; // プレイヤーの位置(y座標) private int y; // プレイヤーの幅 private int width; // プレイヤーの高さ private int height; // プレイヤーの画像 private Image image; // メインパネルへの参照 private MainPanel panel; public Player(int x, int y, MainPanel panel) { this.x = x; this.y = y; this.panel = panel; // イメージをロード loadImage(); } /** * プレイヤーを移動する * * @param dir 移動方向 */ public void move(int dir) { if (dir == LEFT) { x -= SPEED; } else if (dir == RIGHT) { x += SPEED; } // 画面の外に出ていたら中に戻す if (x < 0) { x = 0; } if (x > MainPanel.WIDTH - width) { x = MainPanel.WIDTH - width; } } /** * プレイヤーとビームの衝突を判定する * * @param beam 衝突しているか調べるビームオブジェクト * @return 衝突していたらtrueを返す */ public boolean collideWith(Beam beam) { // プレイヤーの矩形を求める Rectangle rectPlayer = new Rectangle(x, y, width, height); // ビームの矩形を求める Point pos = beam.getPos(); Rectangle rectBeam = new Rectangle(pos.x, pos.y, beam.getWidth(), beam.getHeight()); // 矩形同士が重なっているか調べる // 重なっていたら衝突している return rectPlayer.intersects(rectBeam); } /** * プレイヤーを描画する * * @param g 描画オブジェクト */ public void draw(Graphics g) { g.drawImage(image, x, y, null); } /** * プレイヤーの位置を返す * * @return プレイヤーの位置座標 */ public Point getPos() { return new Point(x, y); } /** * プレイヤーの幅を返す * * @param width プレイヤーの幅 */ public int getWidth() { return width; } /** * プレイヤーの高さを返す * * @return height プレイヤーの高さ */ public int getHeight() { return height; } /** * イメージをロードする * */ private void loadImage() { // プレイヤーのイメージを読み込む // ImageIconを使うとMediaTrackerを使わなくてすむ ImageIcon icon = new ImageIcon(getClass().getResource( "image/player.gif")); image = icon.getImage(); // 幅と高さをセット width = image.getWidth(panel); height = image.getHeight(panel); } }

(プレイヤークラスについて)

・まずはプレイヤーの属性と機能をまとめたplayerクラスを作ります。プレイヤーの属性としては private static final int SPEED = 5; // 移動スピード private int x; // プレイヤーの位置(x座標) private int y; // プレイヤーの位置(y座標) private int width; // プレイヤーの幅 private int height; // プレイヤーの高さ private Image image; // プレイヤーの画像 などがあります。スピードとは1回のmoveメソッドで移動する距離の事です。この値が大きいほど早く 移動する事になります。  ・プレイヤーを移動させるのでmove()メソッドを実装します。 /** * プレイヤーを移動する * * @param dir 移動方向 */ public void move(int dir) { if (dir == LEFT) { x -= SPEED; } else if (dir == RIGHT) { x += SPEED; } // 画面の外に出ていたら中に戻す if (x < 0) { x = 0; } if (x > MainPanel.WIDTH - width) { x = MainPanel.WIDTH - width; } } 引数のdirの方向によってプレイヤーのx座標を変えます。ここでプレイヤーが画面の外に出ない様にも しています。  ・次にイメージをロードしています。 /** * イメージをロードする * */ private void loadImage() { // プレイヤーのイメージを読み込む // ImageIconを使うとMediaTrackerを使わなくてすむ ImageIcon icon = new ImageIcon(getClass().getResource( "image/player.gif")); image = icon.getImage(); // 幅と高さをセット width = image.getWidth(panel); height = image.getHeight(panel); } ImageIconクラスを使ってイメージをロードし、 ImageIconのgetImage()を使って Imageオブジェクトを取り出しています。このように書くとMediaTrackerを使う必要がなくなります。 ImageIconは自動的にMediaTrackerを使ってくれるからです。こっちの方が簡潔な書き方となっています。  ・ゲームループはスレッドとして実装しています。 /** * ゲームループ */ public void run() { while (true) { // 押されているキーに応じてプレイヤーを移動する // 何も押されていないときは移動しない if (leftPressed) { player.move(LEFT); } else if (rightPressed) { player.move(RIGHT); } // 再描画 repaint(); // 休止 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } } おされたキーに応じてplayerを移動させています。leftPressedとrightPressedはboolean型の変数です。 キーが押されていたらtrue、押されていなかったらfalseになります。 sleep()を使って20ミリ秒間休止しています。つまり、1000÷20で50FPSとなります。

2、弾クラス

(ソース2) import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import javax.swing.ImageIcon; /** * 弾クラス */ public class Shot { // 弾のスピード private static final int SPEED = 10; // 弾の保管座標(画面に表示されない場所) private static final Point STORAGE = new Point(-20, -20); // 弾の位置(x座標) private int x; // 弾の位置(y座標) private int y; // 弾の幅 private int width; // 弾の高さ private int height; // 弾の画像 private Image image; // メインパネルへの参照 private MainPanel panel; public Shot(MainPanel panel) { x = STORAGE.x; y = STORAGE.y; this.panel = panel; // イメージをロード loadImage(); } /** * 弾を移動する * */ public void move() { // 保管庫に入っているなら何もしない if (isInStorage()) return; // 弾はy方向にしか移動しない y -= SPEED; // 画面外の弾は保管庫行き if (y < 0) { store(); } } /** * 弾の位置を返す * * @return 弾の位置座標 */ public Point getPos() { return new Point(x, y); } /** * 弾の位置をセットする * * @param x 弾のx座標 * @param y 弾のy座標 * */ public void setPos(int x, int y) { this.x = x; this.y = y; } /** * 弾の幅を返す。 * * @param width 弾の幅。 */ public int getWidth() { return width; } /** * 弾の高さを返す。 * * @return height 弾の高さ。 */ public int getHeight() { return height; } /** * 弾を保管庫に入れる * */ public void store() { x = STORAGE.x; y = STORAGE.y; } /** * 弾が保管庫に入っているか * * @return 入っているならtrueを返す */ public boolean isInStorage() { if (x == STORAGE.x && y == STORAGE.x) return true; return false; } /** * 弾を描画する * * @param g 描画オブジェクト */ public void draw(Graphics g) { // 弾を描画する g.drawImage(image, x, y, null); } /** * イメージをロードする * */ private void loadImage() { ImageIcon icon = new ImageIcon(getClass().getResource("image/shot.gif")); image = icon.getImage(); // 幅と高さをセット width = image.getWidth(panel); height = image.getHeight(panel); } }

(弾クラスについて)

・弾1つ1つをオブジェクトとして扱うためにShotクラスを用意します。弾もプレイヤーと同じく下のような属性が 必要になります。 // 弾のスピード private static final int SPEED = 10; // 弾の保管座標(画面に表示されない場所) private static final Point STORAGE = new Point(-20, -20); // 弾の位置(x座標) private int x; // 弾の位置(y座標) private int y; // 弾の幅 private int width; // 弾の高さ private int height; // 弾の画像 private Image image; 弾の保管座標というのは、プレイヤーが撃っていない弾を置いておく場所です。(−20、20)など画面の外 の座標を設定しておくのがよいかもです。 Shotのコンストラクタでは↓ public Shot(MainPanel panel) { x = STORAGE.x; y = STORAGE.y; ・・・ } 弾オブジェクト野市は保管庫の座標(画面外)で初期かしている。つまり、つくられた弾オブジェクトは 保管庫に入るので画面に表示されないようになっている。 ・次にmove()です。 /** * 弾を移動する * */ public void move() { // 保管庫に入っているなら何もしない if (isInStorage()) return; // 弾はy方向にしか移動しない y -= SPEED; // 画面外の弾は保管庫行き if (y < 0) { store(); } } これはプレイヤーの移動とほとんど同じになっていますspeedの分だけ弾のy座標を更新している。こうすると 弾は上に飛んでいく様に見えますもし画面外(つまりy=0)に出たらstore()を呼びだしてこの弾を保管庫 に移動している。つまり、環境を考慮して一度使った弾をもう一度使用しています。 /** * 弾を保管庫に入れる * */ public void store() { x = STORAGE.x; y = STORAGE.y; } 保管庫に入っている弾は移動させないでそのままreturnしています。 ・次に弾の発射の処理ですMainPanelクラス(メインのソース自体は後にのせています。ここではその一部のみ 切り取りしてます)をみると、弾の発射ボタンはスペースキーです。 フラグfirePressedでスペースキーが押されたらtrue,離している間はfalseになる様になっている。 while (true) { // 発射ボタンが押されたら弾を発射 if (firePressed) { if (shot.isInStorage()) { // 弾が保管庫にあれば発射できる // 弾の座標をプレイヤーの座標にすれば発射される Point pos = player.getPos(); shot.setPos(pos.x + player.getWidth() / 2, pos.y); } } // 弾を移動する shot.move(); ・・・ } もしスペースキーが押されていてfirePressedがtrueになっていたら弾を発射します。ただし 弾を発射するのは保管庫に弾があるときだけです。 発射する弾はプレイヤーの位置にセットして、ゲームループ内でmove()を呼ぶとまっすぐ上に飛ぶ様に見えます。 move()内で保管庫にある弾は移動しないという条件にしているのでここでは必要ありません。 ・弾オブジェクトはコンストラクタで最初に作ってしまっています。 public MainPanel() { ・・・ // プレイヤーを作成 player = new Player(0, HEIGHT - 20, this); // 弾を作成 shot = new Shot(this); ・・・ } ここでなぜスペースキーをおして弾が発射される時にShotオブジェクトを作らないのかというと、発射する度に いちいちオブジェクトを作っていると処理が遅くなってしまうからです。1発づつならまだいいですが連続で発射 し続け場合は生成コストは大きくなっていきます。そのため、あらかじめ弾を全部作って用意しておくという方法を とっています。そのため発射されていない弾が画面に表示されない様に画面外に保管庫を用意しています。 ただし、この方法はメモリを消費するという欠点があります。 ・Shotを下のように配列で管理すれば複数の弾を作れます。この場合は5発の弾を連続で撃てるようにしている ので弾の数(NUM_SHOT)が5になっています。コンストラクタでは5発の弾を作り、配列に入れています。 // 連続発射できる弾の数 private static final int NUM_SHOT = 5; // 弾 private Shot[] shots; public MainPanel() { ・・・ // 弾を作成 shots = new Shot[NUM_SHOT]; for (int i = 0; i < NUM_SHOT; i++) { shots[i] = new Shot(this); } ・・・ } ・次にforループで配列をまわして全ての弾を移動したり、描画している。 // 弾を移動する for (int i = 0; i <NUM_SHOT; i++) { shots[i].move(); } // 弾を描画 for (int i = 0; i < NUM_SHOT; i++) { shots[i].draw(g); } ・次は弾を連発する処理です。tryFire()というメドッドにまとめてあります。tryになっているのは弾が 撃てない場合もあるからです。撃てない場合というのは5発全部撃っている時と前の弾を撃ってからFIRE_INTERVAL しかたっていない時です。一つ目の条件は5発しか弾を用意していないから当然5発以上同時に撃てないのは当然 です。2つ目はまあゲームの都合上ある程度の間隔を入れるためです。 間隔は // 発射できる間隔(弾の充填時間) private static final int FIRE_INTERVAL = 300; のように定義しています。 ・tryToFire()が発射処理です。 /** * 弾を発射する */ private void tryToFire() { // 前との発射間隔がFIRE_INTERVAL以下だったら発射できない if (System.currentTimeMillis() - lastFire < FIRE_INTERVAL) { return; } lastFire = System.currentTimeMillis(); // 発射されていない弾を見つける for (int i = 0; i < NUM_SHOT; i++) { if (shots[i].isInStorage()) { // 弾が保管庫にあれば発射できる // 弾の座標をプレイヤーの座標にすれば発射される Point pos = player.getPos(); shots[i].setPos(pos.x + player.getWidth() / 2, pos.y); // 1つ見つけたら発射してbreakでループをぬける break; } } } 1つ前に弾を発射した時間をlastFireに保存してcurrentTimeMillis()で取得した現在時間と1つ前に発射した lastFireの差がFIRE_INTERVALより小さい場合は発射できない様にしています。 それ以外の場合は保管庫に入っている弾を探して発射します。tryToFire()1回 の呼び出しで1発だけ発射されればいいので、撃っていない弾を1発見つけて発射した場合はbreakでぬけています。 ここでFIRE_INTERVALを0にしてしまうと一回スペースを押しただけで5発全部発射されます。 これはゲームループのrun()が高速でループが何回も回ってしまいtryToFire()が5回以上呼び出されて しまうためにおこってしまう現象です。

(3、敵(エイリアン)クラス)

(ソース3) import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import javax.swing.ImageIcon; /** * エイリアンクラス */ public class Alien { // エイリアンの移動範囲 private static final int MOVE_WIDTH = 210; // エイリアンの墓(画面に表示されない場所) private static final Point TOMB = new Point(-50, -50); // 移動スピード private int speed; // エイリアンの位置(x座標) private int x; // エイリアンの位置(y座標) private int y; // エイリアンの幅 private int width; // エイリアンの高さ private int height; // エイリアンの画像 private Image image; // エイリアンの移動範囲 private int left; private int right; // エイリアンが生きてるかどうか private boolean isAlive; // メインパネルへの参照 private MainPanel panel; public Alien(int x, int y, int speed, MainPanel panel) { this.x = x; this.y = y; this.speed = speed; this.panel = panel; // エイリアンの初期位置から移動範囲を求める left = x; right = x + MOVE_WIDTH; isAlive = true; // イメージをロード loadImage(); } /** * エイリアンを移動する * */ public void move() { x += speed; // 移動範囲を超えていたら反転移動 if (x < left) { speed = -speed; } if (x > right) { speed = -speed; } } /** * エイリアンと弾の衝突を判定する * * @param shot 衝突しているか調べる弾オブジェクト * @return 衝突していたらtrueを返す */ public boolean collideWith(Shot shot) { // エイリアンの矩形を求める Rectangle rectAlien = new Rectangle(x, y, width, height); // 弾の矩形を求める Point pos = shot.getPos(); Rectangle rectShot = new Rectangle(pos.x, pos.y, shot.getWidth(), shot.getHeight()); // 矩形同士が重なっているか調べる // 重なっていたら衝突している return rectAlien.intersects(rectShot); } /** * エイリアンが死ぬ、墓へ移動 * */ public void die() { setPos(TOMB.x, TOMB.y); isAlive = false; } /** * エイリアンの幅を返す * * @param width エイリアンの幅 */ public int getWidth() { return width; } /** * エイリアンの高さを返す * * @return height エイリアンの高さ */ public int getHeight() { return height; } /** * エイリアンの位置を返す * * @return エイリアンの位置座標 */ public Point getPos() { return new Point(x, y); } /** * エイリアンの位置を(x,y)にセットする * * @param x 移動先のx座標 * @param y 移動先のy座標 */ public void setPos(int x, int y) { this.x = x; this.y = y; } /** * エイリアンが生きているか * * @return 生きていたらtrueを返す */ public boolean isAlive() { return isAlive; } /** * エイリアンを描画する * * @param g 描画オブジェクト */ public void draw(Graphics g) { g.drawImage(image, x, y, null); } /** * イメージをロードする * */ private void loadImage() { // エイリアンのイメージを読み込む // ImageIconを使うとMediaTrackerを使わなくてすむ ImageIcon icon = new ImageIcon(getClass() .getResource("image/alien.gif")); image = icon.getImage(); // 幅と高さをセット width = image.getWidth(panel); height = image.getHeight(panel); } }

(エイリアンソース)

まずはエイリアンの属性と機能をまとめたAlienクラスを作ります。エイリアンの属性として private int speed; // 移動スピード private int x; // プレイヤーの位置(x座標) private int y; // エイリアンの位置(y座標) private int width; // エイリアンの幅 private int height; // エイリアンの高さ private Image image; // エイリアンの画像 がありますPlayerクラスやShotクラスとほぼ同じです。プレイヤーもエイリアンも基本はおなじという事です。 プレイヤー、エイリアン、弾などはスプライトと呼ばれ、共通した属性を持ちます。このように共通した属性を 持つクラスは継承を使って書くほうがすっきりします。たとえば、上に挙げた属性を持つSpriteクラスを作り、 Player、Shot、Alienは Spriteを継承するように書くとよい。 JavaでGIFアニメーションを描画すると自動的にアニメーション表示してくれて便利です。 次にエイリアンの移動処理です。プログラムを実行してもらうとわかりますが、エイリアンは左右に移動してい ます。ある区間を往復運動するわけです。この処理を実現するためにエイリアンの移動範囲を指定する必要があ ります。 // エイリアンの移動範囲 private int left; private int right; public Alien(int x, int y, int speed, MainPanel panel) { ・・・ // エイリアンの初期位置から移動範囲を求める left = x; right = x + MOVE_WIDTH; ・・・ } 左端(left)はコンストラクタの引数xで右端(right)はxにMOVE_WIDTH足した値になっています(下図)各 エイリアンはコンストラクタで指定されたxの値が違うので移動範囲も当然変わります。エイリアンは自分の 移動範囲内を往復運動します。 /** * エイリアンを移動する * */ public void move() { x += speed; // 移動範囲を超えていたら反転移動 if (x < left) { speed = -speed; } if (x > right) { speed = -speed; } } 移動範囲の左端leftを超えたらspeedの符号を反対にしています。こうすると逆方向に移動するわけです(跳ね返 り処理)右端rightを超えた場合も同様です。

(4、当たり判定(衝突判定)処理)

ここでソースmainpanelものせておきます (ソース) import java.applet.Applet; import java.applet.AudioClip; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.Random; import javax.swing.JPanel; /** * メインパネル * */ public class MainPanel extends JPanel implements Runnable, KeyListener { // パネルサイズ public static final int WIDTH = 640; public static final int HEIGHT = 480; // 方向定数 private static final int LEFT = 0; private static final int RIGHT = 1; // 連続発射できる弾の数 private static final int NUM_SHOT = 5; // 発射できる間隔(弾の充填時間) private static final int FIRE_INTERVAL = 300; // エイリアンの数 private static final int NUM_ALIEN = 50; // ビームの数 private static final int NUM_BEAM = 20; // プレイヤー private Player player; // 弾 private Shot[] shots; // 最後に発射した時間 private long lastFire = 0; // エイリアン private Alien[] aliens; // ビーム private Beam[] beams; // キーの状態(このキー状態を使ってプレイヤーを移動する) private boolean leftPressed = false; private boolean rightPressed = false; private boolean firePressed = false; // ゲームループ用スレッド private Thread gameLoop; // 乱数発生器 private Random rand; // サウンド private AudioClip fireSound; private AudioClip crySound; private AudioClip bombSound; public MainPanel() { // パネルの推奨サイズを設定、pack()するときに必要 setPreferredSize(new Dimension(WIDTH, HEIGHT)); // パネルがキー入力を受け付けるようにする setFocusable(true); // ゲームの初期化 initGame(); rand = new Random(); // サウンドをロード fireSound = Applet.newAudioClip(getClass().getResource("se/pi02.wav")); crySound = Applet.newAudioClip(getClass().getResource("se/pi00.wav")); bombSound = Applet.newAudioClip(getClass().getResource("se/bom28_a.wav")); // キーイベントリスナーを登録 addKeyListener(this); // ゲームループ開始 gameLoop = new Thread(this); gameLoop.start(); } /** * ゲームループ */ public void run() { while (true) { move(); // 発射ボタンが押されたら弾を発射 if (firePressed) { tryToFire(); } // エイリアンの攻撃 alienAttack(); // 衝突判定 collisionDetection(); // 再描画 repaint(); // 休止 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * ゲームの初期化 */ private void initGame() { // プレイヤーを作成 player = new Player(0, HEIGHT - 20, this); // 弾を作成 shots = new Shot[NUM_SHOT]; for (int i = 0; i < NUM_SHOT; i++) { shots[i] = new Shot(this); } // エイリアンを作成 aliens = new Alien[NUM_ALIEN]; for (int i = 0; i < NUM_ALIEN; i++) { aliens[i] = new Alien(20 + (i % 10) * 40, 20 + (i / 10) * 40, 3, this); } // ビームを作成 beams = new Beam[NUM_BEAM]; for (int i = 0; i < NUM_BEAM; i++) { beams[i] = new Beam(this); } } /** * 移動処理 */ private void move() { // プレイヤーを移動する // 何も押されていないときは移動しない if (leftPressed) { player.move(LEFT); } else if (rightPressed) { player.move(RIGHT); } // エイリアンを移動する for (int i = 0; i < NUM_ALIEN; i++) { aliens[i].move(); } // 弾を移動する for (int i = 0; i < NUM_SHOT; i++) { shots[i].move(); } // ビームを移動する for (int i = 0; i < NUM_BEAM; i++) { beams[i].move(); } } /** * 弾を発射する */ private void tryToFire() { // 前との発射間隔がFIRE_INTERVAL以下だったら発射できない if (System.currentTimeMillis() - lastFire < FIRE_INTERVAL) { return; } lastFire = System.currentTimeMillis(); // 発射されていない弾を見つける for (int i = 0; i < NUM_SHOT; i++) { if (shots[i].isInStorage()) { // 弾が保管庫にあれば発射できる // 弾の座標をプレイヤーの座標にすれば発射される Point pos = player.getPos(); shots[i].setPos(pos.x + player.getWidth() / 2, pos.y); // 発射音 fireSound.play(); // 1つ見つけたら発射してbreakでループをぬける break; } } } /** * エイリアンの攻撃 */ private void alienAttack() { // 1ターンでNUM_BEAMだけ発射する // つまりエイリアン1人になってもそいつがNUM_BEAM発射する for (int i = 0; i < NUM_BEAM; i++) { // エイリアンの攻撃 // ランダムにエイリアンを選ぶ int n = rand.nextInt(NUM_ALIEN); // そのエイリアンが生きていればビーム発射 if (aliens[n].isAlive()) { // 発射されていないビームを見つける // 1つ見つけたら発射してbreakでループをぬける for (int j = 0; j < NUM_BEAM; j++) { if (beams[j].isInStorage()) { // ビームが保管庫にあれば発射できる // ビームの座標をエイリアンの座標にセットすれば発射される Point pos = aliens[n].getPos(); beams[j].setPos(pos.x + aliens[n].getWidth() / 2, pos.y); break; } } } } } /** * 衝突検出 * */ private void collisionDetection() { // エイリアンと弾の衝突検出 for (int i = 0; i < NUM_ALIEN; i++) { for (int j = 0; j < NUM_SHOT; j++) { if (aliens[i].collideWith(shots[j])) { // i番目のエイリアンとj番目の弾が衝突 // エイリアンは死ぬ aliens[i].die(); // 断末魔 crySound.play(); // 弾は保管庫へ(保管庫へ送らなければ貫通弾になる) shots[j].store(); // エイリアンが死んだらもうループまわす必要なし break; } } } // プレーヤーとビームの衝突検出 for (int i = 0; i < NUM_BEAM; i++) { if (player.collideWith(beams[i])) { // 爆発音 bombSound.play(); // プレーヤーとi番目のビームが衝突 // ビームは保管庫へ beams[i].store(); // ゲームオーバー initGame(); } } } /** * 描画処理 * * @param 描画オブジェクト */ public void paintComponent(Graphics g) { super.paintComponent(g); // 背景を黒で塗りつぶす g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); // プレイヤーを描画 player.draw(g); // エイリアンを描画 for (int i = 0; i < NUM_ALIEN; i++) { aliens[i].draw(g); } // 弾を描画 for (int i = 0; i < NUM_SHOT; i++) { shots[i].draw(g); } // ビームを描画 for (int i = 0; i < NUM_BEAM; i++) { beams[i].draw(g); } } public void keyTyped(KeyEvent e) { } /** * キーが押されたらキーの状態を「押された」に変える * * @param e キーイベント */ public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { leftPressed = true; } if (key == KeyEvent.VK_RIGHT) { rightPressed = true; } if (key == KeyEvent.VK_SPACE) { firePressed = true; } } /** * キーが離されたらキーの状態を「離された」に変える * * @param e キーイベント */ public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { leftPressed = false; } if (key == KeyEvent.VK_RIGHT) { rightPressed = false; } if (key == KeyEvent.VK_SPACE) { firePressed = false; } } }

(衝突判定について)

・intersects()を用いた衝突判定 まだこの部分は3エイリアンのソースの中にある部分です。 エイリアン(敵)と弾との衝突判定を実装する場合エイリアンを囲む様に矩形で囲い矩形と矩形の衝突を 判定します。 矩形同士の衝突を判定しているのがAlienクラスのcollideWith()です。弾と衝突判定するメソッドはエイリアン 側に持たせています。各エイリアンが引数で指定された弾(Shot)と衝突しているか判定させているからです。これを 弾に持たせるという方法もあります。 /** * エイリアンと弾の衝突を判定する * * @param shot 衝突しているか調べる弾オブジェクト * @return 衝突していたらtrueを返す */ public boolean collideWith(Shot shot) { // エイリアンの矩形を求める Rectangle rectAlien = new Rectangle(x, y, width, height); // 弾の矩形を求める Point pos = shot.getPos(); Rectangle rectShot = new Rectangle(pos.x, pos.y, shot.getWidth(), shot.getHeight()); // 矩形同士が重なっているか調べる // 重なっていたら衝突している return rectAlien.intersects(rectShot); } まずエイリアンと弾の短形を求めます。短形はRectangleクラスを使います。Rectangleクラスは短形の左上の 座標と幅、高さを指定して作ります。 ・次に短形同士の衝突処理です。短形同士だ衝突するという事は短形同士の座標が重なっているという事です Rectangleクラスのintersects()メソッドで調べる事ができます。 矩形1.intersects(矩形2) という形式です。2つが重なっていたらtrueを返します。ここではエイリアン(rectAlien)の短形と 弾の短形(rectShot)が重なっているか調べます. ・次にMainPanelクラスにこの機能を組み込みます。MainPanelクラスのcollisionDetection()です。 /** * エイリアンと弾の衝突検出 * */ private void collisionDetection() { // エイリアンと弾の衝突検出 for (int i=0; i<NUM_ALIEN; i++) { for (int j=0; j<NUM_SHOT; j++) { if (aliens[i].collideWith(shots[j])) { // i番目のエイリアンとj番目の弾が衝突 // エイリアンは死ぬ aliens[i].die(); // 弾は保管庫へ(保管庫へ送らなければ貫通弾になる) shots[j].store(); // エイリアンが死んだらもうループまわす必要なし break; } } } } このゲームではエイリアンが50体、弾が5つありますこの全部の衝突判定を上 のように2重のforループをまわして確認しています。 ・エイリアンと弾が衝突した場合は、エイリアンも弾も消滅するようにします。弾は保管庫に移動させればよい。 store()で保管庫に入ります。エイリアンの場合も弾の保管庫のように画面外に移動させることで消滅させ ます。それがdie()です。 // エイリアンの墓(画面に表示されない場所) private static final Point TOMB = new Point(-50, -50); /** * エイリアンが死ぬ、墓へ移動 * */ public void die() { setPos(TOMB.x, TOMB.y); } 画面外に移動させています。こうすると消滅したように見える。 エイリアンと弾が消滅したらforループをぬけるためbreakを呼び出しています。弾があたったらこれ以上ループ をまわす必要がないからです。このbreakはなくても動作は変わらないのですがループを無駄にまわさないため に書いています。弾を保管庫に送る処理をなくしてしまうと弾が貫通弾になります。 ・最後に今作ったcollisionDetection()をゲームループで呼び出せば終わりです。 public void run() { while (true) { ・・・ // 衝突判定 collisionDetection(); ・・・ } }

(5、敵の攻撃(ビーム))

(ビームソース) import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import javax.swing.ImageIcon; /** * ビームクラス */ public class Beam { // ビームのスピード private static final int SPEED = 5; // ビームの保管座標(画面に表示されない場所) private static final Point STORAGE = new Point(-20, -20); // ビームの位置(x座標) private int x; // ビームの位置(y座標) private int y; // ビームの幅 private int width; // ビームの高さ private int height; // ビームの画像 private Image image; // メインパネルへの参照 private MainPanel panel; public Beam(MainPanel panel) { x = STORAGE.x; y = STORAGE.y; this.panel = panel; // イメージをロード loadImage(); } /** * ビームを移動する */ public void move() { // 保管庫に入っているなら何もしない if (isInStorage()) return; // ビームはy方向にしか移動しない y += SPEED; // 画面外のビームは保管庫行き if (y > MainPanel.HEIGHT) { store(); } } /** * ビームの位置を返す * * @return ビームの位置座標 */ public Point getPos() { return new Point(x, y); } /** * ビームの位置をセットする * * @param x ビームのx座標 * @param y ビームのy座標 */ public void setPos(int x, int y) { this.x = x; this.y = y; } /** * ビームの幅を返す。 * * @param width ビームの幅。 */ public int getWidth() { return width; } /** * ビームの高さを返す。 * * @return height ビームの高さ。 */ public int getHeight() { return height; } /** * ビームを保管庫に入れる */ public void store() { x = STORAGE.x; y = STORAGE.y; } /** * ビームが保管庫に入っているか * * @return 入っているならtrueを返す */ public boolean isInStorage() { if (x == STORAGE.x && y == STORAGE.x) return true; return false; } /** * ビームを描画する * * @param g 描画オブジェクト */ public void draw(Graphics g) { // ビームを描画する g.drawImage(image, x, y, null); } /** * イメージをロードする * */ private void loadImage() { ImageIcon icon = new ImageIcon(getClass().getResource("image/beam.gif")); image = icon.getImage(); // 幅と高さをセット width = image.getWidth(panel); height = image.getHeight(panel); } } エイリアン(敵)が発射するビームを実装します。これは弾のshotクラスのmove()を少し変えるだけで作る事ができます。 /** * ビームを移動する */ public void move() { // 保管庫に入っているなら何もしない if (isInStorage()) return; // ビームはy方向にしか移動しない y += SPEED; // 画面外のビームは保管庫行き if (y > MainPanel.HEIGHT) { store(); } } ほとんど同じなのでこういう場合は継承を使った方が良さそうな気がします。 ・次にエイリアンが攻撃してくる処理です。MainPanelのalienAttack()です。 /** * エイリアンの攻撃 */ private void alienAttack() { // 1ターンでNUM_BEAMだけ発射する // つまりエイリアン1人になってもそいつがNUM_BEAM発射する for (int i = 0; i < NUM_BEAM; i++) { // エイリアンの攻撃 // ランダムにエイリアンを選ぶ int n = rand.nextInt(NUM_ALIEN); // そのエイリアンが生きていればビーム発射 if (aliens[n].isAlive()) { // 発射されていないビームを見つける // 1つ見つけたら発射してbreakでループをぬける for (int j = 0; j < NUM_BEAM; j++) { if (beams[j].isInStorage()) { // ビームが保管庫にあれば発射できる // ビームの座標をエイリアンの座標にセットすれば発射される Point pos = aliens[n].getPos(); beams[j].setPos(pos.x + aliens[n].getWidth() / 2, pos.y); break; } } } } } NUM_BEAMは用意するビームの数です。ここでは20発になっています。この20発をエイリアンをランダムに決めて 発射させています。死んでいるエイリアンがビームを発射したり保管庫にないビームが発射されると変なのでforループを まわして生きているエイリアン保管庫に入っているビームを探しています。 ・次にエイリアンの攻撃とプレイヤーとの衝突判定です。この処理も先にやった衝突判定とほぼ同じです。 これはplayerクラスのcollideWith()です。ここではプレイヤーとビームの短形が重なっているかを調べます。 /** * プレイヤーとビームの衝突を判定する * * @param beam 衝突しているか調べるビームオブジェクト * @return 衝突していたらtrueを返す */ public boolean collideWith(Beam beam) { // プレイヤーの矩形を求める Rectangle rectPlayer = new Rectangle(x, y, width, height); // ビームの矩形を求める Point pos = beam.getPos(); Rectangle rectBeam = new Rectangle(pos.x, pos.y, beam.getWidth(), beam.getHeight()); // 矩形同士が重なっているか調べる // 重なっていたら衝突している return rectPlayer.intersects(rectBeam); } ・次にMainPanelクラスのcollisionDetection()にプレイヤーとビームの衝突検出を加えます。 private void collisionDetection() { // エイリアンと弾の衝突検出 ・・・ // プレーヤーとビームの衝突検出 for (int i = 0; i < NUM_BEAM; i++) { if (player.collideWith(beams[i])) { // プレーヤーとi番目のビームが衝突 // ビームは保管庫へ beams[i].store(); // ゲームオーバー initGame(); } } } プレイヤーがビームにあたったらゲームオーバーです。initGame()を呼び出してゲームを初期化しています。 サウンドの再生についてはMainPanelクラスでまとめてAudioClipを定義しています。 // サウンド private AudioClip fireSound; private AudioClip crySound; private AudioClip bombSound; コンストラクタでロードしています。 // サウンドをロード fireSound = Applet.newAudioClip(getClass().getResource("se/pi02.wav")); crySound = Applet.newAudioClip(getClass().getResource("se/pi00.wav")); bombSound = Applet.newAudioClip(getClass().getResource("se/bom28_a.wav")); 音を鳴らすのはplay()です。 // サウンドを再生 fireSound.play(); crySound.play(); bombSound.play(); 次にinvaderクラスを作る import java.awt.Container; import javax.swing.JFrame; /** * インベーダーゲーム本体 */ public class Invader extends JFrame { public Invader() { // タイトルを設定 setTitle("インベータ"); // サイズ変更不可 setResizable(false); // メインパネルを作成してフレームに追加 MainPanel panel = new MainPanel(); Container contentPane = getContentPane(); contentPane.add(panel); // パネルサイズに合わせてフレームサイズを自動設定 pack(); } public static void main(String[] args) { Invader frame = new Invader(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }

(実行ファイルを作る)

最後に自己実行型JARファイルを作ります作るにはマニフェストファイルが必要になります。ここでMANIFEST.MFとします。 MANIFEST.MFのなかでmain()を持つクラスを指定します。ここではinvaderクラスがmain()を持っているのでinvaderを 指定しています。 Manifest-Version: 1.0 Main-Class: invader 次に jar cvfm invader.jar MANIFEST.MF *.java *.class *gif *wav と実行する事でjarファイルが作る事ができます。それが実行ファイルとなります。

(感想)

今回のプログラミングはプログラム自体が複数あってそれをまとめたりする作業があったりと大変だったけどゲームプログらミングという事 でいろいろ楽しみながらすることができました。