Pygameでファイアーエムブレムを部分的に再現する(カーソル編)

## はじめに

皆さんこんにちは。初投稿でガチガチに緊張しております、**赤ジャージ**です。
私は2年ほど前から趣味でpythonをいじり始めたペーペープログラマです。今まで数多くのクソゲーゴミゲーを作ってきました。

## ファイアーエムブレムとは？

さて、皆さんは、**ファイアーエムブレム**というゲームをプレイしたことがありますか？簡単に説明すると、キャラクターがマップの中で戦うターン制ストラテジーRPGです。シリーズもので、1990年から今に至るまで、数多くのタイトルが発売されました。よくファイ**ヤ**ーエ**ン**ブレムと間違えやすいので注意！

今回の記事はfe(ファイアーエムブレム、以下fe)に影響を受けた私が、どうにかそれに類したゲームをpythonで作ろうとした奮闘記だと思って下さい。(o_ _)o


# やりたいこと
やりたいことは、カーソルの再現です。
このゲーム、マップの中でカーソルを動かしてキャラを選択し、そのキャラをカーソルで移動させて操作を行うんですが、このカーソルの動きがシリーズを通して操作性抜群で、ストレスなく広いマップを縦横無尽に駆け巡ることができます。

普通のカーソル移動は、1回ボタンを押すと決められた分だけ動くか、長押しすると速く動くかの二択です。feのカーソルは、それの合わせ技で、広いマップを速く移動でき、かつ細かな1マス1マスの移動もできる優れものです。

なぜこのようなことができるか私なりに分析したところ、どうやら入力してから300ミリ秒(0.3秒)ほど経過した際、まだボタンが押されているかどうか調べ、押されていれば速く移動するというロジックで動いているらしいのです。
今回は、それを可能な限り再現してみました。(勿論chatGPTにも手伝ってもらいました)


## 環境
python3.7.2(古いのであまりオススメしません。)
pygame2.3.0

# ソースコード
```python:fecursor.py
import pygame
import sys

# Pygameの初期化
pygame.init()

# 画面の設定
screen_width = 900
screen_height = 540
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("fe的カーソル術")

cursor_img = pygame.image.load("cursor1.png")

cursor_width = 60
cursor_height = 60
cursor_x = 0
cursor_y = 0

# キー入力のトラッキング
key_pressed = {pygame.K_LEFT: False, pygame.K_RIGHT: False, pygame.K_UP: False, pygame.K_DOWN: False}

# カーソルの移動速度
normal_cursor_speed = 60  # 通常速度（60ピクセル/秒）

# 長押しの時間閾値（ミリ秒）
long_press_threshold = 300

# キーが押された時刻
key_press_time = 0

# ゲームループ
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # 画面のクリア
    screen.fill((0, 0, 0))

    # キー入力の処理
    for key in key_pressed:
        if not key_pressed[key] and pygame.key.get_pressed()[key]:
            key_pressed[key] = True
            cursor_speed = normal_cursor_speed  # 通常速度で進む
            key_press_time = pygame.time.get_ticks()  # キーが押された時刻を記録

            if key == pygame.K_LEFT:
                cursor_x -= cursor_speed
            elif key == pygame.K_RIGHT:
                cursor_x += cursor_speed
            elif key == pygame.K_UP:
                cursor_y -= cursor_speed
            elif key == pygame.K_DOWN:
                cursor_y += cursor_speed

        elif key_pressed[key] and not pygame.key.get_pressed()[key]:
            key_pressed[key] = False

    # カーソルの位置を60ピクセルの倍数に制約
    cursor_x = cursor_x - (cursor_x % 60)
    cursor_y = cursor_y - (cursor_y % 60)

    # 長押し中は0.1秒ごとに1マス移動
    if any(key_pressed.values()):
        current_time = pygame.time.get_ticks()
        elapsed_time = current_time - key_press_time

        if elapsed_time >= long_press_threshold and elapsed_time % 100 == 0:
            if key_pressed[pygame.K_LEFT]:
                cursor_x -= 60
            elif key_pressed[pygame.K_RIGHT]:
                cursor_x += 60
            elif key_pressed[pygame.K_UP]:
                cursor_y -= 60
            elif key_pressed[pygame.K_DOWN]:
                cursor_y += 60
    
    cursor_x = max(0, min(cursor_x, screen_width - cursor_width))
    cursor_y = max(0, min(cursor_y, screen_height - cursor_height))
    
    # カーソルの描画
    screen.blit(cursor_img, (cursor_x, cursor_y))

    # 画面の更新
    pygame.display.flip()


# Pygameの終了
pygame.quit()
sys.exit()
```
はい、少々長ったらしいですが、まあ必要最低限だと思います。

