このページでは、Python で tkinter による「マウスカーソルを追いかけるキャラクター」の実現方法について解説していきます。
出来上がりは単純に下のアニメのように画像がマウスカーソルの方向に向かって移動するだけです。
ただ、これを実現するためには、tkinter での定期的な処理やイベント処理だけでなく「三角関数」をうまく利用する必要もあります。三角関数を利用して実現できることについても学べますので、是非読んでみてください!
Contents
マウスカーソルを追いかけるキャラクターの実現方法
それでは、マウスカーソルを追いかけるキャラクターをどうやって実現すれば良いかについて解説していきます。
まず前提として、今回作成するキャラクターはマウスカーソルの位置に直接移動するのではなく、マウスカーソルの方向に向かって一定距離ずつ進むようにしていきたいと思います。
マウスカーソルの位置に直接移動するのであれば、マウスカーソルの位置の座標を取得し、キャラクターの画像をその座標に移動してやれば良いだけです。なので、マウスカーソルの位置の座標さえ取得できれば、簡単に実現することが出来ます。
その一方で、マウスカーソルの方向に向かって一定距離ずつ進ませる場合は、マウスカーソルがキャラクターから見てどの方向にあるかを求め、その方向に対して一定距離のみキャラクターの画像を移動させる必要があります。
キャラクターから見たマウスカーソルの方向(角度)を求めるためにはどうすれば良いでしょうか?また、その方向に対して一定距離のみキャラクターを移動させるためにはどうすれば良いでしょうか?
マウスカーソルを追いかけるキャラクターを実現するためには、この辺りを考えながらプログラミングしていく必要があります。ただ、この辺りの処理は三角関数および逆三角関数を利用すれば簡単に実現することが出来ます。
マウスカーソルの方向を算出する
まず、キャラクターから見たマウスカーソルの方向(角度)の求め方について考えていきましょう!
これは、math
モジュールの math.atan2
関数を利用することで簡単に実現することが出来ます。
math.atan2
は、引数で指定した座標 (x
, y
) より、原点から見てその座標がどの方向に存在するかを求める関数になります。方向というと曖昧かもしれませんが、要は原点とその座標を結ぶ直線が x
軸と正方向になす角の角度を求めることが出来ます(math.atan2
では第1引数には y
座標、第2引数には x
座標を指定します)。
したがって、キャラクターが原点の位置に移動するようにキャラクターの位置とマウスカーソルの位置を平行移動し、さらに平行移動後のマウスカーソルの位置の座標に対して math.atan2
を実行することで、キャラクターから見たマウスカーソルの方向を算出することが出来ます。
あとは、tkinter の bind
メソッドを利用してマウスが移動した時に関数やメソッドが実行されるように設定し、その関数 or メソッドの中で math.atan2
を実行するようにすれば、マウスカーソルの位置が変わるたびに方向 θ
を求めることができるようになります。
こんな感じで、座標から角度・方向を算出したい場合は math.atan2
が便利なので是非覚えておいてください!
ちなみに、多くのプログラミング言語では atan2
だけでなく atan
関数も用意されています。これらの違いについては、下記ページで解説しておりますので、詳しく知りたい方はぜひ読んでみてください。C言語向けの記事ではありますが atan2
と atan
の違いは理解していただけると思います!
スポンサーリンク
マウスカーソルの方向に一定距離移動する
キャラクターから見たマウスカーソルの方向が求められるようになったので、次はその方向に向かってキャラクターを一定距離移動させることを実現していきたいと思います。
キャラクターの現在位置の座標を (x1
, y1
)、方向(角度)を θ
、移動させる距離を r
、さらに移動後のキャラクターの位置の座標を (x2
, y2
) とすれば、これらは下の図のような関係になります。
つまり、キャラクターの位置を示す x
座標を x2 - x1
、y
座標を y2 - y1
だけ増加させれば、キャラクターが方向 θ
に距離 r
だけ移動することになります。
また、最初にも紹介した下の図における青で色をつけた部分は、底辺の長さ x2 - x1
、高さ y2 - y1
、斜辺の長さ r
の三角形になっていることが確認できますね!
さらに斜辺と x
軸とが正方向になす角度が θ
ですので、cosθ
と sinθ
は下記の式によって求めることができることになります。
cosθ = r / (x2 - x1)
sinθ = r / (y2 - y1)
さらに、上式を x2 - x1
と y2 - y1
について解くようにすれば次の式に変形することが出来ます。
x2 - x1 = r * cosθ
y2 - y1 = r * sinθ
つまり、キャラクターの方向 θ
への距離 r
の移動は、キャラクターが存在する位置の x
座標に r * cosθ
を加算し、y
座標に r * sinθ
を加算することで実現することが出来ます。
で、この方向 θ
を マウスカーソルの方向を算出する で紹介した方法で求めるようにすれば、マウスカーソルの方向にキャラクターを移動させることが出来ます。
あとは、tkinter の after
メソッドを利用して関数やメソッドを定期的に実行するように設定し、その関数 or メソッドの中で座標の加算を行うようにすれば、キャラクターの位置がマウスカーソルを追いかける形で変化していくようになります。
さらに同じ関数 or メソッドの中で、キャラクターの位置に応じてキャラクターを表示するための画像を移動させてやれば、キャラクターがマウスカーソルを追いかけてどんどん移動する動きを実現することが出来ます。
こんな感じで、特定の方向 θ
に移動させる際には r * cosθ
と r * sinθ
の計算を利用することが多いので、この計算式も覚えておくと良いと思います!
マウスカーソルを追いかけるキャラクターのスクリプト
以上を踏まえてマウスカーソルを追いかけるキャラクターを tkinter で実現するスクリプトは下記のようになります。
import tkinter
import math
from PIL import Image, ImageTk
WIDTH = 400 # キャンバスの幅
HEIGHT = 400 # キャンバスの高さ
MOUSE_OVER_RANGE = 10 # キャラクターの中心付近の範囲
SPEED = 3 # キャラクターが一度に移動する距離(px)
IMAGE_SIZE = 100 # キャラクターの画像のサイズ
PATH = "画像のファイルパス" # キャラクターの画像のパス
class MouseChaser:
def __init__(self, master):
'''コンストラクタ'''
self.master = master # 親ウィジェット
self.speed = SPEED # キャラの動作スピード(px)
self.mouse_x = None # マウスのx座標
self.mouse_y = None # マウスのy座標
# キャンバスを作成
self.canvas = tkinter.Canvas(
master,
width=WIDTH, height=HEIGHT
)
self.canvas.pack()
# キャラクターの位置の初期化
self.chara_x = WIDTH / 2
self.chara_y = HEIGHT / 2
# キャラクターの画像オブジェクト生成
self.image = self.createImageObj(PATH)
# キャラクタをキャンバスへ描画
self.canvas.create_image(
self.chara_x, self.chara_y,
anchor=tkinter.CENTER,
image=self.image,
tag="character"
)
# 10ms後にmoveを実行する
self.master.after(10, self.move)
# マウスの移動時にdirectionを実行する
self.canvas.bind("<Motion>", self.direction)
def createImageObj(self, path):
'''pathで指定されたパスの画像のオブジェクトを生成'''
# PIL画像オブジェクトを生成
image = Image.open(path)
# IMAGE_SIZExIMAGE_SIZEの矩形に合わせて&縦横比を保ってリサイズ
ratio = min(IMAGE_SIZE / image.width, IMAGE_SIZE / image.height)
resize_size = (round(ratio * image.width), round(ratio * image.height))
resized_image = image.resize(resize_size)
# tkinter画像オブジェクトに変換したものを返却
return ImageTk.PhotoImage(resized_image)
def direction(self, event):
'''マウスカーソルの方向を算出'''
# マウスの座標を覚えておく
self.mouse_x = event.x
self.mouse_y = event.y
# キャラクターから見たマウスカーソルの方向を算出
self.angle = math.atan2(
self.mouse_y - self.chara_y,
self.mouse_x - self.chara_x
)
def isMousedOver(self):
'''キャラクターの中心付近とマウスが重なったかどうかを判断'''
if self.mouse_x < self.chara_x - MOUSE_OVER_RANGE / 2:
return False
if self.mouse_x > self.chara_x + MOUSE_OVER_RANGE / 2:
return False
if self.mouse_y < self.chara_y - MOUSE_OVER_RANGE / 2:
return False
if self.mouse_y > self.chara_y + MOUSE_OVER_RANGE / 2:
return False
return True
def move(self):
'''キャラクターの移動'''
# moveを10ms毎に定期的に実行
self.master.after(10, self.move)
# まだマウスカーソルが動いていないなら何もしない
if self.mouse_x is None or self.mouse_y is None:
return
# キャラクターの中心がマウスカーソルの位置と一致したら止める
if self.isMousedOver():
self.speed = 0
else:
self.speed = SPEED
# キャラクターを距離speedだけangle方向に移動させる
self.chara_x += self.speed * math.cos(self.angle)
self.chara_y += self.speed * math.sin(self.angle)
# 移動後の位置にキャラクターの画像を移動
self.canvas.coords(
"character",
self.chara_x, self.chara_y
)
app = tkinter.Tk()
chaser = MouseChaser(app)
app.mainloop()
実行するためには、予め下記の 画像のファイルパス
部分を変更してキャラクターとして表示する画像のファイルパスを設定する必要があるので注意してください。
PATH = "画像のファイルパス" # キャラクターの画像のパス
上記設定後にスクリプトを実行してアプリ上でマウスカーソルを移動させれば、下のアニメのようにキャラクターがマウスカーソルを追いかける形で移動します。
以降では、簡単に各メソッドの処理内容について解説を行なっていきます。
__init__
__init__
では主に下記のことを行なっています。
- 各種データ属性の初期化
- キャンバスの作成(
tkinter.Canvas
の実行) - キャンバスの配置(
pack
メソッドの実行) - 画像オブジェクトの生成(
createImageObj
メソッドの実行) - キャンバスへの画像の描画(
create_image
メソッドの実行) - 定期処理の開始(
after
メソッドの実行) - マウス移動時のイベント処理の設定(
bind
メソッドの実行)
詳細はスクリプトにコメントで記していますので、詳しくはコメントをご参照しただければと思います。
また、4. を除く 2. から 7. の処理に関しては、それぞれ個別のページで詳しく解説しておりますので、詳しく知りたい内容がございましたら別途該当のページをご参照しただければと思います。
2. の「キャンバスの作成」の解説ページ
Tkinterの使い方:キャンバスウィジェットの作り方3. の「ウィジェットの配置」の解説ページ
Tkinterの使い方:ウィジェットの配置(pack・grid・place)5. の「キャンバスへの図形の描画」の解説ページ
Tkinterの使い方:Canvasクラスで図形を描画する6. の「after
メソッド」の解説ページ
7. の「イベント処理」の解説ページ
Tkinterの使い方:イベント処理を行うスポンサーリンク
createImageObj
メソッド
前述の __init__
で行なっている処理の 4. で実行しているのが、この createImageObj
メソッドになります。
このメソッドでは下記の処理を行なっています。
PATH
の画像ファイルを読み込んで PIL 画像オブジェクトを生成IMAGE_SIZE
xIMAGE_SIZE
の矩形に合わせて画像をリサイズ- リサイズ後の PIL 画像オブジェクトを tkinter 画像オブジェクトに変換して返却
PATH
と IMAGE_SIZE
はスクリプトの先頭付近で設定されるパラメータとなります。
2. で行なっている矩形に合わせて画像をリサイズする方法については下記ページで詳しく解説しておりますので、詳細を知りたい方は下記ページをご参照いただければと思います。
【Python/PIL】縦横比を保ったまま矩形に合わせて画像をリサイズ(resize・thumnail)3. での画像オブジェクトの変換に関しましても下記ページで解説しています。PIL 画像オブジェクトから tkinter 画像オブジェクトへの変換は簡単に行えますので、GUI アプリ作成時に画像を扱う際には PIL が便利です。
【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換direction
メソッド
__init__
と createImageObj
メソッドを実行することで、キャラクターの画像がキャンバスの中心部分に描画されるようになります。ただしまだキャラクターは移動しません。
ここからは、「マウスカーソルを追いかけるキャラクター」を実現するためのメソッドの解説を行なっていきます。
そのメソッドの1つが direction
メソッドであり、このメソッドでは マウスカーソルの方向を算出する で説明した「キャラクターから見たマウスカーソルの方向」を求める処理を行なっています。
この direction
メソッドは、__init__
の中の下記の処理によって "<Motion>"
イベントが発生した際、すなわちマウスカーソルの移動が行われた際に実行されるようになります。
# マウスの移動時にdirectionを実行する
self.canvas.bind("", self.direction)
さらに実行された際には、引数 event
における event.x
と event.y
に移動後のマウスカーソルの座標が渡されます。
このマウスカーソルの座標と、キャラクターの位置の座標であるデータ属性 chara_x
と chara_y
を利用して math.atan2
を実行すれば、キャラクターから見たマウスカーソルの方向を求めることが出来ます。
前述の通り、direction
メソッドはマウスカーソルの移動が行われた際に実行されますので、マウスカーソルが移動するたびにこの方向が変化していくことになります。
また、求めた方向はクラスのデータ属性 angle
に設定していますので、あとはこの angle
の方向にキャラクターを移動させるようにすれば、「マウスカーソルを追いかけるキャラクター」を実現することが出来ます。
move
メソッド
その angle
の方向へのキャラクターの移動を行なっているのが move
メソッドになります。
この move
メソッドでは マウスカーソルの方向に一定距離移動する で説明したキャラクターを一定距離移動させる処理を行なっています。
特にこのキャラクターの移動を行なっているのが move
メソッドの後半部分で、キャラクターの位置の x
座標である chara_x
と キャラクターの位置の y
座標である chara_y
それぞれを マウスカーソルの方向に一定距離移動する で紹介した r * cosθ
と r * sinθ
分だけ増加させています。
ただ、スクリプトにおいては r
がデータ属性の speed
、θ
がデータ属性の angle
にそれぞれ置き換わっているので注意してください。
さらに、キャラクターの移動後の座標 (chara_x
, chara_y
) にキャンバスの coords
メソッドを利用してキャラクターの画像を移動させています。
このキャラクターの座標の計算とその座標への画像の移動の2つにより、キャラクターの移動を実現しています。
ちなみに coords
メソッドをはじめとするキャンバスに描画した図形や画像に対する操作を行うメソッドについては下記ページでまとめていますので、こちらも興味があれば読んでみてください。
また、move
メソッドの最初に下記を実行しており、これにより move
メソッドは一度実行されれば以降は 10
ms ごとに定期的に実行されるようになります。ですので、時間の経過と共にどんどんキャラクターがマウスカーソルに近づいていくことになります。
# moveを10ms毎に定期的に実行
self.master.after(10, self.move)
下記に関しては、まだデータ属性 mouse_x
と mouse_y
が None
の場合に move
メソッドで何もしないようにするための処理です。これらは前述の direction
メソッドでのみ None
以外に設定されるため、マウスカーソルがまだ動かされていない状態ではキャラクターが移動しないことになります。
# まだマウスカーソルが動いていないなら何もしない
if self.mouse_x is None or self.mouse_y is None:
return
さらに下記では、マウスカーソルにキャラクターが到達した際に、キャラクターの移動を止めるための処理になります。マウスカーソルにキャラクターが到達したかどうかを isMousedOver
で判断しており、isMousedOver
が True
を返却した際にはデータ属性 speed
が 0
になることになります。
# キャラクターの中心がマウスカーソルの位置と一致したら止める
if self.isMousedOver():
self.speed = 0
else:
self.speed = SPEED
speed
が 0
の場合は下記の右辺が 0
になり、キャラクターの移動が停止することになります。
# キャラクターを距離speedだけangle方向に移動させる
self.chara_x += self.speed * math.cos(self.angle)
self.chara_y += self.speed * math.sin(self.angle)
また、isMousedOver
が False
を返却した場合はデータ属性 speed
をデフォルトの値 SPEED
に設定するようにしています(SPEED
はスクリプトの先頭付近で設定している値です)。
したがって、一度キャラクターがマウスカーソルに到達して停止した後でも、マウスカーソルを他の位置に移動させればキャラクターが再度移動し始めることになります。
スポンサーリンク
isMousedOver
メソッド
で、前述の通り、isMousedOver
メソッドではマウスカーソルにキャラクターが到達したかどうかの判断を行なっています。
要は、下の図のようにキャラクターの中心付近にマウスカーソルが存在するかどうかを判断し、存在する場合は True
を、それ以外を False
を返却しているだけです。
以上が本スクリプトの解説となります。
まとめ
このページでは、Python で tkinter による「マウスカーソルを追いかけるキャラクター」の実現方法について解説しました!
なんだか簡単に実現できそうにも思えますが、実は三角関数を駆使する必要があって、特に三角関数が苦手な方にとっては難易度はそれなりに高かったのではないかと思います。
このページで解説した内容はほんの一例で、三角関数を利用することでキャラクターの様々な動作を実現することが可能です!
特にゲームなどを作ろうと思うと三角関数を利用することも多いです。特に三角関数が苦手な方であれば、逆に三角関数を理解するチャンスでもありますので、いろんなゲーム作りに挑戦してみると良いと思います!