前の頁へ   次の頁へ

老い学の捧げ物 Part Ⅰ

4.選択画面の作成

複数の項目から一つを選ぶためのGUIとして、ラジオボタン、メニューなどがあります。遊びたい面の番号を選ぶだけの機能なので、ラジオボタンがいいでしょう。ところがpygameにはラジオボタンどころか普通のボタンもありません。そこで pythonの標準ライブラリにあるTkinterを使ってラジオボタンを作成し、その選択値をpygameに渡すことを考えました。しかしTkinterの画面を閉じないとpygameが起動しないという問題が生じ、Gにはその解決が難しかったため諦め、すべてpygameでコーディングすることにしました。そこでButtonsクラスをbutton.pyとして作成します。

Buttonsクラスの機能は、面データ数のボタンを作成して各々に面データ番号を対応させ、それを画面に表示してマウスで選択した番号をMapクラスに送る、というものです。

ファイル名:button.py
001:import pygame
002:from pygame.locals import *
003:import sys
004:
005:SCR_RECT = Rect(0, 0, 800, 480) # 画面サイズ
006:GREEN = (34,150,34)
007:BLACK = (0,0,0)
008:pygame.init()
009:sysfont = pygame.font.SysFont(None, 25)
010:
011:class Buttons:                         # stage選択の画面用
012:   def __init__(self, num, x, y):
013:      self.num = num
014:      self.x = x                       # button左上 x座標
015:      self.y = y                       # button左上 y座標
016:      
017:   # 四角の描画
018:   def draw(self, GS, screen):
019:      pygame.draw.rect(screen, GREEN, Rect(self.x, self.y, GS, GS))        # fill
020:      pygame.draw.rect(screen, BLACK, Rect(self.x, self.y, GS, GS), 5)     # 外枠は黒
021:
022:   # マウスクリック用
023:   def getRect(self, GS):
024:      return pygame.Rect(self.x, self.y, GS, GS)
025:
026:   # 数字を描画
027:   def writeNum(self, screen):
028:      suu = sysfont.render(str(self.num), False, (255,255,0))
029:      screen.blit(suu, (self.x+5, self.y+8))       # 5,8 = offset

作成するボタンに必要なのは、面番号に対応する整数numと、マウスのクリック座標を検知するための座標(x,y)です。それを12-15行でインスタンス変数に登録しています。あとはコメントに書いた機能をもつメソッドを作成しただけです。なお各buttonは正方形で描画され、「GS」は正方形の一辺のピクセル数です。このButtonsクラスが正常に機能するかどうか、test.pyでテストしてみましょう。このtest.pyはあとでMapクラスに追加します。

ファイル名:test.py
001:import pygame
002:from pygame.locals import *
003:import sys
004:import math
005:from button import Buttons                # Buttons classを import
006:
007:WIDTH = 800
008:HEIGHT = 480
009:SCR_RECT = Rect(0, 0, WIDTH, HEIGHT)
010:pygame.init()
011:screen = pygame.display.set_mode(SCR_RECT.size)
012:   
013:class Test:
014:   def __init__(self):
015:      pass
016:
017:   def getStage(self, num):
018:      btS = round(math.sqrt(num))                        # 画面短辺方向のボタンの数
019:      if num == 2:                                       # 2個の場合は2とする
020:         btS = 2
021:      btPx = int(HEIGHT/btS)                             # ボタン一辺のピクセル数
022:      buttons = []
023:      for i in range(num):                               # numの数だけ
024:         x = i // btS                                       # 横方向のボタン数
025:         y = i % btS                                        # 縦方向のボタン数
026:         buttons.append(Buttons(i, x*btPx, y*btPx))         # Buttonsのインスタンスを配列に入れる
027:      for button in buttons:
028:         button.draw(btPx, screen)
029:         button.writeNum(screen)
030:      pygame.display.update()
031:
032:      while(1):
033:         for event in pygame.event.get():
034:            if event.type == MOUSEBUTTONDOWN and event.button == 1:
035:               for button in buttons:
036:                  if button.getRect(btPx).collidepoint(event.pos):
037:                     print(button.num)
038:            # 終了用のイベント処理
039:            if event.type == QUIT:
040:               pygame.quit()
041:               sys.exit()
042:            if event.type == KEYDOWN and event.key == K_ESCAPE:
043:               pygame.quit()
044:               sys.exit()
045:                     
046:def main():
047:   test = Test()
048:   stage = test.getStage(200)
049:
050:if __name__ == "__main__":
051:   main()

