unity開発メモ帳

Unityゲーム開発です

【ゴリ押し】六角形のマップ(ヘックスマップ)をタイルごとにハイライト表示させる

はじめに

こんにちは。

ストラテジーゲームでよく見る、六角形のマップをunityで作った話です。

前もって記述しますが作者は数学的知識が皆無なので、もっと楽な実装方法が多分あります、、、

 

 

モデルを用意する

左:マップ用モデル 右:ハイライト用モデル

私はblenderの <add mesh extra objects> アドオンを利用して作成しました

そこが知りたい方は[add mesh extra objects ハニカム]で検索を。

 

 

 

マウスに合わせてハイライトを動かす

このままでは味気がないので、ハイライト表示を追加します

実装手順

  1. カメラからマウスのrayを飛ばす
  2. rayがマップに衝突した座標を取得する
  3. 取得した座標をマップに配置できるよう整える
  4. 整えた座標を使ってハイライト表示を移動させる

1.カメラからマウスのrayを飛ばす

これは簡単に実装できました。

Camera-ScreenPointToRay - Unity スクリプトリファレンス

ScreenPointToRayはスクリーン上の点から画面の奥へと向かうrayを作ってくれます。便利。

 

2.rayがマップに衝突した座標を取得する

まずは、マップのオブジェクトに8番のレイヤーを追加,設定しておきます(名前は何でもok)

  


次に、この行を追加します

 

 

これにより、rayがマップに当たったときの座標をハイライト表示の座標に使うことができます。(うまく動かない場合はコライダーがマップについているか確認してください)

この段階ではハイライトがタイルをまたいで移動してしまっています

 

3.取得した座標をマップに配置できるように整える

ここが難関でした。

考え方としては、マップの座標を「座標A」と「座標B」の2種類に分け、マウスが座標Aの範囲内であれば処理Aを、マウスが座標Bの範囲内であれば処理Bを行わせるというものです。

 

なぜ2種類の座標に分けるかと言うと、座標Bの境界は「斜め線」で、座標Aの境界は「縦線」なので、これらの座標を1種類の座標として認識してしまうと座標の正規化が難しくなるためです(説明難しい)

 

座標Aと座標Bの処理分け

(作者は数学的知識が皆無なので雑で面倒くさい方法をとっています、、、)

ここから出てくる座標の具体的な数値は私が測った数値ですので、各自適切な値を当てはめてみてください

 

 

下ののNormalizePos関数は、受け取った座標を確認し,それがどの座標グループに入るのかを判断しています 

gist359d8065925b9f4153c4642e97a45201

 

 

↓コードの補足画像.  ※拡大推奨

 

 

また、座標Aは更に

  • Zが0付近のA-1
  • それ以外のA-2

に分けられています

 

 

座標Aの処理

 

A-1の処理

//原点の行のXとZ
if (absZ < MinZ)
{

   //原点の行のときのZ
   repos.z = 0f;

   //Xの処理
   if (Mathf.Abs(pos.x) > addedX)
     //原点列以外
     repos.x = Mathf.Floor((pos.x + addedX) / sAddedX) * sAddedX;
   else
     //原点列
     repos.x = 0f;

}

>>A-1の範囲内のとき、

  Z座標 : 0

       X座標 : マウスのX座標を正規化

 

A-2の処理

 //ZがA-2の範囲であれば
else if (absZ % addedZ >= MaxZ || absZ % addedZ <= MinZ)
{
    
    //行数
    float ColumnsNum = Mathf.Round(Mathf.Abs(pos.z) / addedZ);
    
    //Zの処理
    repos.z = ColumnsNum * addedZ * Mathf.Sign(pos.z);


    //Xの処理
    //偶数行と奇数行の処理
    if (!Utilities.Isodd(ColumnsNum))
    {

        if (Mathf.Abs(pos.x) < addedX)
            //0列目
            repos.x = 0f;
        else
            //0列目以外
            repos.x = Mathf.Floor((pos.x + addedX) / sAddedX) * sAddedX;

    }
    else
        //奇数行は偶数行を半個分右にずらす
        repos.x = Mathf.Floor(pos.x / sAddedX) * sAddedX + addedX;

}

>>A-2の範囲内のとき

  Z座標 : [現在の行]と[1行あたりの距離]、そして[pos.zの符号]から計算

  X座標 : 0列目はx=0、それ以外の列の奇数行目は単純な座標の正規化を、

      偶数行目はその正規化した値を右にずらします

 

※Utilities.Isodd()は渡した値が奇数ならtrueを返す静的メソッドです。詳しい説明は割愛します。

 

 

