ゲーム開発で使用したテクニック～文字アニメーション～

個人でゲーム開発をしている中で、実装に苦労した部分が多かったので、テクニックとして紹介していこうと思います。

今回は「文字アニメーション」について書きますが、長くなります、、、
正直かなり力技なので、もう少しスマートにできるんじゃないかなぁと思ってます。

## 概要
プレイヤーが回復したときやダメージを受けたときなど、数字をアニメーションさせます。
## 文字画像の準備
最初はただのテキストを使用していたのですが、味気なかったので文字画像を作りました。
横に０～９が並んでいる画像です。
![text_number.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763806/001519ff-6ad4-4362-b48c-88545ed42b0d.png)
![text_number_green.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763806/58363b24-5fc6-4d14-9b99-f32990586041.png)
![text_number_red.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763806/d1e2276e-9812-4496-8329-d942b6ce5ce5.png)
## 必要な情報を整理する
```python
def animation_str(self, number, Player, color=None):
```
```python
p_x,p_y = Player.ICON_POS
num_list = list(str(number))
text_y_1 = p_y
text_x_1 = p_x
text_y_2 = p_y
text_x_2 = p_x + 15
step_max = 3.0
step_min = 2.0
step_1 = 0.0
step_2 = 0.0
p_img = Player.P_IMG_D_1
if Player.DIRECTION == "U":
    p_img = Player.P_IMG_U_1
elif Player.DIRECTION == "L":
    p_img = Player.P_IMG_L_1
elif Player.DIRECTION == "R":
    p_img = Player.P_IMG_R_1
```
関数の引数である「number」は表示したい数字、「color」は表示したい色です。
プレイヤーの位置から表示させたいので、プレイヤーの現在地「Player.ICON_POS」は必要です。
1文字ずつアニメーションさせたいので、「list(str(number))」で数字を1文字ずつバラバラにします。
「text_y_1」「text_x_1」「text_y_2」「text_x_2」が数字の表示位置です。番号が何文字目かを表しています。「text_x_2」だけ「+15」しているのは、2文字目の表示を右にずらす必要があるからです。
「step_max」は数字の上昇する最高点、「step_min」は数字の下降する最低点です。「step_min」が上昇に比べて小さいのは、プレイヤーの頭辺りで下降を止めたかったから調整しました。
「step_1」「step_2」は現在の移動量です。「step_max」まで加算され、「step_min」まで減算されます。
数字はプレイヤーのいる升から1升上まで表示されます。なので数字をアニメーションしている間は該当の升を再描画しないといけません。そのためにプレイヤーの現在の向きと対応した切り取り位置は必要です。
## 何文字まで許容するか
```python
# 表示する文字数が2桁の場合
if len(num_list) == 2:
    （中略）
# 表示する文字数が1桁の場合
elif len(num_list) == 1:
    （中略）
else:
	# 0文字以下、3文字以上は何もしない
	pass
```
動的に何文字までいけるようにと頑張ってはみたものの断念しました。
幸い今回は最大でも2桁の表示でよかったので、1桁と2桁の処理を作成しました。
## 1桁の場合
```python
# 文字を上昇させる
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_1 += 0.1
    if step_1 > step_max:
        step_1 = 0.0
        break
    else:
        # Y座標を減算する
        text_y_1 -= step_1
# 文字を下降させる
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_1 += 0.1
    if step_1 > step_min:
        step_1 = 0.0
        break
    else:
        # Y座標を加算する
        text_y_1 += step_1
# ディレイをかけて文字を消す
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_1 += 0.3
    if step_1 > 3.0:
        pg3.time.delay(300)
        self.clear_cell(p_x, p_y)
        self.clear_cell(p_x, p_y-1)
        self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
        pg3.display.update()
        step_1 = 0.0
        break
    else:
        pg3.time.delay(10)
```
「背景を初期化->プレイヤーを描画->数字を描画」の流れを繰り返します。
文字の上昇が終わったら1つ目の無限ループを抜けます。
文字の下降が終わったら2つ目の無限ループを抜けます。
最後はディレイをかけて文字を消し、3つ目の無限ループを抜けます。
「clear_cell」は升目を再描画しています。
「get_multi_transform」は数字を文字画像に変換して描画する関数（後述）です。
## 2桁の場合
```python
# 1文字目を上昇させる
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (rect_x, rect_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_1 += 0.1
    if step_1 > step_max:
        step_1 = 0.0
        break
    else:
        # Y座標を減算する
        text_y_1 -= step_1
# 1文字目を下降、2文字目を上昇させる
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    self.get_multi_transform(int(num_list[1]), text_x_2, text_y_2, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、それぞれ規定値に達したら終了させる
    step_1 += 0.1
    step_2 += 0.1
    if step_1 > step_min and step_2 > step_max:
        step_1 = 0.0
        step_2 = 0.0
        break
    if step_1 <= step_min:
        # 1文字目はY座標を加算する
        text_y_1 += step_1
    if step_2 <= step_max:
        # 2文字目はY座標を減算する
        text_y_2 -= step_2
# 1文字目はそのまま、2文字目を下降させる
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    self.get_multi_transform(int(num_list[1]), text_x_2, text_y_2, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_2 += 0.1
    if step_2 > step_min:
        step_2 = 0.0
        break
    else:
        # Y座標を加算する
        text_y_2 += step_2
# 1文字目はそのまま、2文字目もそのまま、ディレイをかけて文字を消す
while True:
    # プレイヤーの位置と一升上を初期化する
    self.clear_cell(p_x, p_y)
    self.clear_cell(p_x, p_y-1)
    # プレイヤーを描画する
    self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
    # 文字を描画する
    self.get_multi_transform(int(num_list[0]), text_x_1, text_y_1, color)
    self.get_multi_transform(int(num_list[1]), text_x_2, text_y_2, color)
    pg3.display.update()
    pg3.time.delay(5)
    # ステップを加算して、規定値に達したら終了させる
    step_2 += 0.3
    if step_2 > 3.0:
        pg3.time.delay(300)
        step_2 = 0.0
        self.clear_cell(p_x, p_y)
        self.clear_cell(p_x, p_y-1)
        self.screen.blit(Player.P_IMG, (p_x, p_y), p_img)
        pg3.display.update()
        break
    else:
        pg3.time.delay(10)
```
基本的に1桁の場合と流れは同じです。
1ループ目は1文字目が最高点に達するまで上昇させる、2ループ目は1文字目が最低点に達するまで下降させ、2文字目が最高点に達するまで上昇させる、3ループ目は2文字目が最低点に達するまで下降させる、4ループ目はディレイをかけて文字を消す、という流れです。
2ループ目は1文字目と2文字目を両方処理している為、上昇と下降で移動量が違う場合は注意が必要です。
## 数字から文字画像への変換
```python
def get_image_pos(self, number):
    """
    画像のトリミング位置を返す
    """
    trim_pos = (number*110*0.14, 0, 110*0.14, 124*0.14)
    return trim_pos

def get_multi_transform(self, number, x, y, color=None):
    """
    数字画像で描画する
    """
    str_number = str(number)
    img = self.IMG_NUMBER_1
    if color == "green":
        img = self.IMG_NUMBER_2
    elif color == "red":
        img = self.IMG_NUMBER_3
    for i in range(0, len(str_number), 1):
        target = str_number[i]
        trim_pos = self.get_image_pos(int(target))
        self.screen.blit(img, (x+i*15, y), trim_pos)
```
要領はプレイヤーの画像を切り抜いて表示したときと同じです。
数字によって切り抜く際に変わるのはX座標だけです。
「number\*110\*0.14」でX座標を算出しています。
引数の「color」で使用する画像を決定しています。
文字アニメーションで使う場合、numberは1文字ですが、他で固定表示したい場合があったので、何文字でもいけるようにしてあります。
「(x+i\*15, y)」の部分で、1文字ずつ表示する座標を右にずらしています。

「文字アニメーション」に関しては以上となります。

## リリース済みのゲームはこちら
・PC向け無料ゲーム
『LAbyrinth』（2Dの迷路探索ゲーム）
●Freem!
https://www.freem.ne.jp/win/game/33791
●Unityroom
https://unityroom.com/games/2025-labyrinth-isukaka