test.pyプログラムは、5行目でButtonsクラスをimportしています。getStage()メソッドでは、面データ数である「num」を受け取り、numの数だけボタンと番号を表示して、マウスクリックした座標に対応した面番号をCMDに表示します。26行では、Buttonクラスのインスタンスをnumの数だけ作成して配列buttonsに書き込んでいます。pythonはインスタンスも配列の要素にできるので、便利ですね。そして27-30行では、buttons[]に格納されたbuttonインスタンスを描画し、対応する番号も描画しています。numの数に応じてボタンの大きさを自動的に変えています。

while文の34-37行では、マウス右ボタンを押した時にマウスカーソルの座標を検知し、その座標をもつbuttons[]のインスタンス変数を取得してCMDに表示します。
では、48行目getStage()の引数を適当に決めてtest.pyを起動すると、CMDに押したボタンの番号が表示されます。引数の数に応じてボタンの大きさが変わっても、ボタン内のどこをクリックしても数字が正しく表示されますね。ボタンの大きさに応じて数字の大きさを変えたいところですが、そのままにしてあります。

それではMapクラスでButtonsクラスを使い、選択した面データを画像として画面に表示してみましょう。soukoフォルダに「mapStage.py」「microban.txt」「souko.py」の三つのファイルを保存し、それを編集します。Gはときどき、カレントディレクトリと違うディレクトリで誤って起動し、編集が反映されていないことをプログラミングのミスだと誤解して時間を無駄にしています。pyプログラムの起動時にはいつもカレントディレクトリを確認する癖をつけましょう。

soukoフォルダにある「mapStage.py」と「test.py」をエディタで開き、test.pyの一部をmapStage.pyにコピペしてmapStage.pyを編集します。編集後のmapStage.pyを以下に示します。追加・変更箇所は緑色で示しています。

ファイル名:mapstage.py
001:import pygame
002:from pygame.locals import *
003:import sys
004:sys.path.append("py\module")
005:import loadimage as LI
006:import math
007:from button import Buttons                # Buttons classを import
008:
009:WIDTH = 800
010:HEIGHT = 480
011:SCR_RECT = Rect(0, 0, WIDTH, HEIGHT)
012:GS = 32
013:   
014:class Map:
015:   pygame.init()
016:   screen = pygame.display.set_mode(SCR_RECT.size)
017:   blockImg = LI.load_image("../images/block.png", -1)
018:   bagImg = LI.load_image("../images/bag.png", -1)
019:   placeImg = LI.load_image("../images/place.png", -1)
020:   storedImg = LI.load_image("../images/stored.png", -1)
021:   playerImg = LI.load_image("../images/player.png", -1)
022:
023:   def __init__(self):
024:      self.mapdata = []                      # mapdataを作成
025:      self.filename = 'microban.txt'
026:      self.blocks = []                       # 壁座標(tuple)の List
027:      self.places = []                       # 収納場所座標(tuple)の List
028:      self.bags = []                         # 荷物座標(tuple)の List
029:      self.stored = []                       # 格納座標(tuple)の List
030:      self.player = []                       # プレーヤー座標(tuple)の List、要素数は1
031:      self.stage = []                        # 選択された面のデータ配列
032:
033:   def getData(self):                        # self.mapdataの作成
034:      flag = []                              # '#'がある行は'0' mapdataに不要な行は'1'
035:      f = open(self.filename, 'r')
036:      while(True):                           # 一行ずつ読んで listに格納
037:         line = f.readline()
038:         if '#' in line:
039:            flag.append('0')
040:         else:
041:            flag.append('1')
042:         if not line:
043:            break
044:      f.close()
045:      max = 0
046:      # 面数を取得して初期化
047:      for i in range(len(flag)):
048:         if flag[i] == '0' and flag[i+1] == '1':            # 不要行の次に'#'を含む行があれば
049:            max += 1
050:      self.mapdata = [[] for _ in range(max)]               # mapdata[][]の初期化
051:
052:      f = open(self.filename, 'r')
053:      lines = f.readlines()                                 # 全行の list
054:      f.close
055:      n = 0
056:      for i in range(len(lines)):                           # 全行を調べる
057:         if '#' in lines[i]:
058:            self.mapdata[n].append(lines[i].rstrip('\n'))   # 改行を削除して配列に追加
059:         if flag[i] == '0' and flag[i+1] == '1':            # 不要行の次に'#'を含む行があれば
060:            n += 1                                          # mapdata[n++]
061:
062:   def getStage(self, num):                           # 面を番号として選択 num:マップデータの面の数
063:      btS = round(math.sqrt(num))                        # 画面短辺方向のボタンの数
064:      if num == 2:                                       # 2個の場合は2とする
065:         btS = 2
066:      btPx = int(HEIGHT/btS)                             # ボタン一辺のピクセル数
067:      buttons = []
068:      for i in range(num):                               # numの数だけ
069:         x = i // btS                                       # 横方向のボタン数
070:         y = i % btS                                        # 縦方向のボタン数
071:         buttons.append(Buttons(i, x*btPx, y*btPx))         # Buttonsのインスタンスを配列に入れる
072:      for button in buttons:
073:         button.draw(btPx, self.screen)
074:         button.writeNum(self.screen)
075:      pygame.display.update()
076:
077:      while(1):
078:         for event in pygame.event.get():
079:            if event.type == MOUSEBUTTONDOWN and event.button == 1:
080:               for button in buttons:
081:                  if button.getRect(btPx).collidepoint(event.pos):
082:                     self.index = button.num
083:                     stage = self.mapdata[self.index]
084:                     return stage                           # stage[][]を返す
085:
086:   def setChars(self, stage):
087:      self.blocks.clear()
088:      self.places.clear()
089:      self.bags.clear()
090:      self.stored.clear()
091:      self.player.clear()
092:      for i in range(len(stage)):               # 行数
093:         for j in range(len(stage[i])):         # 文字数
094:            if stage[i][j] =='#':
095:               self.blocks.append((j, i))          # 壁(x,y)
096:            if stage[i][j] =='.':
097:               self.places.append((j, i))          # 置き場(x,y)
098:            if stage[i][j] =='$':
099:               self.bags.append((j, i))            # 荷物(x,y)
100:            if stage[i][j] =='@':
101:               self.player.append((j, i))          # 荷物(x,y)
102:            if stage[i][j] =='*':
103:               self.stored.append((j, i))          # 収納済(x,y)
104:               self.bags.append((j, i))            # 壁(x,y)
105:               self.places.append((j, i))          # 置き場(x,y)

