Pythonのarcadeでスクロールする六角形マップを実装！

# **はじめに**

この記事では、PythonのArcadeライブラリを使用して、スクロール可能な六角形マップを作成する方法を説明します。ゲーム開発などで、六角形グリッドマップを扱う際に役立つでしょう。

## **コード解説**

### scrolling.py

このファイルでは、ウィンドウの生成とスクロール機能の実装を行います。

```python
import arcade
from components.hexmap import Hexmap

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Hex Map with Scrolling"
GRID_SIZE = 15
HEX_RADIUS = 30
SCROLL_SPEED = 25  # スクロール速度


class MyGame(arcade.Window):
    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        arcade.set_background_color(arcade.color.AMAZON)
        self.hexmap = Hexmap(HEX_RADIUS, GRID_SIZE)
        self.view_x = 0
        self.view_y = 0
        self.last_key_press_time = 0
        self.mouse_x = 0
        self.mouse_y = 0


    def on_draw(self):
        arcade.start_render()

        # ビューポートを設定 (スクロール)
        arcade.set_viewport(self.view_x, SCREEN_WIDTH + self.view_x
                            , self.view_y, SCREEN_HEIGHT + self.view_y)

        self.hexmap.draw_hexagon(on_mouse_coordinates=(self.mouse_x, self.mouse_y))

    def on_key_press(self, key, modifiers):
        dx = 0
        dy = 0

        if key == arcade.key.LEFT:
            dx = -SCROLL_SPEED
        elif key == arcade.key.RIGHT:
            dx = SCROLL_SPEED
        elif key == arcade.key.UP:
            dy = SCROLL_SPEED
        elif key == arcade.key.DOWN:
            dy = -SCROLL_SPEED

        new_view_x = self.view_x + dx
        new_view_y = self.view_y + dy

        # スクロール範囲の制限
        self.view_x = min(400, max(-400, new_view_x)) # -400 ~ 400
        self.view_y = min(400, max(-300, new_view_y)) # -300 ~ 400

        print(f"view_x: {self.view_x}, view_y: {self.view_y}")
        
        if key == arcade.key.Q:
            arcade.close_window()

    def on_mouse_motion(self, x, y, dx, dy):
        """
        マウスの移動時座標をスクロールに合わせて補正
        
        Args:
            x: マウスのx座標
            y: マウスのy座標
        """
        self.mouse_x = x + self.view_x # 補正
        self.mouse_y = y + self.view_y


def main():
    game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    arcade.run()


if __name__ == "__main__":
    main()

```

`SCROLL_SPEED`変数でスクロールの速度を調整できます。`on_key_press`関数で、矢印キーの入力に応じて`view_x`、`view_y`変数を更新し、スクロールを実現しています。スクロール範囲は-400~400、-300~400に制限されています。ここは適宜調節してください。

```python
    def on_key_press(self, key, modifiers):
        # ... (コード省略)

        # スクロール範囲の制限
        self.view_x = min(400, max(-400, new_view_x)) # -400 ~ 400
        self.view_y = min(400, max(-300, new_view_y)) # -300 ~ 400

        print(f"view_x: {self.view_x}, view_y: {self.view_y}")

        if key == arcade.key.Q:
            arcade.close_window()
```
`on_mouse_motion`関数では、マウスの移動時座標をスクロールに合わせて補正し取得します。

```python
    def on_mouse_motion(self, x, y, dx, dy):
        """
        マウスの移動時座標をスクロールに合わせて補正
        
        Args:
            x: マウスのx座標
            y: マウスのy座標
        """
        self.mouse_x = x + self.view_x # 補正
        self.mouse_y = y + self.view_y
```

### components/hexmap.py

このファイルでは、六角形グリッドの生成と描画、ハイライト表示、マウス座標の取得、距離計算を行います。

```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:
            vertices: 六角形の頂点座標のリスト。
        """
        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_coordinates=None
                    , attack_coordinates = None
                    , on_mouse_coordinates=None):
        """
        六角形グリッドマップを描画する。

        Args:
            highlight_coordinates (tuple): ハイライトを表示するために必要なX,Y座標 例）クリックした位置から3hex分の周囲をハイライト表示したい場合は、クリックした位置のX,Y座標
            on_mouse_coordinates (tuple):  マウスのX,Y座標
        """
        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))
                hex_vertices_list = self.hexagon_vertices(x, y)

                self.draw_single_hexagon(x, y, hex_vertices_list, highlight_coordinates, attack_coordinates, on_mouse_coordinates)

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

        Args:
            x (float): 六角形の中心x座標。
            y (float): 六角形の中心y座標。
            hex_vertices_list (list): 六角形の頂点座標リスト。
            clicked_hex (tuple): クリックされた六角形の中心座標。
            highlight_trigger (bool): ハイライトを有効にするかどうか。
            on_mouse_point(): マウスの位置座標
        """
        
        if highlight_coordinates:
            distance = self.calcutate_distance(x, y, highlight_coordinates)
            if distance <= self.radius * 5.5:  # ハイライト範囲内
                self.draw_highlighted_hexagon(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)
        
        if attack_coordinates:
            distance = self.calcutate_distance(x, y, attack_coordinates)
            if distance <= self.radius*2.0:
                self.attack_highlighted_draw(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)
        
        if on_mouse_coordinates:
            distance = self.calcutate_distance(x, y, on_mouse_coordinates)
            if distance <= self.radius:
                self.draw_hex_on_mouse(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)

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

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(0, 0, 255, 50))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)
    
    def attack_highlighted_draw(self, hex_vertices_list):
        """
        攻撃範囲のハイライトされた六角形を描画する。

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(255, 0, 0, 50))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)
    
    # マウスがどこのhexにいるかを表示
    def draw_hex_on_mouse(self, hex_vertices_list):
        """
        hexmap上のhexにマウスが置かれた時に白色に描画する
        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(255, 255, 255, 100))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)

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

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_outline(point_list=hex_vertices_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:
            distance = self.calcutate_distance(mouse_x, mouse_y, center)
            if distance <= self.radius:
                return center
        return None
    
    def calcutate_distance(self, hex_center_x, hex_center_y, point):
        """
        現在地の座標と、ヘックスの中心座標との距離を算出する
        
        Args:
            hex_center_x (float): hexmapの中心x座標
            hex_center_y (float): hexmapの中心y座標
            hex_center_point(tuple): tuple形式のx, y座標

        Returns:
            tuple: 最も近い六角形の中心座標。見つからない場合はNone。
        """
        # 距離の計算ロジックを入れる
        dx = hex_center_x - point[0]
        dy = hex_center_y - point[1]
        distance =  math.sqrt(dx**2 + dy**2)
        return distance
```


**実行方法**

1.  必要なライブラリをインストールします。実装するときは仮想環境を作成することをお勧めします。
    ```bash
    pip install arcade
    ```
2.  `scrolling.py`を実行します。
    ```bash
    python scrolling.py
    ```

**まとめ**

この記事では、PythonとArcadeライブラリを用いてスクロール可能な六角形マップを作成する方法を紹介しました。ゲーム開発などに応用できるでしょう。


**今後の展望**

* スクロール機能の改善
* より高度なマップ生成アルゴリズムの実装

# ほかにもこんなのやってます
- [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でHexmap上のユニットを移動させる](https://qiita.com/nami_fumi/items/296adbdce9ef94a30852)

## シューティングを作成してみました！ぜひ遊んでみてください^^
- [コード Github](https://github.com/programingbigger/SimpleShooter)

# 参考文献
- [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/)
    - 初期コードを書かせる時にいいです
- [pythonのarcadeでhexmapを作成してみた！](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)