2009年11月3日

ヘックスなシミュレーションゲーム

以前公開したSRPGの戦闘システム風JavaAppletを更新しました。
描画周りと敵AIに手を加えました。

ヘックスのアルゴリズムの実装について書いてみようと思います。
結構苦労したので・・・。

ヘックスの座標系:
特に考えずに決めてしまったので、ちょっと後悔してます。
基本的には
□ □ □ □
 □ □ □ □
□ □ □ □
 □ □ □ □
こんな感じのギザギザなx座標を使ってます。
カーソルの移動とかはこの座標系がいいですね。

距離を考えるときには
□ □ □ □
 □ □ □
□ □ □ □
 □ □ □
こんなのですね。
縦方向基準な座標系とでもいうのでしょうか。
距離についての詳細は下記。

□ □ □ □
 □ □ □ □
  □ □ □ □
   □ □ □ □
多分最も一般的なのがこれだと思うんですが、
結局使わずに終わってしまいました。
一応言い訳しておくと、
座標系の実装は隠すようになってるから何でもいいんです。
今更変えるのは面倒なんです・・・。意味ないし・・・。

2ヘックス間の距離:
http://blogs.wankuma.com/izmktr/archive/2009/06/26/176657.aspx

こちらのブログを参考にしました。天才だと思います。
見つけるのに苦労しました。検索結果のもっと上のほうに出てきてもいいのに・・・。

射線:
障害物の向こう側に攻撃が届かないシステムについて。
点と線分の距離から、
障害物のあるヘックスの中心からの距離が一定以下ならそいつに阻まれます。
http://www.deqnotes.net/acmicpc/2d_geometry/lines
こちらを参考に点と線分の距離を求めてます。

以下ソース引用
private static int obstacleHeight = 4; // 障害物の高さ
private static int unitHeight = 2; // ユニットの高さ
/* 
 * 攻撃者の座標はcurrentとして設定済。
 * 目標の座標targetと射程Range(水平方向と高さ方向の射程。min, maxは水平方向の射程)から、
 * 攻撃可能か否かbooleanを返す。
 */
public boolean raySearch(Coord<Integer> target, Range range) {
 if(!searchFloat(target.x, target.y, range)) // range内にtargetがいなければ偽
  return false;
 
 int hexSize = 128; // 理論上は1でもいいが、大きい方が誤差が出にくい?
 double r = hexSize/2 * 1.732/2 * 1.2; // hexsizeを覆う円の半径。これに射線が触れるか判定する。
 
 Coord<Double> currentC = getCoordByHex(current.x, current.y, hexSize); // ヘックス座標から実座標を取得。
 Coord<Double> targetC = getCoordByHex(target.x, target.y, hexSize);
 
 double thetaL = Math.atan2(targetC.y - currentC.y, targetC.x - currentC.x); // 射線の角度
 
 for(int y = 0; y < map.length; y++)
  for(int x = 0; x < map[y].length; x++) { // マップ上のヘックス全てについて、
   if((target.x == x && target.y == y)
     || (current.x == x && current.y == y)
     || distanse(current.x, current.y, x, y) > range.max) // 自分か目標か射程外なら障害物になり得ない
    continue;
   
   Coord<Double> barC = getCoordByHex(x, y, hexSize); // 今チェックするヘックスの実座標
   
   int offsetH = 0;
   if(map[y][x] <= OBSTACLE) // 障害物なら
    offsetH = obstacleHeight;
   
   int difH = height[target.y][target.x] - height[current.y][current.x]; // 自分と目標の高さの差
   int h = height[y][x]  + offsetH - unitHeight; // ユニットの高さの分、当たりやすくなる
   if((barC.x - currentC.x) * difH / (targetC.x - currentC.x) + height[current.y][current.x] > h
     || (barC.y - currentC.y) * difH / (targetC.y - currentC.y) + height[current.y][current.x] > h) // このヘックスが、xかy方向に高さで邪魔にならなければ、次。
   continue;
   
   // 以降、図形の問題をごにょごにょ
   double theta = Math.atan2(barC.y - currentC.y, barC.x - currentC.x) - thetaL;
   
   if(Math.cos(theta) < 0 || Math.cos(thetaL - Math.atan2(targetC.y - barC.y, targetC.x - barC.x)) < 0)
    continue;
   
   if(Math.hypot(barC.x - currentC.x, barC.y - currentC.y) * Math.abs(Math.sin(theta)) < r)
    return false;
  }
 return true;
}

汚いコードだと思いますがすいません。
この関数は引数のtarget座標に攻撃が届くか否かを返します。
Coordは座標(x,y)を持つクラスです。

current:攻撃者の座標としてすでにセットされてます。
hexSize:ただの係数なので1でも何でもいいんですが、
計算する上である程度の大きさがあったほうがいいです。
後で位置関係をatan2に突っ込むのですが、そこがうまくいかなくなります。
r:ヘックスを覆う円の半径です。√3/2が六角形の半径です。
大きめにしないと隣り合った障害物の隙間を通り抜けてしまいます。
getCoordByHex():はヘックス座標系から実際の画面上(?)の座標を返します。

まず、距離が射程range外もしくはマップ外であればはじきます。
でもって、マップのすべてのヘックスについて
障害物でないか、射程外か、自分自身かターゲットでなければ、
上で引用した点と線分の距離を用いて判定しています。

後半になるほど面倒になって説明が雑になってますが許してください・・・。

0 件のコメント: