複数の項目から一つを選ぶための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 LI006: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、要素数は1031: 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面を選ぶとこの画像が表示されます。ゲームっぽくなってきました。