ちなみにコードに出てくるcursor.pngというのは
![cursor1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3367669/f5050155-19e0-ff42-d338-45c973a05e09.png)
これです。
見づらくて5分クオリティですが一応マスとしての体形を保っているでしょう。

このコードを実行すると、
![スクリーンショット (22).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3367669/85eb49fd-a022-a98a-a490-d9ed5178d3c8.png)

こんな感じの画面が出てくると思います。

# やってること
```python
cursor_width = 60
cursor_height = 60
cursor_x = 0
cursor_y = 0

# カーソルの移動速度（通常速度）
normal_cursor_speed = 60  # 通常速度（60ピクセル/秒）

# カーソルの位置を60ピクセルの倍数に制約
cursor_x = cursor_x - (cursor_x % 60)
cursor_y = cursor_y - (cursor_y % 60)

cursor_x = max(0, min(cursor_x, screen_width - cursor_width))
cursor_y = max(0, min(cursor_y, screen_height - cursor_height))
```
まず、カーソルの大きさは60x60ピクセルになっております。そして、カーソルの移動速度は60、つまりカーソルの大きさを1マスとすると、1マス以上の範囲でしか移動できないように制限し、いつ、どんな時でもカーソルはどこかのマスに当てはまっている、ということです。
説明がわかりにくいかもしれませんが、つまりはゲームにおいて1つのカーソルが2つ以上のマスを選択することがないようにしているのです。

```python
# キー入力のトラッキング
key_pressed = {pygame.K_LEFT: False, pygame.K_RIGHT: False, pygame.K_UP: False, pygame.K_DOWN: False}

# 長押しの時間閾値（ミリ秒）
long_press_threshold = 300

# キーが押された時刻
key_press_time = 0

for key in key_pressed:
        if not key_pressed[key] and pygame.key.get_pressed()[key]:
            key_pressed[key] = True
            cursor_speed = initial_cursor_speed  # 初速度で進む
            key_press_time = pygame.time.get_ticks()  # キーが押された時刻を記録

            if key == pygame.K_LEFT:
                cursor_x -= cursor_speed
            elif key == pygame.K_RIGHT:
                cursor_x += cursor_speed
            elif key == pygame.K_UP:
                cursor_y -= cursor_speed
            elif key == pygame.K_DOWN:
                cursor_y += cursor_speed

        elif key_pressed[key] and not pygame.key.get_pressed()[key]:
            key_pressed[key] = False

    # 長押し中は0.1秒ごとに1マス移動
    if any(key_pressed.values()):
        current_time = pygame.time.get_ticks()
        elapsed_time = current_time - key_press_time

        if elapsed_time >= long_press_threshold and elapsed_time % 100 == 0:
            if key_pressed[pygame.K_LEFT]:
                cursor_x -= 60
            elif key_pressed[pygame.K_RIGHT]:
                cursor_x += 60
            elif key_pressed[pygame.K_UP]:
                cursor_y -= 60
            elif key_pressed[pygame.K_DOWN]:
                cursor_y += 60
```
こちらは少しややこしいです。順を追って説明します。
1.まず、私が方向キーを押します。

2.すると、`key_pressed`のいずれかのフラグ(押した方向による)が`True`になります。そして、`key_press_time = pygame.time.get_ticks()`が、押された時刻を算出します。

3.フラグがTrueになったことで、その方向に初速度、つまり60ピクセル、さらに言い換えれば1マス進みます。

4.まず、`current_time = pygame.time.get_ticks()`が、ループ内の現在時刻を`current_time`に代入します。
そして`elapsed_time = current_time - key_press_time`が現在時刻とボタンの押し始めの時刻の差を出し、それが300ミリ秒より大きければ0.1秒ずつ1マス移動するというものです。


## おわりに
少し説明が難しくなってしまいましたが、やっていることは非常に単純です。しかも、このコードは別にfeもどきでなくともどのゲームでも使える、つまり**汎用性が効きます**。
著作権なんてうるさいことは言わないので、ぜひあなたのゲームでも使ってみて下さいね！