前回のあらすじ
前回はバトルシステムの基礎を作ってみた。
今回は「ボタン入力でアニメーションを再生するだけ」のプログラムなので前回とは全く関係ない。
前置き
今回はFF6の様なドット絵アニメーションに挑戦してみた。
「ような」というかほとんど「まんま」だけれど
動画
ソースコード
ソースコードはGoogleDriveに置いておきます。そこからダウンロードして遊んでみてください。
動作環境や本連載のテーマについてはこちらの記事にまとめてあります。
前回(ver2.0.0)からの変更点
今回は、キャラクターを動かすテスト用のプログラムなので前回とは全く関係ない。
最低限必要な知識
今回のプログラムに必要なのはif文やクラス指向等のPythonの基礎知識とtkinterの知識。tkinterの知識と言っても「GUIを作るプログラム」という認識があればOK
大まかな流れ
今回のプログラムの流れは単純明快。
画像をロードするクラスとそれを組み合わせてパラパラ漫画のように動かすプログラムの2つで構成されている
もっと詳しく書くと
- imageディレクトリ
- img.py:画像のパスを作成し画像をロードさせるプログラム
- 画像ファイル
- animation.py
- 〇〇_animation関数:画像を組み合わせてアニメを作る関数
- delete_normal_img関数:デフォルトの立ち絵を消す関数
- generate_normal_img関数:デフォルトの立ち絵を表示させる関数
- character_view関数:GUIの作成と〇〇_animation関数で作成したアニメを再生させるプログラム
という感じ。
アニメーションの流れはこんな感じ
各部分の解説
img.py:画像のロード
from tkinter import PhotoImage
class ImageLoader:
def __init__(self):
self.dotgirl_normal_img = None
self.dotgirl_dead_img = None
self.dotgirl_action_img = None
self.dotgirl_attack_img = None
self.dotgirl_ticktick_1_img = None
self.dotgirl_ticktick_2_img = None
self.dotgirl_tonaeru_1_img = None
self.dotgirl_tonaeru_2_img = None
self.back = None
self.dotgirl_omitooshi_1_img = None
self.dotgirl_omitooshi_2_img = None
self.dotgirl_omitooshi_3_img = None
self.dotgirl_omitooshi_4_img = None
self.dotgirl_omitooshi_5_img = None
self.dotgirl_omitooshi_6_img = None
self.dotgirl_omitooshi_last_stage_img = None
self.dotgirl_trans_1_img = None
self.dotgirl_trans_2_img = None
self.dotgirl_trans_3_img = None
def load_images(self):
# 各画像のファイルパスの設定
DOTGIRL_NORMAL_PATH = "./image/dotgirl_normal.png"
DOTGIRL_DEAD_PATH = "./image/dotgirl_dead.png"
DOTGIRL_ACTION_PATH = "./image/dotgirl_action.png"
DOTGIRL_ATTACK_PATH = "./image/dotgirl_attack.png"
DOTGIRL_TICKTICK_1_PATH = "./image/dotgirl_ticktick_1.png"
DOTGIRL_TICKTICK_2_PATH = "./image/dotgirl_ticktick_2.png"
DOTGIRL_TONAERU_1_PATH = "./image/dotgirl_tonaeru_1.png"
DOTGIRL_TONAERU_2_PATH = "./image/dotgirl_tonaeru_2.png"
BACK_PATH = "./image/back.png"
DOTGIRL_OMITOOSHI_1_PATH = "./image/omitooshi_1.png"
DOTGIRL_OMITOOSHI_2_PATH = "./image/omitooshi_2.png"
DOTGIRL_OMITOOSHI_3_PATH = "./image/omitooshi_3.png"
DOTGIRL_OMITOOSHI_4_PATH = "./image/omitooshi_4.png"
DOTGIRL_OMITOOSHI_5_PATH = "./image/omitooshi_5.png"
DOTGIRL_OMITOOSHI_LAST_STAGE_PATH = "./image/omitooshi_final.png"
DOTGIRL_TRANS_1_PATH = "./image/trans_1.png"
DOTGIRL_TRANS_2_PATH = "./image/trans_2.png"
DOTGIRL_TRANS_3_PATH = "./image/trans_3.png"
# インスタンスとパスの紐づけ
self.dotgirl_normal_img = PhotoImage(file=DOTGIRL_NORMAL_PATH)
self.dotgirl_dead_img = PhotoImage(file=DOTGIRL_DEAD_PATH)
self.dotgirl_action_img = PhotoImage(file=DOTGIRL_ACTION_PATH)
self.dotgirl_attack_img = PhotoImage(file=DOTGIRL_ATTACK_PATH)
self.dotgirl_ticktick_1_img = PhotoImage(file=DOTGIRL_TICKTICK_1_PATH)
self.dotgirl_ticktick_2_img = PhotoImage(file=DOTGIRL_TICKTICK_2_PATH)
self.dotgirl_tonaeru_1_img = PhotoImage(file=DOTGIRL_TONAERU_1_PATH)
self.dotgirl_tonaeru_2_img = PhotoImage(file=DOTGIRL_TONAERU_2_PATH)
self.back = PhotoImage(file=BACK_PATH)
self.dotgirl_omitooshi_1_img = PhotoImage(file=DOTGIRL_OMITOOSHI_1_PATH)
self.dotgirl_omitooshi_2_img = PhotoImage(file=DOTGIRL_OMITOOSHI_2_PATH)
self.dotgirl_omitooshi_3_img = PhotoImage(file=DOTGIRL_OMITOOSHI_3_PATH)
self.dotgirl_omitooshi_4_img = PhotoImage(file=DOTGIRL_OMITOOSHI_4_PATH)
self.dotgirl_omitooshi_5_img = PhotoImage(file=DOTGIRL_OMITOOSHI_5_PATH)
self.dotgirl_omitooshi_last_stage_img = (
PhotoImage(file=DOTGIRL_OMITOOSHI_LAST_STAGE_PATH))
self.dotgirl_trans_1_img = PhotoImage(file=DOTGIRL_TRANS_1_PATH)
self.dotgirl_trans_2_img = PhotoImage(file=DOTGIRL_TRANS_2_PATH)
self.dotgirl_trans_3_img = PhotoImage(file=DOTGIRL_TRANS_3_PATH)
image_loader = ImageLoader()
なんだか読む気が失せそうな文字の羅列だけれど、見た目の割にやってる内容は凄くシンプル
- ImageLoaderクラスのインスタンスとして各画像の変数を設定
- 画像のファイルパスをそれぞれに設定
- インスタンスとパスを紐づける
つまり、画像のあだ名と住所を紐づける作業を行っているというわけ
インスタンスをNoneとしているのはimport時にエラーが起きるから。エラーの原因は未だに良く分かっていないけれど「クラスをimportした時にインスタンスに画像が直接結びついているとエラーが起きる?」っぽい感じだったので、初めはNoneとして何も設定せず後からload_image関数を呼び出すことであとづけして画像を呼び出す流れになった。
〇〇_animation:アニメーションを作り出す
コードの解説をする前にアニメーションの作り方について少し考えてみよう。ぶっちゃけ当たり前というか言わなくても分かるものなので読まなくてもいい
アニメーションを作るには2つの重要なポイントがある。それは「画像の差分」と「画像の順番」だ。
画像の差分とはそれぞれの画像の差のこと。例えば「歩くアニメーション」を作りたいとしたら
- 「右足で踏み出し、左足で踏み切る」
- 「右足だけが接地」
- 「左足足で踏み出し、右足で踏み切る」
- 「左足だけが接地」
という4つの差分が最低限必要だ。
この画像の差分をどんどん細かくしていけば行くほど滑らかなアニメーションを作ることが出来る。
では、今度はその画像の順番について考えてみよう。
もし上の画像の順番がこの様な物理法則を無視した順番だったらキャラクターの動きはどうなってしまうだろうか
- 「右足で踏み出し、左足で踏み切る」
- 「左足足で踏み出し、右足で踏み切る」
- 「右足だけが接地」
- 「左足だけが接地」
※gifが再生されない場合はクリック
もちろんわけのわからない動きなってしまう。
つまりアニメーションを作るときにいちばん大切なのは「正しい順番」である。どれだけ細かいアニメーションを作ったとしてもその順番がぐちゃぐちゃならそれは全く無意味なものになってしまうからだ。
・
さて、順番の大切さを認識できたところで本題のコードの解説に移ろう。このプログラムには全部で8つのアニメーションが設定されているが、基本的な「正しい順番プログラム」は変わりないので、ここではtrans_animationを例にとって解説してみる。
まずはtrans_animationで関われている3枚の画像を紹介しよう。
li.dotgirl_trans_1_img
li.dotgirl_trans_2_img
li.dotgirl_trans_3_img
そしてこれらを組み合わせるとこうなる
この3枚の画像を「正しい順番」でつなげることによって一つの「一つの動きのループ」が構成されている。
なんとなくtras_animationのイメージが湧いたところで、その「正しい順番」をどうやって作っているのか実際のコードで確認してみよう。
def trans_animation(canvas, animation_count=0, loop_count=0):
if loop_count <= 9:
if animation_count == 0:
canvas.create_image(character_canvas_x,
character_canvas_y,
image=li.dotgirl_trans_1_img,
anchor=character_anchor,
tag="image_1")
root.after(500, lambda: (
canvas.delete("image_1"),
trans_animation(canvas, animation_count + 1, loop_count + 1)
))
elif animation_count == 1:
canvas.create_image(character_canvas_x,
character_canvas_y,
image=li.dotgirl_trans_2_img,
anchor=character_anchor,
tag="image_2")
root.after(500, lambda: (
canvas.delete("image_2"),
trans_animation(canvas, animation_count + 1, loop_count + 1)
))
elif animation_count == 2:
canvas.create_image(character_canvas_x,
character_canvas_y,
image=li.dotgirl_trans_3_img,
anchor=character_anchor,
tag="image_3")
root.after(500, lambda: (
canvas.delete("image_3"),
trans_animation(canvas, animation_count == 0, loop_count + 1)
))
elif loop_count == 10:
canvas.create_image(character_canvas_x,
character_canvas_y,
image=li.dotgirl_trans_2_img,
anchor=character_anchor,
tag="last_image")
root.after(500, lambda: (
canvas.delete("last_image"), generate_normal_img(canvas)))
このプログラムを簡単に説明すると、
- def trans_animation(canvas, animation_count=0, loop_count=0):
- trans_animationに引数「animation_count」と「loop_count」を渡す。
- 0〜9ループ目
- if loop_count <= x:
- 現在が何ループ目なのか確認する
- if animation_count == x:
- animation_countの数値で処理を分岐
- canvas.create_image~
- canvasに画像を生成。canvasとは画像の下敷き(青色のヤツ)のこと、これがないとtkinterは画像を生成できない
- root.after()
- afterメソッドを使って上の処理が行われた500ミリ秒後に画像を消去&関数を再実行
- 10ループ目(最後)
- canvas.create_image~
- 画像を生成
- root.after()
- afterメソッドを使って上の処理が行われた500ミリ秒後に画像を消去&基本の画像を生成
※「基本の画像」というのは最初からウィンドウに配置してあるデフォルト状態の画像の事。この画像が残されていると画像が二重に表示されてしまうので、character_view関数のbutton_〇〇_animation内で〇〇_animationを呼び出す前に消去している。
という感じだ。
順番はanimation_count=画像の順番・loop_count=アニメーション全体のループ回数という2つの引数の数値で判断するようにしている。
trans_animationは全部で3つの画像から構成されているので引数で1回目、2回目、3回目というように処理を分岐させる事ができれば順番を作り出せるというわけだ。
#1回目
if animation_count == 0:
trans_animation(canvas, animation_count + 1, loop_count + 1)
#2回目
elif animation_count == 1:
trans_animation(canvas, animation_count + 1, loop_count + 1)
#3回目(最後)
elif animation_count == 2:
trans_animation(canvas, animation_count == 0, loop_count + 1)
具体的に言えば、一つの画像を生成する処理(canvas.image_create)の最後にanimation_countの数値を加算する。animation_countは画像の処理分岐と紐づいているので画像の順番も自ずと繰り上がるという仕組み。ついでにloop_countの数値も加算させているのでtrans_animation全体のループ回数も加算される
でも、これではどんどんanimation_countの数値が増えてしまうので、最後の画像(trans_animationの場合、3枚目の)生成処理ではanimation_countを0に戻し、また0から画像生成が続くようにして3枚ひとかたまりのループが続くようにしてある。
まとめると、
- 画像の順番と全体のループ数を記憶する引数を作成し、関数に引き渡す
- 渡された引数を元にif文でどの画像を生成するのか判断しそれを実行する。
- 処理の最後に画像がまだ後ろに続くなら引数を加算し、続かないのなら(最後なら)引数を0に戻す。
- ついでにループの引数も加算させて、1に戻る
という処理が行われている。
・
canvas.create_imageやafterメソッドについては公式のドキュメントなどは公式リファレンスや他ブログ記事を参照したほうが正確だと思うので、この記事では軽めに紹介する。
canvas.create:画像を生成する
canvas.create_image(X座標,Y座標,image=表示したい画像,anchor=画像を配置する基準点,tag="画像のあだ名")
今回のプログラムでは座標部分にグローバル関数を使っている。ついでにimport文についても少し説明
import tkinter as tk
from image.img import image_loader as li
character_canvas_x = 100
character_canvas_front_action_x = 50
character_canvas_back_action_x = 150
character_canvas_y = 100
character_anchor = tk.CENTER
importの部分は見たまんま。tkinterと先のimg.pyの中のimage_loader関数をliというあだ名で呼び出している。
回数の説明の中で「画像に変な「li.」がくっついてる」と思われた方がいるかもしれないが、そのli.はこのimage_loader関数のあだ名である。つまり、li.dotgirl_trans_1_imgは「image_loader関数でdotgirl_trans_1_imgを呼び出しているよ」ってこと
グローバル関数はキャラクターのcanvas内の座標を指定するもの。先程もちらりと挟んだ通りtkinterで画像を表示するにはcanvasを下敷きにしないといけないので画像を下敷きのどこに置くか指定しなければならない。
x軸が3つあるのは特定のアニメーションではキャラクターが前後するため(貼ってある動画見てもらうと分かると思う)。
tk.CENTERというのはcanvas内で画像を置く際の基準点のこと。CENTERはcanvasの真ん中が基準になる。
.afterメソッド:処理の後に実行したい処理を指定する。tkinterではtime.sleepが使えないので処理を遅延させたい時にも役立つ
処理.after(ミリ秒, lambda: 実行したい処理)
また、今回のプログラムでは画像を上書きしているわけでなく、続く画像を生成してキャラクターのアニメーションを作っているので前の画像を一々消しておかない(canvas.delete)とキャラクターが2重に表示されてしまう。
まとめと課題と次回予告と
今回はゲームにアニメーションを取り入れてみた。キャラクターを動かすことが出来るようになったことでよりゲームらしくなったと思う。
次回はよりゲームに近づけるために効果音をつけるかも?しれない。
次回