人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

画像の移動と跳ね返り処理

パイソンが画面の中を跳ね回るアニメーションのサンプルです。アニメーションの原理とPygameでの実現方法を解説します。絵が動き回るとゲームって感じが出てきます。

bound_image.zip

f:id:aidiary:20100605094343p:plain

サンプルスクリプト1

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
import sys
 
SCR_WIDTH,SCR_HEIGHT = 640,480
 
pygame.init()
screen = pygame.display.set_mode((SCR_WIDTH,SCR_HEIGHT))
pygame.display.set_caption(u"画像の移動と跳ね返り処理")
 
img = pygame.image.load("python.png").convert_alpha()
img_rect = img.get_rect()
 
vx = vy = 2  # 1フレームの移動ピクセル
clock = pygame.time.Clock()
 
while True:
    clock.tick(60)  # 60fps
    
    # 画像の移動
    img_rect.move_ip(vx, vy)
    # 跳ね返り処理
    if img_rect.left < 0 or img_rect.right > SCR_WIDTH:
        vx = -vx
    if img_rect.top < 0 or img_rect.bottom > SCR_HEIGHT:
        vy = -vy
    
    screen.fill((0,0,255))
    screen.blit(img, img_rect)
    pygame.display.update()
    
    for event in pygame.event.get():
        if event.type == QUIT: sys.exit()
        if event.type == KEYDOWN and event.key == K_ESCAPE: sys.exit()

サンプルスクリプト2

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
import sys
 
SCR_WIDTH,SCR_HEIGHT = 640,480
 
pygame.init()
screen = pygame.display.set_mode((SCR_WIDTH,SCR_HEIGHT))
pygame.display.set_caption(u"画像の移動と跳ね返り処理2")
 
img = pygame.image.load("python.png").convert_alpha()
img_rect = img.get_rect()
 
vx = vy = 120  # 1秒間の移動ピクセル
clock = pygame.time.Clock()
 
while True:
    time_passed = clock.tick(60)  # 60fpsで前回からの経過時間を返す(ミリ秒)
    time_passed_seconds = time_passed / 1000.0  # ミリ秒を秒に変換
    
    # 画像の移動
    img_rect.x += vx * time_passed_seconds
    img_rect.y += vy * time_passed_seconds
    # 跳ね返り処理
    if img_rect.left < 0 or img_rect.right > SCR_WIDTH:
        vx = -vx
    if img_rect.top < 0 or img_rect.bottom > SCR_HEIGHT:
        vy = -vy
    
    screen.fill((0,0,255))
    screen.blit(img, img_rect)
    pygame.display.update()
    for event in pygame.event.get():
        if event.type == QUIT: sys.exit()
        if event.type == KEYDOWN and event.key == K_ESCAPE: sys.exit()

アニメーションの基本

テレビとかでやってるアニメってどうやって作るか知ってます?ある絵を描いて、また位置を少しずらして絵を描いてという作業を何回も何回も繰り返しているんだそうです。たくさんの少しずつずらした絵を何枚も何枚も描いて順に画面に表示させると動いているように見えるんですね。30分のアニメでもかなりの枚数を描くようです。

私は小さいときにこれを聞いてまさかと思いました。以前は、描いた絵をカメラの前で動かしてるんだと思ってました(笑)少しずつずらして描くなんてものすごく大変で不可能だと思ったわけです。ただぱらぱら漫画のことふっと思い出して妙に納得したのを覚えています。ぱらぱら漫画ってのはノートのすみに方に少しずつ違う絵を描いていってぱらぱらめくると動いてるように見えるって遊びで昔流行っていました。アニメーションは少しずつ違う絵を何枚も描いて順に表示してできているということを実感したわけです。

アニメの一枚一枚の絵をフレーム(こま)と呼びます。昔の漫画にはセル画といってアニメの1フレームを描いた透明なフィルムがおまけでついていました。このフレームを順に画面に表示するわけです。プログラムでアニメーションを作るときも仕組みは同じです。すこしずつ画像をずらして描いたフレームを用意し、順に表示することで画像を動かします。サンプルプログラムでは下の部分です。

vx = vy = 2  # 1フレームの移動ピクセル
clock = pygame.time.Clock()

while True:
    clock.tick(60)  # 60fps
    
    img_rect.move_ip(vx, vy)   # 画像を少しだけ移動
    
    screen.fill((0,0,255))
    screen.blit(img, img_rect)  # 画像の描画
    pygame.display.update()

画像の位置を少しだけ(vx、vyだけ)ずらして1フレームを描き、また画像の位置をずらして1フレームを描くというのを whileループで繰り返しています。フレームを描くのがアニメーターさんじゃなくてコンピュータってこと以外、テレビのアニメと同じですよね?

1秒間に描くフレーム数をFPS(Frame Per Second)と呼びます。決してFirst Person Shooterではありません(笑)上のサンプルでは、60FPSに設定しています。つまり、1秒間に60回フレームが更新されます。「画像を少しだけ移動 => 画像の描画」を1秒間に60回繰り返しているわけです。ゲームでは、ジャンルにもよりますが大体60FPSくらいあれば十分だと言われています。ちなみにテレビアニメは30FPSです。

デフォルトのFPSは使っているパソコンによって変わってしまいます。そこで、Pygameでは、pygame.time.Clockのtick()関数で簡単にFPSを設定できるようになってます。

画像の移動