31行目でインスタンス変数stageを定義しています。stageには、選択された面データの二次元配列を格納します。82-83行では、選択されたボタンの番号をself.indexとしてMapクラスのインスタンス変数に代入しています。82行を無くし83行を単にstage = self.mapdata[button.num]とするだけでいいのですが、あとでこのself.indexをボタン番号の色変更に使う予定なのでこうしました。

次にmainループを含むメインクラスであるsouko.pyを編集します。前回からの変更箇所を緑色で示しています。

ファイル名:souko.py
import pygame
from pygame.locals import*
import sys
sys.path.append("C:\myPython\module")
import loadImage as LI			# moduleの利用
from mapStage import Map		# Mapクラスのインポート

SCR_RECT = Rect(0, 0, 800, 480)
GS = 32

class Souko:
    pygame.init()
    screen = pygame.display.set_mode(SCR_RECT.size)

    def __init__(self):
        pass

    def drawChar(self, map, screen):					# mapを渡す
        for i in map.blocks:
            screen.blit(map.blockImg, (i[0]*GS+200, i[1]*GS))		# Mapクラスの変数の利用
        for i in map.bags:
            screen.blit(map.bagImg, (i[0]*GS+200, i[1]*GS))
        for i in map.places:
            screen.blit(map.placeImg, (i[0]*GS+200, i[1]*GS))
        for i in map.stored:
            screen.blit(map.storedImg, (i[0]*GS+200, i[1]*GS))
        for i in map.player:
            screen.blit(map.playerImg, (i[0]*GS+200, i[1]*GS))
        pygame.display.update()

    def main(self):
        map = Map()							# Mapクラスのインスタンス作成
        map.getData()							# mapdata[][]の作成
        map.stage = map.getStage(len(map.mapdata))			# 面の選択
        self.screen.fill((0,0,0))
        map.setChars(map.stage)						# 選択した面に画像をセット
        while (1):
            self.drawChar(map, self.screen)
            # イベント処理
            for event in pygame.event.get():
            	# 終了用のイベント処理
            	if event.type == QUIT:				# 閉じるボタンが押されたとき
            		pygame.quit()
            		sys.exit()
            	if event.type == KEYDOWN:			# キーを押したとき
            		if event.key == K_ESCAPE:		# Escキーが押されたとき
            			pygame.quit()
            			sys.exit()

if __name__ == "__main__":
    souko = Souko()
    souko.main()

カレントディレクトリをC:\mypython\soukoとし、python souko.pyで起動します。microban.txtの面の数(153)だけボタンが並んだ画面が現れ、任意のボタンを選んでクリックすると、ボタンの画面が消えて選んだ面の画像が表示されます。

例えば128面を選ぶとこの画像が表示されます。ゲームっぽくなってきました。

前の頁へ   次の頁へ