例題12-4:唐突に神経衰弱を作ってみる

A: さて、次の例題はマウスの使い方だ。

B: えーと、もう突っ込み疲れたんですが突っ込むべきでしょうか。

A: ん?何を?

B: (ため息) …例題12はもうお開きにしたんじゃないんですか?

A: なんだ、そんなことか。聞かなくても答えはわかってるだろう?

B: …2、3通りありそうな気がしますが、まあ。

A: じゃあ適当に頭の中で補っといてくれ。とにかく、たまってた小ネタを一気に放出したつもりだったんだがマウスの扱い方のことをすっかり忘れてたことに気づいてさ。ありゃー、やっちゃったなと思ったところへ神経衰弱のプログラムをぱぱっと書かなきゃいけなくなったんで、これをマウスの扱い方の例題にしてしまえということで。

B: 神経衰弱?

A: カードゲームの。子供のころトランプでやったことない?

B: いや、そりゃわかってますが、「Aさんが神経衰弱のプログラムを書かなきゃいけない」って事態がわかりません。

A: あー。そりゃな、 ( 自 主 規 制 ) …というわけなのさ。

B: はあ、 ( 自 主 規 制 ) ですか。そりゃ仕方ないですねえ。

A: とにかくそんなわけで、せっかく書いたんだから使えるだけ使いまわさないと癪なもんで。

B: 相変わらずせこいですねえ。

A: ほっとけ。このプログラムは前回のビジランス(試作)以上に心理実験の体裁をなしていない。っていうか、このプログラムの完成形では二重課題にするために神経衰弱のための処理ともう一つの課題のための処理が入り混じっていてすっごくわかりにくいので、サンプル用にするために神経衰弱の部分だけ抜き出している。1枚めくったら-1点、1ペア揃ったら+10点、プレイ時間は10分間で、それまでにすべて揃ったらカードを並べ替えて最初から。

../_images/12-4-01.png

B: なんかOSのオマケのゲームみたいですね。

A: 全部めくった時のcongratulationのメッセージとかハイスコア保存機能とかはないけどな。とにかく、このサンプルプログラムで紹介したいのはpygame.mouseを使ったマウスの使い方。基本的にはキーボードと同じようにpygame.evevt.get()でイベントを得て、マウスに関するイベントがあればそれに対応する処理を行えばいい。マウスに関するイベントは以下の通り。

イベントのtype属性

イベントの発生条件

付随する属性

pygame.locals.MOUSEBUTTONDOWN

マウスのボタンが押された

button … 押されたボタンを表す値(通常の右手用マウスであれば1=左、2=中央、3=右)

pos … マウスカーソルの座標を表すタプル

pygame.locals.MOUSEBUTTONUP

押されていたマウスのボタンが離された

button … 離されたボタンを表す値(通常の右手用マウスであれば1=左、2=中央、3=右)

pos … マウスカーソルの座標を表すタプル

pygame.locals.MOUSEMOTION

マウスが動かされた

buttons … 3つのボタンの状態を表すタプル(押されていれば1、いなければ0)

pos … マウスカーソルの座標を表すタプル

rel … マウスカーソルの移動量を表すタプル

B: ボタンが放されたってイベントもあるんですね。

A: マウスをドラッグするような動作に処理を割り当てたいときに便利だね。ちなみにキーボードでもpygame.locals.KEYUPというイベントがある。

B: キーボードもマウスも扱い方は同じなんですね。わかりやすくていいなあ。じゃあ続いてイベントのkey属性を見ればどのボタンが押されたか…

A: keyじゃなくてbutton属性ね。

B: って、ああ、そうか。微妙に違うってのはいやらしいぞ。

A: ついでに言っておくと、MOUSEBUTTONDOWNとMOUSEBUTTONUPのbutton属性では左のボタンは"1"だが、MOUSEMOTIONのbuttons属性で左ボタンの状態を見るときにはbuttons[0]を参照しなきゃいけない。

B: うっ。

A: さらに言っておくと、 VisionEggの座標系ではスクリーンの左下が(0,0)だが、pygameは左上が(0,0)だ 。だからVisionEggと組み合わせて使う場合は Y座標の値をスクリーンのY方向のサイズから引いて反転させなければいけない。rel属性のY軸の符号も反転する必要がある。

B: ううっ。

A: とどめに言っておくと、 pos属性やrel属性の値はタプルなので、値を書き換えることはできない。 したがって、Y座標を反転する場合はその結果を格納するリストやタプルを作らなければならない。

B: うううっ、前言撤回、わかりやすくないです。