次に画像を移動させる方法です。一般的には、画像の位置座標 (x, y) を自分で用意してそれを更新するのが一般的ですが、Pygameではpygame.Rectを使うのが簡単です。Rectは画像を囲む四角形の左上の座標 (left,top)、幅 (width)、高さ (height) を表すオブジェクトです。

f:id:aidiary:20100605094344p:plain

画像のRectはpygame.Surface.get_rect()で取得できます。画像はSurfaceであることはイメージを描画する(2008/5/5)で説明してます。

img = pygame.image.load("python.png").convert_alpha()
img_rect = img.get_rect()

print img_rectとするとという文字列が表示されます。これは、ロードしたパイソンの画像が (0,0) の位置にあり、幅42ピクセル、高さ42ピクセルであることを意味してます。画像を動かすには(0,0)の部分、上の図で言うと(left, top)を更新してやればいいことは想像がつきます。そこで

    # 画像の移動
    img_rect.left += vx
    img_rect.top += vy

とやってもよいのですが、サンプルのようにpygame.Rect.move_ip(vx, vy)関数を使うと簡単です。この関数は画像をX軸方向にvx ピクセル、Y軸方向にvyピクセルだけ移動させます。サンプルでは、vx = vy = 2としているのでX軸に2ピクセル、Y軸に2ピクセル移動します。つまり、右下方向に進みます。この (vx, vy)を一般的に速度と呼びます。値が大きいほど画像は速く移動します。vxとvyの値を変えてどのように移動が変わるかいろいろ調べてみるとアニメーションの原理がよくわかります。0やマイナスの値も試してみてください。

    screen.blit(img, img_rect)

画像を描画するときはblit()にimgとともにimg_rectを渡せば指定した場所に簡単に描画できます。PygameではRectはあちこちに出てきて頻繁に使います。

跳ね返り処理

上でいろいろ試してみた方は気がついたと思いますが、vxをプラスの値にすると右に進み、vxをマイナスの値にすると左に進みます。またvyをプラスの値にすると下に進み、マイナスの値にすると上に進みます。vxとvyを組み合わせると斜めに移動します。

では、画像がウィンドウの壁にあたったとき、反射して逆方向に進ませるにはどうすればよいでしょう?速度の符号を反転すればいいですね!X軸方向で壁にあたったらvxの符号を反転、Y軸方向で壁にあたったらvyの符号を反転します。たとえば、右に進んでるとき符号を反転すると左に進みます。左に進んでるとき符号を反転すると右に進みます。

    # 跳ね返り処理
    if img_rect.left < 0 or img_rect.right > SCR_WIDTH:  # 左または右で衝突
        vx = -vx  # 符号を反転すると逆方向に進む
    if img_rect.top < 0 or img_rect.bottom > SCR_HEIGHT:  # 上または下で衝突
        vy = -vy  # 符号を反転すると逆方向に進む

次にウィンドウの壁に当たったかどうかのチェックですが、これはウィンドウのサイズ (SCR_WIDTH, SCR_HEIGHT) とimg_rectの位置関係からチェックできます。下のRect の属性をもとにif文の意味を考えてみてください。

Rectの属性

f:id:aidiary:20100605094345p:plain

Rectの属性(メソッドではない!)を使うことでさまざまな座標にアクセスできます。上の図にまとめた通りです。left, right, top, bottom, width, height, w, h, centerx, centeryは単なる数値を返します。topleft, topright, bottomleft, bottomright, midtop, midbottom, midleft, midright, center, sizeは座標やサイズを表すタプルを返します。他の言語だと自分で座標を計算する関数を作る必要がありますが、Pygameだとあらかじめ用意されていてすごく便利です。

1秒間の移動ピクセル数を指定する方法

vx = vy = 120  # 1秒間の移動ピクセル
clock = pygame.time.Clock()

while True:
    time_passed = clock.tick(60)  # 60fpsで前回からの経過時間を返す(ミリ秒)
    time_passed_seconds = time_passed / 1000.0  # ミリ秒を秒に変換
    
    # 画像の移動
    img_rect.x += vx * time_passed_seconds
    img_rect.y += vy * time_passed_seconds

サンプルスクリプト2は、パイソンの移動速度は結局同じなのですが、tick()の戻り値を使った書き方をしています。tick()は前回(1フレーム前)tick()が呼ばれてからの経過時間(ミリ秒)を戻り値として返してます。たとえば、60fpsの場合、1秒間に60回tick()が呼ばれることになるのでだいたい1000ミリ秒/60回でtick()の間隔は16〜17ミリ秒になります。実際、time_passedを表示してみるとそうなってます。 time_passed_secondsはミリ秒を秒に変更してます。16ミリ秒は0.016秒になります。

このサンプルでは、vx、vyに1秒間の移動ピクセル数を指定しています。 上の例では、1秒間に120ピクセルの速度を設定してます。では、1フレームでは何ピクセル動かせばよいでしょう?1フレームの経過時間は time_passed_secondsです。よって、vx * time_passed_seconds、つまり120(ピクセル/秒)×0.016(秒)で大体2ピクセル動かせばよいとわかります。これは、サンプルスクリプト1で1フレームの移動距離を2としたのとほとんど同じです。

tick()の戻り値を使うのはすこし複雑ですが、1フレームの正確な経過時間をもとに移動距離を割り出せるため正確なアニメーションが実現できます。

メモ

tick()を設定しなくてもwhileループがものすごいはやさで回ることはなく、60FPSくらいに抑えられているようです。どこで休止を入れているのかわかりません・・・知ってたら教えてください。