JavaでRogueを書いてもらう(2)
さて、Rogueの続きを書いていきましょう。ソースがまだ、ダウンロードできないのですが私がちゃんと用意していないだけです。
今日は、次回の分量を少なくするために、簡単なところを加えておきましょう。
攻撃と移動の融合
現在の移動メソッドは次のようになっています。
class Character{ ... void move(int x, int y){ this.x = x; this.y = y; } }
これを変更して、移動先に敵(他のキャラクタ)がいたら攻撃を行い、いなければ実際に移動するようにしてみましょう。
class Character{ ... Dungeon dungeon; void move(int x, int y){ Character c = dungeon.getCharacter(x, y); if(c != null){ this.x = x; this.y = y; }else{ attack(c); } } } class Dungeon{ private List monsters = new ArrayList(); private Character[][] charmap; ... Character getCharacter(int x, int y){ return charmap[y][x]; } }
これによって、attack()はprivateにしてしまいましょう。(つまり、直接にattack()を呼び出すことはしないで、move()の結果、その移動先に敵がいたら自動的にattack()してくれるようにします。)このように、外部から直接呼び出せるインターフェース(メソッド)を少なくします。
壁に移動? そりゃあ無理
まず、もう少しメソッドを増やしておきましょう。実際にキーボードなりから発する命令は「左に移動せよ」などですから、それらを直接メソッドにします。(move()を直接呼び出すのも、これによって原則的に禁止します。)
class Character{ ... public void left(){ move(this.getX()-1, this.getY()); } public void right(){ move(this.getX()+1, this.getY()); } public void up(){ move(this.getX(), this.getY()-1); } public void down(){ move(this.getX(), this.getY()+1); } }
これでleft()は左に敵がいればその敵を攻撃しますし、いなければ左に移動できます。
しかし、左は壁かもしれません。壁に移動できては困ります。そのあたりを実装します。壁チェックはleft()だのup()だので個々にやる必要はなく、move()で行います。同じことは二箇所に書かない。これはソフトウェア開発の常識というものです。
class Character{ ... boolean move(int x, int y){ // booleanに変更 if(dungeon.movable(x, y)){ Character c = dungeon.getCharacter(x, y); if(c == null){ this.x = x; this.y = y; }else{ attack(c); } return true; }else{ System.out.println("その方向には動けない"); return false; } } public boolean left(){ return move(this.getX()-1, this.getY()); } public boolean right(){ return move(this.getX()+1, this.getY()); } public boolean up(){ return move(this.getX(), this.getY()-1); } public boolean down(){ return move(this.getX(), this.getY()+1); } } class Dungeon{ ... boolean movable(int x, int y){ switch(map[y][x]){ case WALL: return false; default: return true; } } }
この変更で、move()やleft()その他はbooleanを返すことにしました。例えば左へ移動しようとしたが壁でしたという場合にはfalseを返すことで、「その移動が失敗した(だから別の選択肢を選んでくださいね)」というメッセージを呼び出し元(ユーザといってもよいです)に伝えられるからです。
実際に移動できるかどうかは、Characterクラスではなく、Dungeonクラスでないと判別できないわけですから、Dungeonクラスにmoveble()というメソッドを創設し、それをCharacterクラスで呼び出します。
メソッドの実装場所
今回に限りませんが、初心者レベルでも、開発では次のことに気を遣うべきです。
- 同じことを二箇所に書かない
- 同じクラスに似たことを集める
前者ですが例えば、
public boolean left(){ if(dungeon.moveble(x, y)){ move(this.getX()-1, this.getY()); return true; }else{ return false; } }
とか、left()で動けるかどうかの判定をしない。move()に書いてしまえばleft()、right()その他に重複して書く必要がありませんし、処理が今後複雑になってもmove()さえ変更すればOK。upleft()とか斜め移動を追加したときも気にしなくてよいことになります。
次に、後者です。
例えば、movable()をDungeonでなくCharacterに実装することだってできます。それはしなかったわけですが、次のようになるでしょうか。
class Character{ ... boolean movable(int x, int y){ char[][] map = dungeon.getMap(); switch(map[y][x]){ case Dungeon.WALL: return false; default: return true; } } } class Dungeon{ static char WALL = '#'; ... char[][] getMap(){ return map; } }
別に動くといえば動くのですが、「この位置には動けるのか?」ということは、一括してDungeonに書くのが非常に自然です。
もしかしたら、今後Character以外でも、movable()が必要になるかもしれません。その時に個々に実装するのは愚かです。また、WALLの他に今後別の地図要素を導入するかもしれません。その時、Dungeonにその要素を書き加えるのは当然ですが、それに伴ってCharacterのmovable()にまで影響してくるようだとたまりません。一つの関心事項は一つのクラスに集め、他のクラスから例えばWALLとかそういったものにアクセスさせないのが、簡潔なプログラムにつながります。
次回予告
次回は、ついに、プレーヤを動かすと、敵が自動的に動くという仕掛けを作りたいと思います。