A: まあ、一度理解してしまえば簡単なもんだよ。サンプルプログラムでは86行目から88行目でマウスのボタンが押されたかチェックし、押されていればmposという変数にマウスカーソル座標を格納してY座標を反転している。mpos = list(e.pos)という具合にlist()を使ってタプルをリストに変換しているのがポイントだね。

B: これ、コピペして使いまわして大丈夫ですかね?

A: VisionEggで使う、mposという変数を他で使っていないという条件を満たしていればOKだね。

B: じゃあ僕のコピペライブラリーに入れておこう。

A: そんなもん作ってんのか。まあ関数のところで「徹底的に使いまわせ」と教えたのは私だが。

B: 結構重宝してますよん。

A: そりゃなにより。さて、別に大したテクニックじゃないんだが、紹介しておきたいテクニックがこのサンプルプログラムには使われている。

B: 「大したことない」なんて言っちゃって、本当は「これすげえよ!」とか思ってるんじゃないんですかー?

A: いや、本当に大したことないんだよ。マウスのボタンが押されたときに「どのカードの上で押されたか」を判定する部分だ。15行目から21行目の関数だね。カードはVisionEgg.Textures.Textureを使って描いているわけだが、これらのカードの位置や大きさはVisionEgg.Textures.Textureのparameters.positionとparameters.sizeという属性に保持されている。 だから、いちいちカードの位置を保持するリストを用意したりカードの位置を計算したりしなくても、これらの属性をチェックしてやればカードとマウスカーソルが重なっているかはすぐにわかる。そして、重なっているカードを見つけたら、そのカードのparameters.on属性をFlaseにするという実に自然な手続きでカードを消すことができる。

B: ふーん、まあ、そりゃそうですねえ。で?

A: で?って、それだけなんだけどさ。例題5でダイアログを使ったときとか、今まで何度か「オブジェクト」という考え方が出てきたが、いまいちよくわからなかっただろう?

B: はあ、いまだによくわかっていませんが。

A: 私も初めてオブジェクトという概念を本で読んだときには、いったい何がいいのかよくわからかったんだよ。でもな、○h!×という雑誌にシューティングゲームの敵や弾をオブジェクトで管理するという記事が載っていて、それを読んで「ああ、そうなのか」と一気に疑問が氷解した感じがしてさ。このサンプルプログラムを書いていたらそのことをちょっと思い出して感慨にふけってたのさ。

B: ○h!×?

A: ははっ。古くからPCを使ってる人にしか通じないネタかな。

B: …和やかな顔してるところへすみませんが、要するにAさんが昔話をするためにこのテクニックを取り上げたんですか?

A: んー。まあ、そうかな。

B: … (やれやれ)

A: サンプルプログラムのisMouseOver()関数では第1引数に渡されるオブジェクトはすべて長方形でparameters.positionとparameters.sizeという属性を持っていると仮定しているが、楕円などにも対応しようとするとオブジェクトの形に応じて重なりを判定する処理を振り分ける必要がある。そうすると、それぞれの形状に応じた判定方法をオブジェクトにメソッドとして実装するほうがシンプルだろう。

B: ???

A: いずれ機会があればきちんと解説しよう。ただ、心理実験でそこまで複雑なプログラムが必要になることはあまりないだろうな。今回のサンプルにしても、二重課題の一方をカードゲームにするというあまり一般的じゃない実験を計画したからこそマウスとオブジェクトの重なり判定なんて処理が出てきたわけで。

B: はあ、なんだかあまり期待できなさそうですが、機会があったらお願いします。

A: さて、今回のテーマはマウスの使い方だけなんで、これでお開きにしてもいいんだが、もうひとつだけ触れておこう。 今までのサンプルプログラムでは「キーを押す」、「ボタンを押す」といった 被験者による操作 に対して何かの処理を行っていたが、そのような操作とは独立にキーボードやマウスの状態を調べたいことがある。

B: ? どういう場合だろう。

A: えーと、サンプルがあった方がいいな。すぐ書けるからちょっと待って。

A: …っと、完成。読んでいる皆さんはこちらをダウンロードしてください。→ 12-3a.py

B: じゃあ実行して…。なんか正方形がくるくる回ってますね。

A: 正方形にマウスカーソルを重ねてごらん。

B: おお、色が濃くなるぞ。

../_images/12-4-02.png

カーソルが重なると色が濃くなる

A: マウスを左クリックすると正方形の色が赤→緑→青と変化する。右クリックすると終了だ。 このサンプルでは、正方形が動いているのでマウスを操作しなくても正方形とカーソルが重なるということが起こる。 このような場合、pygame.locals.MOUSEMOTIONなどのイベントは生じないから、イベントを監視する方法では対応できない。 そこで、刺激を描画する前に以下のような関数を呼び出してフレーム毎にマウスカーソルの位置を確認する。

