Python プログラムで動かすフェアリーチェスアプリ開発、連載第 9 回です。
はマウス操作で駒を動かせるようにしました。しかし、その駒の移動がカクカクしているので、
今回は駒の移動にアニメーションを追加してスムーズに動いているように表示させます。
完成イメージ
もともといたマスからクリックしたマスまでスムーズに移動しています。
GLUT でアニメーションさせる
GLUT でアニメーションを実現するには、一定時間後に指定した関数を実行するglutTimerFunc()
を使う方法と、
プログラムが待ち時間にあるときに指定した関数を実行するglutIdleFunc()
を使う方法があります。
今回は後者を使っています。
マウスが移動先のマスをクリックしたときにアニメーションが始まり、
その時点からの経過時間によって駒の位置が変化します。
アニメーション中であることを示す変数self.moving
と、経過時間を表す変数self.time
を追加します。
main.py1...2from time import sleep3...45class Game:6def __init__(self):7...8# アニメーション9self.moving = False10self.time = 111...12...1314def idle_move(self):15'''駒が動く時のアニメーション'''16sleep(1.0 / 100)17self.time += 118if self.time >= 10:19self.moving = False20glutIdleFunc(None) # アニメーションの無効化21glutMouseFunc(self.mouse) # マウス操作の有効化22glutPostRedisplay()2324...2526def draw(self):27...28if self.time == 1:29self.main()3031...3233if self.moving:34# 動き中の駒を描画する35if self.endpos in self.gameboard:36glEnable(GL_TEXTURE_2D)37draw_img(..., piece_ID[self.gameboard[self.startpos if self.time == 0 else self.endpos].name])38glDisable(GL_TEXTURE_2D)39...4041def mouse(self, button, state, x, y):42...43# 左クリック44if (button == GLUT_LEFT_BUTTON45and state == GLUT_DOWN):46try:47# 行先選択48if (self.select_dest49and self.parse_mouse() in self.valid_moves(50self.gameboard[self.startpos], self.startpos, self.gameboard)):51self.select_dest = False52self.endpos = self.parse_mouse()53self.time = 054self.moving = True55glutIdleFunc(self.idle_move) # アニメーションの有効化56glutMouseFunc(None) # マウス操作の無効化57# 駒選択58elif ...
sleep
というのは、指定時間プログラムを待機させるための関数です。
これを指定しないと目にも止まらぬ速さで変化してしまうため、アニメーションの意味がありません(マシンのスペックにもよりますが…)。
そもそもアニメーションというのは、パラパラ漫画のように時間差で違う画像を表示させることで動いているように見せているだけです。
idle_move()
では、1/100 秒(マシンのスペックによって変化する)ごとに1コマ進めていき(self.time += 1
)、
それが 10 回行われたらアニメーションを終了させます。
26 行目のif self.time == 1:
は必須ではありませんが、パフォーマンスの向上のためにつけています。
idle_move()
がglutPostRedisplay()
を毎回のアニメーションで 10 回も呼び出す(つまりdraw()
も 10 回呼び出される)のですが、
main()
は1回だけ通れば十分です。
これの関係で、self.time == 0
のときのみ動く駒の存在するとされる位置はself.startpos
に、
それ以外のときはself.endpos
に入っています。
駒の時間-位置関数を指定する
さきほどのコードの 37 行目のdraw_img()
の第一、第二引数にはそれぞれ、画像を描画する位置の x 座標と y 座標が入ります。
ここにどんな関数を入れるかによって、動き方が変わってきます。
とりあえず、直線的な動きをさせてみます。
ちょっと数学の話です。
縦軸を 、横軸を としたとき、この赤のグラフは
と表すことができます。
よって、
1draw_img(self.startpos[0] + (self.endpos[0] - self.startpos[0]) * self.time / 10,2self.startpos[1] + (self.endpos[1] - self.startpos[1]) * self.time / 10,3piece_ID[self.gameboard[self.startpos if self.time == 0 else self.endpos].name])
となります。
もっとなめらかな動きにしたいなら?
直線的な補間の場合、動き始めから終わりまでつねに同じ速さです。
正弦曲線(サインカーブ)を用いて、動き始めと終わりは減速させるようなもっとなめらかな動きをさせることができます。
コードを導くには、しっかりとした(高校レベルぐらいの)数学の計算をやらなければならなりません。
(でも決してこの計算ができなければいけないわけではありません!)
正弦曲線 のグラフを拡大縮小・平行移動して、この赤いグラフに重なるようにします。
のグラフは2点 を通ります。
この2点が変形後に に重なるようにします。
まず、拡大縮小操作をして幅と高さを合わせます。
幅が だったのを にするので、 軸方向に 倍に拡大して
高さが だったのを にするので、
軸方向に 倍に拡大して
あとは平行移動をします。
今の変形で点 が
点 に移ったので、
これが点 に来るようにします。
軸方向に 軸方向に
だけ平行移動して
これで式は出来上がりました。
これをコードに書くと
1draw_img(self.startpos[0] + ((self.endpos[0] - self.startpos[0]) / 2)2* (sin(pi*(self.time - 5) / 10) + 1),3self.startpos[1] + ((self.endpos[1] - self.startpos[1]) / 2)4* (sin(pi*(self.time - 5) / 10) + 1),5piece_ID[self.gameboard[self.startpos if self.time == 0 else self.endpos].name])
この場合from math import pi, sin
が必要になります。
同様に計算ができれば他の曲線によって補間することもできます。
さて、ここで一度動作を見てみましょう。
おっと、移動中は移動先に表示される駒を隠さないといけませんね。
行先に表示される駒を隠す
これはで用意したdark_squares_list
を使えば簡単にできます。
main.py1def draw(self):2...3if self.moving:4# 行先の駒を隠す5if self.endpos in dark_squares_list:6glColor(0.82, 0.55, 0.28)7else:8glColor(1.00, 0.81, 0.62)9square(*self.endpos)10# 動き中の駒を描画する11if self.endpos in self.gameboard:12glEnable(GL_TEXTURE_2D)13draw_img(self.startpos[0] + ((self.endpos[0] - self.startpos[0]) / 2)14* (sin(pi*(self.time - 5) / 10) + 1),15self.startpos[1] + ((self.endpos[1] - self.startpos[1]) / 2)16* (sin(pi*(self.time - 5) / 10) + 1),17piece_ID[self.gameboard[self.startpos if self.time == 0 else self.endpos].name])18glDisable(GL_TEXTURE_2D)
いい感じですね。
ついでですが、開始位置がわかりやすいようにマスに色付けをします。
main.py1def draw(self):2...3draw_squares()4# 移動開始位置のマスの色を変える5if None not in self.startpos:6glColor(0.0, 1.0, 0.0, 0.2)7square(*self.startpos)8...
これで完成イメージと同じような動作ができるようになりました。
アニメーションが追加されて、より迫力のある遊び心地になりました。
はチェスの特殊ルールのひとつであるアンパッサンを実装したいと思います。読んでくれてありがとうございました。
では