技と名がつくと深入りしてしまうスキルマニアのエンジニア

ドラクエで学ぶオブジェクト指向(4) 戦いのプレリュード……の前に

»

 大枠が決まったところで、中間ゴールを設定しようと思う。いくら後追いでも、いまのペースではいつまでたっても完成しないだろう。

 適切な目標がなければモノ作りは進まない。「OOPによるドラクエの完全再現」というゴールでは、誰が見てもあまりにも規模が大きい。分割して統治せよ。実現可能なサイズに分割するべきだ。

 お城やダンジョンを作って、フィールドを自由に動き回れるシステムというのもいい。しかし、ドラクエといえばバトルである。まずは、戦闘システムの構築から始めてみようと思う。文字ベースのバトルなら画像を用意しなくてもいいのでロジックに集中できるだろう。

 パーティバトルもあるが、最初は1対1のバトル。勇者対モンスターの構図も取り払うつもりでいる。Heroクラスは考える要素が多いので、しばらく寝かせておく。モンスター対モンスターの戦いから作っていこう。最近ではモンスターズなんてシリーズもあるみたいだ。

 n対nの戦い、対勇者戦、グラフィックやエフェクトの表現などはアジャイル的展開で追加していくことにしよう。真にOOP的な実装ができたなら、想定内の変更は容易なはずである。

 「テキストベースのモンスター対モンスター(1対1)のバトル」

 これをファースト・イテレーションのゴールと定める。

■リファクタリング

 さて、目標をモンスター同士のバトルと定めたので、既存クラスを修正していく。前回、考察した獲得経験値と獲得Gがずっと気になっていた。

Monster.java
/** 倒された相手に与える経験値 */
private int givenEx = 0;
/** 倒された相手に与えるG */
private int givenG = 0;

 『勇者にとって、経験値とGは獲得するものであるが、モンスターの経験値とGは与えるものである。モンスターが勝利しても、勇者の経験値が得られない。同じものでも考え方が違うので、モンスター側ではgivenEx、givenGとした』

 実は、根本的に間違っている感じがする。過ちに気がつけば、さっさと修正するのが吉だろう。モンスター同士のバトルという明確な目標が決まったので、普通に経験値とGは保持することにした。Heroクラスと共通の要素になるので、親クラスのBattlerに持たせておく。

 シンプルにするために、攻撃力と守備力もいったんBattlerクラスに預けておく。このあたりのメソッドは最終的にどうなるかは、いろいろと迷うことになるだろう。

 Battlerクラスのパラメータはこのようになっている。

/** 名前 */
protected String name = null;
/** ヒットポイント */
private int hp = 0;
/** マジックポイント*/
private int mp = 0;
/** 最大ヒットポイント */
protected int maxHp = 0;
/** 最大マジックポイント */
protected int maxMp = 0;
/** すばやさ */
private int quik = 0;
/** ゴールド */
private int g = 0;
/** 経験値 */
private int ex = 0;
/** 攻撃力 */
protected int offense = 0;
/** 守備力 */
protected int deffense = 0;

 max_hpという書き方が気に入らなかったので、微妙に変数名は修正している。納得できるまで名前を変更できるのも、リファクタリング機能のおかげである。そんなことはEclipseでもNetBeansでもVisual Studioでも常識なので特に解説はしない。

 戦闘に関する基本動作をBattlerクラスに持たせると、Monsterクラスには何も処理がなくなった。このクラスはモンスター固有の攻撃や特殊な振る舞いのために残しておくことにする。

■HPを増減させるメソッド

 各フィールドにはsetter/getterと呼ばれるアクセサメソッドを用意した。getterはよいとしてsetterの場合、

 setHp(10)→変数hpに10を代入

という処理となる。戦いは命の削りあいである。10のダメージを与えたら、hpから10を減らしたい。hp += damage のようなメソッドを用意する。

/**
* HPを0から最大HPの範囲で増減させる。
* @param hp
*/
public void addHp(int hp){
    this.hp = ( this.hp + hp < 0 ) ? 0 : ( this.hp + hp > this.maxHp  ) ? this.maxHp: hp;
}

 引数が正の場合はHP回復させる。負の場合はダメージを与えることができる。ただ、addというネーミングがどうもしっくりとこない。ほかによい名前が浮かんだらメソッド名を変更しよう。

 MPなど、ほかのメソッドにも同様のメソッドを追加しておいた。上限のないGなどは少しシンプルな処理になる。がんばればいくらだってお金持ちになれる、というユメのあるメソッドだ。厳密にいうとintの上限が存在するのだが……せめてlongにするべきか迷い中である。

/**
* Gを0以上の範囲で増減させる。
* @param g
*/
public void addG(int g){
    this.g = ( this.g + g < 0 ) ? 0 : g;
}

 お金が足りない場合は、HasNotGoldException を投げてみたい。

■スレッドについて

 HP、MPのsetterがスレッドセーフじゃないというご指摘をいろんな方からいただいた。よくよく考えてみると範囲チェック後、代入する前に別のスレッドが値を書き換える可能性がある。

 ふたつのスレッドA,Bが実行されているとして、setMpメソッドの動きを考えてみる。それぞれが同時にsetMpを実行したとする。

スレッドA 0~最大MPの範囲チェック
スレッドB MPが変更する処理をゴニョゴニョ
スレッドA MPに値を代入

 という順番になったときに スレッドAの範囲チェックが無意味になる。

 ギラの消費MPは2である。例えば、残りMPが2の場合にギラを唱えようとしている瞬間にMPを吸い取るマホトラの呪文がかけられたらどうなるだろう?

 このような場合、マルチスレッド下では、ギラ呪文を唱えるために必要なMP消費と、マホトラで奪われるMPのどちらが優先されるかは実行するまで分からない。MPが吸い取られているにも関わらず、ギラが実行されてしまう可能性もあるのだ。

 synchronizedメソッドを使えば、この問題は回避できる。しかし、synchronizedメソッドは、多用すればするほど全体の処理速度が低下する。本当に必要でない限り乱発するのは避けたい。StringBufferに対するStringBuilderクラスのようにスレッドアンセーフを選ぶという道もあるだろう。

 戦闘は単一スレッドで行うという方針もあるのでこれも保留しようと思う。マルチスレッド時における「同時ギラ-マホトラ問題」と定義しておく。

 それなりにペンディングが増えてきている。放置していると忘れることは間違いないので、簡単に課題管理表なんてものを作ってみた。課題が増えればtracでチケット管理をすることになるかもしれない。

Kadai_dq4_2

 これで下準備はできたと思う。次回はいよいよ攻撃メソッドに着手してみよう。

 どこか遠くからレベルアップを告げるアラート音が鳴り響く……。

hagane_dq_src4.zipをダウンロード

Comment(2)

コメント

はがねのつるぎ

>wanwanさん

コメントありがとうございます。

初めて知りました。ドラクエⅠからずっとマジックパワーだったみたいですね。
衝撃の事実に戸惑っています(^^)

次回から「マジックパワー」に統一させていただきます。

貴重な情報ありがとうございます。

コメントを投稿する