データベースとは?|初心者でもわかるプログラミング学習入門
GEEK JOB編集部
前章までで、シューティングに必要なPlayer、Bullet、Enemyといったクラスを作成し、複数の要素に対してはListで管理することを学びました。
本章では、上記の3つのクラスの親として、GameObjectというスーパークラスを作成し、それを継承することで、コードを簡略化する方法について学びたいと思います。
以下が完成イメージです。
それでは、まずはGameObjectというクラスを作り、Player、Bullet、Enemyに共通する部分を抽出しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
GameObject.java public class GameObject { protected int x; protected int y; protected int width; protected int height; public void update() { } public void draw(Graphics g) { } } |
こうすることで、たとえば先ほどまで書いていたEnemyクラスは、以下のように書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Enemy.java public class Enemy extends GameObject { public Enemy(int x, int y) { this.x = x; this.y = y; this.width = 20; this.height = 20; } @Override public void update() { this.y += 1; } @Override public void draw(Graphics g) { g.drawRect(x, y, width, height); } } |
width、heightという変数は、前の3つのクラスにはありませんでしたが、のちのち衝突判定に使うことを考えて入れてあります。
同様にして、Playerクラス、Bulletクラスも書き換えます。
ここで、これまでprivateで宣言していた変数が、GameObjectクラスの中ではprotectedになっていることに気が付いたでしょうか。
これらはアクセス修飾子と呼ばれ、以下のような違いがあります。
修飾子 | アクセス権 |
public | 全てのクラスからアクセス可能 |
protected | 自クラス内、同じパッケージ、サブクラスのからならアクセス可能 |
なし | 自クラス内、同じパッケージからならアクセス可能 |
private | 自クラスからのみアクセス可能 |
アクセス権をそれほど意識しなくても、ある程度であればプログラムを書くことはできますが、より安全なコードを書くためには、このような形で適切にアクセス権を制限し、予期せぬ操作によるデータ改ざんを防ぐ必要があります。
先ほどまでは、自クラスからのみ参照していたので、private修飾子を用いていましたが、今回はGameObjectを継承したPlayer、Bullet、Enemyといったサブクラス内で変数を参照しているので、protected修飾子に変更する必要があります。
ところで、このように変数を自クラス以外から参照できるようにする方法には、もう1つやりかたがあります。
それが、private変数にアクセサを設定するという方法です。
説明するより実際に見てもらったほうが早いと思うので、以下のコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
GameObject.java public class GameObject { private int x; private int y; private int width; private int height; public int getX(){ return x; } public int getY(){ return y; } public void setX(int x) { this.x = x; } public void setY(int y){ this.y = y; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public void update() { } public void draw(Graphics g) { } } Enemy.java public class Enemy extends GameObject { public Enemy(int x, int y) { this.setX(x); this.setY(y); this.setWidth(20); this.setHeight(20); } @Override public void update() { this.setY(this.getY()+1); } @Override public void draw(Graphics g) { g.drawRect(this.getX(), this.getY(), this.getWidth(), this.getHeight()); } } |
x、y、width、heightそれぞれに対して、getやsetというメソッドが追加されています。これらはゲッタ、セッタといい、あわせてアクセサと呼びます。
それぞれ該当の変数の値の参照や代入というシンプルな機能しかありませんが、これによってインスタンスの変数の値を自クラスの外から直接参照・変更できないようにでき、コードの安全性を高めることができます。
変数をpublicやprotectedにして、「enemy.width」のような形で簡単にアクセスできるようにするか、変数をprivateにして、アクセサを設定して安全性を高めるかは、常にトレードオフの関係にあり、プログラマは状況に応じて両者を使い分ける必要があります。
今回は読みやすさと簡略さを優先して、前者の方法を取りたいと思います。
以上で、3つのクラスに共通する処理をGameObjectに抽出したことで、いくらかコードがシンプルになったかと思います。
今度は、オブジェクトの生死を意味するisAliveという変数と、そのアクセサを設定してみましょう。
ただし、isAliveはboolean変数なので、Javaの標準のコーディング規約に則って、ゲッタにはisAlive()というメソッドを用います。
また、今回のシューティングゲームの中でオブジェクトが生き返ることはないため、セッタの代わりにdelete()というメソッドを作り、意味を明確にしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
GameObject.java public class GameObject { protected int x; protected int y; protected int width; protected int height; private boolean isAlive = true; public boolean isAlive() { return isAlive; } public void delete() { this.isAlive = false; } public void update() { } public void draw(Graphics g) { } } |
GameObjectクラスに以上の処理を書いておけば、Enemyなどの各クラスでは、「自分が生きている場合に描画する」ことを意味する以下の部分だけ書き加えればいいことになります。
1 2 3 4 5 6 |
Enemy.java public void draw(Graphics g) { if(this.isAlive()) { g.drawRect(x, y, width, height); } } |
続いて、GameObjectに衝突判定のメソッドと、オブジェクトの生死を格納する変数を追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
GameObject.java public class GameObject { protected int x; protected int y; protected int width; protected int height; private boolean isAlive = true; public boolean isAlive() { return isAlive; } public void delete() { this.isAlive = false; } public boolean collideWith(GameObject object) { if(!object.isAlive()) { return false; } if( this == object ) { return false; } return x < object.x + object.width && object.x < x + width && y < object.y + object.height && object.y < y + height; } public void update() { } public void draw(Graphics g) { } } |
collideWith()が衝突判定で、引数として渡されたオブジェクトと自分が衝突している場合にtrueを返すメソッドになっています。
まず1つめのif文で、対象のobjectが死んでいたら、衝突することはありえないので、falseを返すようにしています。
2つめのif文では、自分自身と衝突判定を行わないように、falseを返しています。
return文の中身が実際の衝突判定になりますが、具体的にどのような判定を行っているかは、本筋とするオブジェクト指向の説明から逸れるので割愛します。
このcollideWith()を利用して、定期的に呼ばれるField.javaのactionPerformed()の中で、「もしenemyがbulletと衝突していたら、衝突した二つのオブジェクトを削除する」という処理を追加すると、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
Field.java public void actionPerformed(ActionEvent e) { bullet.update(); for(Enemy enemy: enemies) { if(enemy.collideWith(bullet)) { enemy.delete(); bullet.delete(); } enemy.update(); } repaint(); } |
このように、これまでクラスごとに個別に設定していたメソッドが、親クラスであるGameObjectに追加するだけで、Player、Bullet、Enemyのどのクラスでも利用できるようになるのです。
以上で、ひとまず弾を出して敵を倒せるという、シューティングの最低限の機能は実装できました。