pygame.mouse.get_pos()

マウスカーソルの座標を示すタプルを返す

pygame.mouse.get_pressed()

マウスボタンの状態を示すタプル(1ならば押されている)を返す

B: 今ふと思ったんですが、イベントが生じないということはマウスカーソルが動いていないということで、それだったら最後にpygame.locals.MOUSEMOTIONが起こった時のposを保存しておけばいいんじゃないんですか?

A: むっ。なかなか鋭いな。フルスクリーンモードならそれでもいけるかも知れないが、ウィンドウモードだったらカーソルのフォーカスが他のウィンドウに移ってしまった場合の処理が面倒くさいかも知れんぞ。書いたことないからよくわかんないけど。

B: あー、その発想はなかった。でもカーソルが他のウィンドウに行ってる間はどうでもいいような気が。

A: 心理実験ではあまり考える必要がない状況だろうね。ま、いつものごとく自分の好きな方で書けばいいよ。私ぁget_pos()やget_pressed()を呼ぶ方が好きだね。せっかくだから、キーボードの状態についても同じようにチェックする関数があるので紹介しておこう。

pygame.key.get_pressed()

キーボードのキーが押されている状態を示すタプルを返す

B: これだけですか?

A: pygame.keyにはほかにもいろいろなメソッドがあるけど、まあ心理実験にはpygame.key.get_pressed()があれば十分だろうと思う。興味があったらIPythonでhelp pygame.keyしてみたらいいよ。戻り値のタプルは300以上も要素があって目的のキーが何番目の要素か探すのが大変なので、pygame.localsもimportしておいて以下のように参照すればいい。

k = pygame.key.get_pressed()
if k[pygame.locals.K_z]:
    #zキーが押された時の処理
elif k[pygame.locals.K_x]:
    #xキーが押された時の処理
以下省略

B: ふむふむ。このキーの名前は前の例題で一覧表が出てきましたね、えーと。

A: 例題3-2 だな。pygame.key.get_pressed()のいいところは、複数のキーが同時に押されているかどうかをチェックすることができる点だ。例えばxとzの同時押しなら k[pygame.locals.K_x] and k[pygame.locals.K_z] がTrueであるか確かめればいいよね。イベントを使ってももちろんできるが、get_pressed()を使う方が簡単だと思う。

B: へえ、複数同時押しって10個でも20個でもいけるんですか?

A: 私ぁ20個ものキーを同時押しさせる実験なんて被験者したくないぞ。同時に何個までのキー押しを受け付けるかはキーボードによって異なる。ゲーム用のキーボードだと全キー同時押しに対応している製品もあるようだが、普通はキーの組み合わせ次第でせいぜい2、3個程度だろう。ちなみに私が使っているMicrosoftのキーボードで試してみると、SPACE-X-Pの3個同時押しは認識するが、SPACE-X-Zの三個同時押しだと2つまでしかダメだ。

B: へ? 組み合わせによって違うんですか?

A: 違うよ。キーボードをいろいろ分解して比べてみると面白いからやってみたらいい。

B: いろいろ分解って、そんな何個も持ってませんよ。でもそういえばAさんってやたらキーボード持ってますよね。…趣味?

A: ちゃうわい。個人的にMicrosoftのキーボードを愛用してるんだが、PCを買うとキーボードが要らんのについてくるんだよな。仕方ないんでキーボードを別に買って、付属品はそのまま実験室の棚へ。要らんキーボードが蓄積されていくわけだ。キーボード別売りにしてくれりゃいいのに。

B: ああ、なるほど。そういうわけですか。

A: よっし、そろそろこの例題もお開きかな。次回の予定はなーんも考えてない。ていうか、イヤな仕事を放ったらかしにしてたら山積みになっちゃって、そろそろそっちを片付けないとまずい。

B: ご愁傷様です。