座標Bの処理

ここが1番面倒でした。

 

考え方としては、マップ上に一次関数のグラフを描き、利用します

一次関数は大雑把に言うと直線のグラフです

 

一次関数の式はどう求めるかと言うと、

一次関数の式は y=ax+b の形で表されます。今回、bの値は引くグラフによって異なるので求めず、aの値を求めます。

 

端的に言うと、下の図で言う[MinZ]を[addedX]で割ってください。

私が測った値を入れて計算すると、

MinZ / addedX = 0.125 / 0.21644 = 0.577527.....

でaの値は約0.57753となり、一次関数の式は

y = 0.57753x + b

と表せます。

マウスのグラフの位置と基準グラフの位置関係は、この式のbの値(切片)を見ればわかります

 

 

ここまでをコードに書くと、

else
{

    //y=0.57753x+bの式のbの値を求めて利用

    //傾きa
    float a = 0.57753f;

    //0.57753Xに代入
    float x = pos.x * a;

    //xとyの値からbの値を求める
    float b = pos.z - x;

}

 

 

しかし、このままでは右斜め上へ伸びるグラフのみしか描けないので、マウスの位置によって下図のように正と負を切り替える必要があります

 

 

private bool IsPlusPos(float x,float y)
{

    float addedZ = 0.375f;
    float addedX = 0.21644f;

    float nmX = Mathf.Floor(x / addedX);

   float nmY = Mathf.Floor(y / addedZ);

    bool isPlus = false;

    if (Utilities.Isodd(nmX-nmY))
    {

        isPlus = true;

     }

     return isPlus;

} 

IsPlusPos関数は、渡したx,yの座標が正のグラフの範囲内であればtrueを返し、負のグラフであればfalseを返す関数です

 

この関数を利用すると、先程のコードは

else
{

    //y=0.57753x+bの式のbの値を求めて利用

    //傾きa
    float a = 0.57753f;
    
    //追加-----------------------------

    //+と-のグラフを切り替える
    if (!IsPlusPos(pos.x, pos.z))
        a = -a;

    //---------------------------------     

    //0.57753Xに代入
    float x = pos.x * a;

    //xとyの値からbの値を求める
    float b = pos.z - x;

}

となり、マウスの座標によって正負を切り替えられるようになりました。

 

あとはこのbの値を使って正規化後の座標を特定するのみです。

else
{

    //y=0.57753x+bの式のbの値を求めて利用

    //傾きa
    float a = 0.57753f;
    
    //+と-のグラフを切り替える
    if (!IsPlusPos(pos.x, pos.z))
        a = -a;

    //0.57753Xに代入
    float x = pos.x * a;

    //xとyの値からbの値を求める
    float b = pos.z - x;
    
    //追加___________________________________

    
    float sign = Mathf.Sign(pos.z);
    
    //bとyの符号が違うとバグるので対策。
    if (sign != Mathf.Sign(b))
        sign = -sign;
    
    //MinZ以上MaxZ未満であればok
    float sub = MaxZ-0.01f;

    //bを0.25で割ったあまりをもとに分岐 
    if (b % MaxZ < MinZ * sign)
        //MinZ未満ということは基準より少しはみ出しているということ
        repos = NormalizePos(new Vector3(pos.x, pos.y, pos.z + sub));
    else
    if (b % MaxZ >= MinZ * sign)
        //MinZ以上ということは基準線にぎりぎり届いていないということ
        repos = NormalizePos(new Vector3(pos.x, pos.y, pos.z - sub));

}

追加後の内容では、

bをMaxZで割ったあまりを「マウスのグラフの切片」

MinZを「基準グラフの切片」

として上下どちらのタイルに正規化すべきかを判断しています。

 

上下どちらのタイルに正規化すべきかが決まったあとは、調節した座標を用いて自身であるNormalizePosを再度呼んでいます

 

座標の正規化メソッドのまとめ

NormalizePos関数をまとめると次のようになります

giste36cbe067999d9f6d76083b17ab8ae28

 

 

4.整えた座標を使ってハイライト表示を移動させる

ここからは、ここまでの部品を組み合わせていきます

gist663f2f88f41aff410cdd1a6a135c9bad

 

実行すると,,,

すげえ!

 

 

感想

ぶっちゃけこの仕様いるかどうかといえばいらないです。

数学の知識が足りないので勉強したらもっと良いコードになると思います。(数学勉強しよ、、、

文章やコードで何かおかしな点があればコメントをお願いします。

 

ありがとうございました