pythonのarcadeを使って、ヘックスマップ&ボタン&移動範囲の表示をやってみた

# コードの実行結果
初期画面
![スクリーンショット 2024-11-03 12.44.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/5ea8c23e-98ac-2868-0b0e-188a20dc2969.png)

ヘックスを押して移動を押すと、移動範囲3ヘックス分が表示される
![スクリーンショット 2024-11-03 12.45.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/ed7466a7-a6de-2e24-e242-ba36dbdded60.png)

停止ボタンまたは他のどこかの範囲を選択すると移動範囲が消え、最初の画面に戻る


# コードの内容
先日投稿した記事（[pythonのarcadeでhexmapを作成してみた！](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)）に以下の機能を追加しました。

## 追加した内容
ヘックスを押して⇨移動ボタンを押す⇨範囲が表示
停止または違う箇所を押すと、範囲が消える

# コード
```python
import arcade
import math
import arcade.gui

WIDTH = 800
HEIGHT = 600
RADIUS = 30  # 半径
GRID_SIZE = 10  # hexの個数
TITLE = "test_hexmap"

# ボタンの配色の設定
default_style = {
    "font_name": ("calibri", "Hiragino Sans")
    , "font_size": 15
    , "font_color": arcade.color.WHITE
    , "border_width": 2
    , "border_color": None
    , "bg_color": (21, 19, 21),

    # used if button is pressed
    "bg_color_pressed": arcade.color.WHITE,
    "border_color_pressed": arcade.color.WHITE,  # also used when hovered
    "font_color_pressed": arcade.color.BLACK,
}

"""
実現したこと
- ヘックスを押して⇨移動を押す⇨範囲が表示
- 停止または違う箇所を押すと、範囲が消える
"""

class UnitControlButtons:
    def __init__(self, window, hex_map):
        self.manager = arcade.gui.UIManager(window)
        self.manager.enable()
        self.hex_map = hex_map
        self.window = window

        # 移動ボタンの作成
        move_button = arcade.gui.UIFlatButton(text="移動", style=default_style)
        move_button.on_click = self.on_move_click

        # 停止ボタンの作成
        stop_button = arcade.gui.UIFlatButton(text="停止", style=default_style)
        stop_button.on_click = self.on_stop_click

        # ボタンの間隔を設定
        v_box = arcade.gui.UIBoxLayout()
        v_box.add(move_button.with_space_around(bottom=10))
        v_box.add(stop_button)

        # Add the layout to the UI manager
        self.manager.add(arcade.gui.UIAnchorWidget(anchor_x="left", anchor_y="center_y", child=v_box))

    def on_move_click(self, event):
        print("ユニット移動")
        if self.window.clicked_hex:
            self.window.highlight = True
            self.window.on_draw()

    def on_stop_click(self, event):
        print("ユニット停止")
        self.window.highlight = False
        self.window.on_draw()

class Hexmap:
    def __init__(self, radius, grid_size):
        self.radius = radius
        self.grid_size = grid_size
        self.hex_centers = []

    # 頂点の計算
    def hexagon_vertices(self, x, y):
        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):
        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):
        if clicked_hex and highlight_trigger == True:
            # 距離を計算
            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):
        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):
        arcade.draw_polygon_outline(point_list=points_list, color=arcade.color.BLACK, line_width=2)

    def get_clicked_hex(self, mouse_x, mouse_y):
        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

class MyHexMap(arcade.Window):

    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        self.hex_map = Hexmap(radius=RADIUS, grid_size=GRID_SIZE)
        self.clicked_hex = None
        self.highlight = False
        self.button_control = UnitControlButtons(self, self.hex_map)

    def setup(self):
        arcade.set_background_color(arcade.color.WHITE)

    def on_draw(self):
        arcade.start_render()
        self.button_control.manager.draw()
        self.hex_map.draw_hexagon(highlight_trigger=self.highlight, clicked_hex=self.clicked_hex)

    def on_mouse_press(self, x, y, button, modifiers):
        self.clicked_hex = self.hex_map.get_clicked_hex(mouse_x=x,mouse_y=y)
        if self.clicked_hex:
            print(f"Clicked hex point {self.clicked_hex}")

    def on_key_press(self, symbol, modifiers):
        if symbol == arcade.key.Q:
            arcade.close_window()


def main():
    myhexmap = MyHexMap(width=WIDTH, height=HEIGHT, title=TITLE)
    myhexmap.setup()
    arcade.run()


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

# 他にもこんなのやってます!

- [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)

# 参考文献
-  [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)
    -  計算方法や配置を理解するの参考になった良記事

-  [The Python Arcade Library](https://api.arcade.academy/en/latest/index.html)
    - arcade公式サイト
- [Drawing Text](https://api.arcade.academy/en/2.6.17/examples/drawing_text.html)
    - これらの記事は、様々なプログラミング言語やライブラリを用いてテキストを描画する方法を解説
    - ここには、日本語の設定方法は載っていないので注意
    - いい感じの日本語にしたい場合は、「Hiragino Sans」を使う
- Gemini