PythonのArcadeでHexmap上のユニットを移動させる

# コードの実行結果
初期起動画面
![スクリーンショット 2024-11-04 21.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/0d31d51e-8077-b396-c1f7-a8e4ad9038c0.png)

ユニットを選択すると赤い囲いが出てきます。
この状態の時にhexmap上を移動できます。
![スクリーンショット 2024-11-04 21.10.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/037f3e95-dcc3-92a6-056d-d4731d617c08.png)

マウスでhexmap上をクリックすると、そのhexmapの中心座標位置に移動します。
![スクリーンショット 2024-11-04 21.11.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/911ea371-3a68-3c5d-7ac3-c0c654e0004e.png)

# コードの概要
[前回記事](https://qiita.com/nami_fumi/items/446126377a818e2923c3)にコードを付け加えて、hexmap上でユニットをクリックして移動させるインタラクティブなデモを作成しました。
画像は適当なものを使ってください。

## ポイント

* **Hexマップの描画:** `HEXMAP`クラスでHexマップの描画と、マウス座標から最も近いHexの中心座標を取得する処理を実装しています。
* **ユニットの選択:** マウスプレスイベントでユニットをクリックしたかを判定し、選択状態を管理します。
* **ユニットの移動:** マウスリリースイベントで、選択されたユニットをHexマップの中心座標へスムーズに移動させます。スムーズな移動は、移動ベクトルの正規化によって実現しています。
* **ハイライト表示:** クリックされたHexの周囲をハイライト表示することで、移動先を分かりやすくしています。←今回のコードでは、使われていません

# コード
## main.py
```python
import arcade
import math
from components.HEXMAP import Hexmap

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "hexmap上のユニット移動の例"
UNIT_FILEPATH = "xxxx.png"
RADIUS = 30  # 半径
GRID_SIZE = 10  # hexの個数
UNIT_SCALE = 0.3
UNIT_SPEED = 3

class MyUnit(arcade.Sprite):
    def __init__(self, filename, scale, speed):
        super().__init__(filename, scale)
        self.speed = speed
        self.destination_x = None # destinationは方向という意味。
        self.destination_y = None
    
    def update(self):
        # 方向先が指定されたら、ユニットを移動
        if self.destination_x is not None and self.destination_y is not None:
            # 移動方向の正規化
            """
            現在地とマウスクリック時の距離を計算し、移動するときに反映
            
            # Geminiさんに移動の正規化をするようにした理由を説明
            ユニットがクリックされた地点へ一定の速度で滑らかに移動するようにするためです。
            正規化によって、移動ベクトル (dx, dy) の長さを1に保ち
            方向のみを表す単位ベクトルに変換しています。
            """
            dx = self.destination_x - self.center_x
            dy = self.destination_y - self.center_y
            distance = math.sqrt(dx**2 + dy**2)
            
            if distance > self.speed:
                # 移動方向を正規化
                # 移動のスケールをmax1にしている
                dx = dx/distance
                dy = dy/distance
                
                # ユニットの移動
                self.center_x = self.center_x + dx
                self.center_y = self.center_y + dy
            else:
                self.center_x = self.destination_x
                self.center_y = self.destination_y
            # 初期値に戻す
            self.destination_x = None
            self.destination_y = None

class MyWindow(arcade.Window):
    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        self.unit_list = arcade.SpriteList()
        self.hex_map = Hexmap(radius=RADIUS, grid_size=GRID_SIZE)
        self.highlight = False
        self.select_unit = None
        self.unit_position = None
        
    def setup(self):
        # 背景の設定
        arcade.set_background_color(arcade.color.AQUAMARINE)
        
        # ユニットの初期設定
        self.unit = MyUnit(filename=UNIT_FILEPATH, scale=UNIT_SCALE, speed=UNIT_SPEED)
        self.unit.center_x = 225.0 # hexmap上の中心位置 X
        self.unit.center_y = 233.8268590217984 # hexmap上の中心位置 Y
        self.unit_list.append(self.unit)
        
    
    def on_draw(self):
        arcade.start_render()
        self.unit_list.draw()
        self.hex_map.draw_hexagon(highlight_trigger=self.highlight, clicked_hex=self.unit_position)
        
        # ユニットが選択されたら、周りを赤い視覚で囲む
        if self.select_unit:
            arcade.draw_rectangle_outline(
                center_x=self.select_unit.center_x,
                center_y=self.select_unit.center_y,
                width=self.select_unit.width + 10,  # 少し大きめに枠を描画
                height=self.select_unit.height + 10,
                color=arcade.color.RED,
                border_width=2
            )

    
    def on_mouse_press(self, x, y, button, modifiers):
        print("on_mouse_press")
        unit_hit = arcade.get_sprites_at_point(point=(x, y), sprite_list=self.unit_list)
        if unit_hit:
            self.select_unit = unit_hit[0]
            print(f"click unit")
        
        # self.unit.center_x = x
        # self.unit.center_y = y
    
    def on_mouse_release(self, x, y, button, modifiers):
        print("on_mouse_release")
        if self.select_unit:
            self.unit_position = self.hex_map.get_hex_center_position(mouse_x=x, mouse_y=y) # ユニットのhexmap上の場所
            if self.unit_position:
                self.select_unit.center_x = self.unit_position[0]
                self.select_unit.center_y = self.unit_position[1]
        else:
            # ユニットが選択されていない、つまりユニット以外のところを押した
            self.highlight = False
            self.select_unit = None
        print(f"unit point: X = {self.select_unit.center_x}, Y = {self.select_unit.center_y}")
    
    def on_update(self, delta_time):
        self.unit.update()
    
    def on_key_press(self, symbol, modifiers):
        if symbol == arcade.key.Q:
            print("Q key press")
            arcade.close_window()

def main():
    mywindow = MyWindow(width=SCREEN_WIDTH, height=SCREEN_HEIGHT, title=SCREEN_TITLE)
    mywindow.setup()
    arcade.run()

if __name__ == "__main__":
    main()
```

## components.HEXMAP
```python
import arcade
import math

class Hexmap:
    """
    六角形グリッドマップを生成、描画するクラス。
    ハイライト表示は、3hex分

    Attributes:
        radius (float): 六角形の半径。
        grid_size (int): グリッドのサイズ (行と列の数)。
        hex_centers (list): 各六角形の中心座標を格納するリスト。
    """
    def __init__(self, radius, grid_size):
        """
        Hexmapクラスのコンストラクタ。

        Args:
            radius (float): 六角形の半径。
            grid_size (int): グリッドのサイズ。
        """
        self.radius = radius
        self.grid_size = grid_size
        self.hex_centers = []

    # 頂点の計算
    def hexagon_vertices(self, x, y):
        """
        六角形の頂点座標を計算する。

        Args:
            x (float): 六角形の中心x座標。
            y (float): 六角形の中心y座標。

        Returns:
            list: 六角形の頂点座標のリスト。
        """
        vertices = []
        for i in range(6):
            angle = i * math.pi / 3
            vx = x + self.radius * math.cos(angle)
            vy = y + self.radius * math.sin(angle)
            vertices.append((vx, vy))
        return vertices

    # ヘックスマップの描画
    def draw_hexagon(self, highlight_trigger=False, clicked_hex=None):
        """
        六角形グリッドマップを描画する。

        Args:
            highlight_trigger (bool): ハイライトを有効にするかどうか。
            clicked_hex (tuple): クリックされた六角形の中心座標。
        """
        root_3 = math.sqrt(3)
        horizontal_spacing = 3 * self.radius / 2
        vertical_spacing = root_3 * self.radius

        for row in range(self.grid_size):
            for col in range(self.grid_size):
                x = col * horizontal_spacing
                y = row * vertical_spacing

                if col % 2 == 1:
                    y += vertical_spacing / 2
                self.hex_centers.append((x, y))
                points_list = self.hexagon_vertices(x, y)

                self.draw_single_hexagon(x, y, points_list, clicked_hex, highlight_trigger)

    # ハイライトの表示
    def draw_single_hexagon(self, x, y, points_list, clicked_hex, highlight_trigger):
        """
        個々の六角形を描画する。ハイライトの有無を判断。

        Args:
            x (float): 六角形の中心x座標。
            y (float): 六角形の中心y座標。
            points_list (list): 六角形の頂点座標リスト。
            clicked_hex (tuple): クリックされた六角形の中心座標。
            highlight_trigger (bool): ハイライトを有効にするかどうか。
        """
        if clicked_hex and highlight_trigger:
            # 距離を計算
            dx = x - clicked_hex[0]
            dy = y - clicked_hex[1]
            distance =  math.sqrt(dx**2 + dy**2)
            if distance <= self.radius * 5.5:  # ハイライト範囲内
                self.draw_highlighted_hexagon(points_list)
            else:
                self.draw_normal_hexagon(points_list)
        else:
            self.draw_normal_hexagon(points_list)

    # ハイライトヘックスを表示
    def draw_highlighted_hexagon(self, points_list):
        """
        ハイライトされた六角形を描画する。

        Args:
            points_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=points_list, color=(255, 0, 0, 50))
        arcade.draw_polygon_outline(point_list=points_list, color=arcade.color.BLACK, line_width=2)

    # 通常のヘックスを表示
    def draw_normal_hexagon(self, points_list):
        """
        通常の六角形を描画する。

        Args:
            points_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_outline(point_list=points_list, color=arcade.color.BLACK, line_width=2)

    def get_hex_center_position(self, mouse_x, mouse_y):
        """
        マウス座標から最も近い六角形の中心座標を取得する。

        Args:
            mouse_x (float): マウスのx座標。
            mouse_y (float): マウスのy座標。

        Returns:
            tuple: 最も近い六角形の中心座標。見つからない場合はNone。
        """
        for center in self.hex_centers:
            dx = mouse_x - center[0]
            dy = mouse_y - center[1]
            distance = math.sqrt(dx**2 + dy**2)
            if distance <= self.radius:
                return center
        return None
```

# ほかにもこんなのやってます
- [pythonのarcadeでhexmapを作成してみた！
](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)
- [Python&arcadeで簡単！レトロ風シューティングゲームを作ろう！🚀](https://qiita.com/nami_fumi/items/dc679b47f57fa5a18f68)
- [pythonのarcadeを使って、ユニット選択とボタンの表示をやってみた](https://qiita.com/nami_fumi/items/c819bd71dac6123f942d)
- [Pythonのarcadeでクリックでユニットをスムーズに移動](https://qiita.com/nami_fumi/items/446126377a818e2923c3)

# 参考文献
- [The Python Arcade Library](https://api.arcade.academy/en/latest/index.html)
    - arcade公式サイト
- [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)
    - Hexmapの計算方法や配置を理解するの参考になった良記事
- Gemini
    - コードをよりよくするときにいいです
- [perplexity](https://www.perplexity.ai/)
    - 初期コードを書かせる時にいいです
- [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)
    - Hexmapの計算方法や配置を理解するの参考になった良記事
- [pythonのarcadeでhexmapを作成してみた！](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)
- [Pythonのarcadeでクリックでユニットをスムーズに移動](https://qiita.com/nami_fumi/items/446126377a818e2923c3)