A: そんなわけで、次はどんなネタになるかわかりませんが、ではでは。

  1# -*- coding: shift-jis -*-
  2
  3import VisionEgg
  4import VisionEgg.Core
  5import VisionEgg.Textures
  6import VisionEgg.Text
  7import VisionEgg.MoreStimuli
  8
  9import pygame
 10import pygame.locals
 11import pygame.time
 12import random
 13
 14#関数
 15def isMouseOver(target,mpos):
 16    tpos = target.parameters.position
 17    size = target.parameters.size
 18    if (tpos[0]-size[0]/2.0 < mpos[0] < tpos[0]+size[0]/2.0) and (tpos[1]-size[1]/2 < mpos[1] < tpos[1]+size[1]/2):
 19        return True
 20    else:
 21        return False
 22
 23#VisionEggの初期化
 24VisionEgg.config.VISIONEGG_HIDE_MOUSE = False
 25screen = VisionEgg.Core.get_default_screen()
 26SX,SY= screen.size
 27
 28cardbackimage = VisionEgg.Textures.Texture('cards_png\\b1fv.png')
 29
 30cardimages = []
 31for s in ['d','h','s','c']:
 32    for n in ['1','2','3','4','5','6','7','8','9','10','j','q','k']:
 33        cardimages.append(VisionEgg.Textures.Texture('cards_png\\'+s+n+'.png'))
 34
 35bgWidth = 720
 36bgHeight = 600
 37cardWidth = 80
 38cardHeight = 100
 39offsetX = (SX-bgWidth)/2.0+cardWidth/2.0
 40offsetY = (SY-bgHeight)/2.0+cardHeight/2.0
 41
 42cardstims = []
 43cardbackstims = []
 44numcardstims = 0
 45for x in range(13*4):
 46    cx = x % 9 * cardWidth + offsetX
 47    cy = SY - (x / 9 * cardHeight + offsetY)
 48    cardstims.append(VisionEgg.Textures.TextureStimulus(anchor='center',position=(cx,cy)))
 49    cardbackstims.append(VisionEgg.Textures.TextureStimulus(texture=cardbackimage,size=cardbackimage.size,
 50                                                        anchor='center',position=(cx,cy)))
 51
 52
 53scoreText = VisionEgg.Text.Text(anchor='left',position=(cx+80,cy)) #cy,cyを引き継ぐと楽
 54cardBackground = VisionEgg.MoreStimuli.Target2D(position=(4*cardWidth+offsetX,SY-(2.5*cardHeight+offsetY)), 
 55                                                size=(bgWidth,bgHeight), 
 56                                                color=(0,0.6,0)) 
 57
 58stimAll = cardstims[:]
 59stimAll.extend(cardbackstims)
 60stimAll.append(scoreText)
 61stimAll.insert(0,cardBackground)
 62
 63viewport = VisionEgg.Core.Viewport(screen=screen, stimuli=stimAll)
 64
 65
 66#カードの並べ替え
 67cardidx = range(13*4)
 68random.shuffle(cardidx)
 69
 70for i in range(13*4):
 71    cardstims[i].parameters.texture = cardimages[cardidx[i]]
 72    cardstims[i].parameters.size = cardimages[cardidx[i]].size
 73
 74#カード状態の初期化
 75currentCards = []
 76removedCards = []
 77numMiss = 0
 78score = 0
 79
 80
 81startTime = VisionEgg.time_func()
 82t = VisionEgg.time_func()
 83while t-startTime < 60.0*10: #10分
 84    t = VisionEgg.time_func()
 85    for e in pygame.event.get():
 86        if e.type == pygame.locals.MOUSEBUTTONDOWN:
 87            mpos = list(e.pos)
 88            mpos[1] = SY-mpos[1]
 89            for i in range(13*4):
 90                if (i in removedCards) or (i in currentCards) :
 91                    continue
 92                if isMouseOver(cardbackstims[i],mpos):
 93                    cardbackstims[i].parameters.on = False
 94                    currentCards.append(i)
 95                    break
 96    scoreText.parameters.text=('Score:%d' % (score))
 97    screen.clear()
 98    viewport.draw()
 99    VisionEgg.Core.swap_buffers()
100
101    #check cards after swapping buffers
102    if len(currentCards)>=2:
103        c0 = min(cardidx[currentCards[0]],cardidx[currentCards[1]])
104        c1 = max(cardidx[currentCards[0]],cardidx[currentCards[1]])
105        if c0%13 == c1%13:
106            removedCards.extend(currentCards)
107            currentCards = []
108            score += 10
109        else:
110            cardbackstims[currentCards[0]].parameters.on = True
111            cardbackstims[currentCards[1]].parameters.on = True
112            currentCards = []
113            numMiss += 1
114            score -= 1
115            pygame.time.wait(500)
116
117    if len(removedCards) == 13*4: #すべてオープン
118        #何かcongratulationが欲しい
119        
120        #カードの並べ替えと反転
121        random.shuffle(cardidx)
122        for i in range(13*4):
123            cardstims[i].parameters.texture = cardimages[cardidx[i]]
124            cardstims[i].parameters.size = cardimages[cardidx[i]].size
125            cardbackstims[i].parameters.on = True
126        #カード状態の初期化、スコアはそのまま
127        currentCards = []
128        removedCards = []
129        numMiss = 0