1 連絡事項
- 次回の授業は、年明けの 1月19日(月)です。
- 小テスト❽ を実施します
- Unity 1-Week GAME JAM
- https://unityroom.com/unity1weeks
- 開催 12月22日(月) 0時 〜 12月28日(日) 20時 … お題「もうひとつ」
- 課題08の注意事項や内容について、再度、確認してください。
- 提出期限 : 2026年01月18日 (日) 23時
- Teams に提出箇所を作成済みなので、忘れないうちにリポジトリの
URL (
https://github.com/XXXX/YYYY.git) を提出ください。
1.1 開発用ユーティリティの紹介
(プロンプト例)
Pythonでゲームを開発しようと考えています。迷路の生成アルゴリズムについて紹介してください。もっとも基本的かつ簡単なアルゴリズムから知りたいです。
2 定着確認
次回、第25回講義 では、オブジェクト指向プログラミング関連の知識を確認する「小テスト❽」を実施します。以下、小テストの練習問題です (このままの内容や形式で出題されるわけではありません)。
Pythonにおけるオブジェクト指向プログラミング に関する説明として、十分に適切な文章となるように括弧【 】にあてはまる語を答えよ。なお、以下に掲載する解答例以外にも別解があります。
- 【X】は【Y】の設計図/テンプレート/型であり、【Y】はその【X】に基づいて具体的に生成された実体である。
- 答え: 【X】クラス (Class)。【Y】オブジェクト (Object) または インスタンス (Instance)
- オブジェクト (Object) は【X】とも呼ばれる。
- 答え: インスタンス (Instance)
- クラス/オブジェクトに属する「関数」を一般に【X】という。
- 答え: メソッド (Method) または メンバー関数 (Member Function)
- クラス/オブジェクトに属する「変数」を一般に【X】という。
- 答え: 属性 (Attribute)、プロパティ (Property)、フィールド(Field)、メンバー変数 (Member Variable) 。Python の OOP に関する文脈においては、属性 (Attribute) が最も適切な答えとなります。
- クラス定義においてオブジェクトの生成 (初期化)
を行なう関数を【X】という。【X】は、具体的には【Y】という関数名で定義する。
- 答え: 【X】コンストラクタ
(Constructor)。【Y】
__init__。
- 答え: 【X】コンストラクタ
(Constructor)。【Y】
- メソッドやコンストラクタは、基本的には最低でも【X】個以上の引数を持つように定義し、その第1引数には慣例的に【Y】という名前を付ける。なお、ここで「基本的には」とは「静的メソッドやクラスメソッドを例外として」という意味である。
- 答え: 【X】1。【Y】
self。
- 答え: 【X】1。【Y】
- 2つの連続したアンダースコア (アンダーバー)
のことを【X】という。
- 答え: ダンダー (Dunder)
Pythonにおけるオブジェクト指向プログラミングに関する説明として、最も適切な文章となるように 括弧【 】のなかからカンマで区切られた語を選択せよ。
- コンストラクタにおいては
returnを記述【すべきではない, しなければならない】。- 答え: 「すべきではない」
- クラスの外部からアクセスされることを意図しない「属性/プロパティ」や「メソッド」には【ダンダーで始まる,
ダンダーで終わる,
ダンダーで始まりダンダーで終わる】ような名前を設定する。
- 答え: 「ダンダーで始まる」
次の クラス基礎01.py
を実行したとき、「期待する実行結果」のような標準出力を得たい。括弧【
】にあてはまる文を答えよ。
- 次のプログラムは「括弧抜き」していませんが、どの部分が「括弧抜き」されていても答えられるようにしておいてください。
class Person :
def __init__(self,name,age):
self.age = age
self.name = name
def get_next_year_age(self):
return self.age + 1
p1 = Person('キャロル',29)
print(f'来年の今頃は、{p1.name}も{p1.get_next_year_age()}歳だねwww')▼期待する実行結果▼
来年の今頃は、キャロルも30歳だねwww
次の クラス基礎02.py
を実行したとき、「期待する実行結果」に示すような結果
(標準出力) を得たい。括弧【 】にあてはまる文を答えよ。
- 次のプログラムは「括弧抜き」していませんが、どの部分が「括弧抜き」されていても答えられるようにしておいてください。
class Person :
def __init__(self,name,age):
self.age = age
self.name = name
def celebrate_birthday(self):
self.age +=1
def __str__(self):
return f'{self.name}({self.age}歳)'
p1 = Person('アリス',24)
p2 = Person('ボブ',32)
print(p1)
print(p2)
print('---')
p2.celebrate_birthday() # 誕生日を祝う!メソッド
print(p2)▼期待する実行結果▼
アリス(24歳)
ボブ(32歳)
---
ボブ(33歳)
3 今回講義の概要
前回講義で作成したゲームプログラムの改良を通してオブジェクト指向プログラミングについて理解を深めるとともに、グローバル変数 などの新しい要素についても学んでいきます。
- 今回の講義では前々回に構築したPyGame開発環境や前処理したキャラ画像を使用します。また、前回の内容について ある程度は理解していること を前提とします。
ライセンスについて
今回の講義でも「spellyon氏」が「点睛集積 (http://dispell.net/)」で配付している画像素材を利用します。改めて点睛集積の 利用規約 http://dispell.net/rule_material.html を確認して、承諾のうえ利用してください。
また同様に「管理人nko氏」が「DOT ILLUST (https://dot-illust.net/)」で配付している画像素材も利用します。DOT ILLUST の 利用規約 https://dot-illust.net/terms/ を確認して、承諾のうえ利用してください。
4 前回プログラムのリファクタリングとグローバル変数
前々回に構築した開発環境のなかに
06_game2_01.py
というファイルを新規作成して、以下のプログラムを貼り付けて実行し、その動作を確認してください。このプログラムは、前回講義の最後に示した「クラスを使って構造化したゲームプログラム」を
リファクタリング (refactoring)
して、表示倍率の変更
にも対応させたものになります。
リファクタリングとは
プログラミングにおける リファクタリング とは、可読性と保守性の向上を目的として、外部動作 (プログラムの表面的な振る舞い) を変更せずに内部構造を改善するプロセスを指します。簡単にいうと プログラムを「よりきれい」に「より効率的」に、また「より理解しやすくするため」にコードの書き方を変える作業 です。
動作確認後、読解してください。
import pygame as pg
# 初期化・グローバル変数
# PlayerCharacter と main の両方から参照する変数
scale_factor = 2
chip_s = int(24*scale_factor) # マップチップ基本サイズ
# PlayerCharacterクラスの定義
class PlayerCharacter:
# コンストラクタ
def __init__(self,init_pos,img_path):
self.pos = pg.Vector2(init_pos)
self.size = pg.Vector2(24,32)*scale_factor
self.dir = 2
img_raw = pg.image.load(img_path)
self.__img_arr = []
for i in range(4):
self.__img_arr.append([])
for j in range(3):
p = pg.Vector2(24*j,32*i)
tmp = img_raw.subsurface(pg.Rect(p,(24,32)))
tmp = pg.transform.scale(tmp, self.size)
self.__img_arr[i].append(tmp)
self.__img_arr[i].append(self.__img_arr[i][1])
def turn_to(self,dir):
self.dir = dir
def move_to(self,vec):
self.pos += vec
def get_dp(self):
return self.pos*chip_s - pg.Vector2(0,12)*scale_factor
def get_img(self,frame):
return self.__img_arr[self.dir][frame//6%4]
# ゲームループを含むメイン処理
def main():
# 初期化処理
pg.init()
pg.display.set_caption('ぼくのかんがえたさいきょうのげーむ II')
map_s = pg.Vector2(16,9) # マップの横・縦の配置数
disp_w = int(chip_s*map_s.x)
disp_h = int(chip_s*map_s.y)
screen = pg.display.set_mode((disp_w,disp_h))
clock = pg.time.Clock()
font = pg.font.Font(None,15)
frame = 0
exit_flag = False
exit_code = '000'
# グリッド設定
grid_c = '#bbbbbb'
# 自キャラ移動関連
cmd_move = -1 # 移動コマンドの管理変数
m_vec = [
pg.Vector2(0,-1),
pg.Vector2(1,0),
pg.Vector2(0,1),
pg.Vector2(-1,0)
] # 移動コマンドに対応したXYの移動量
# 自キャラの生成・初期化
reimu = PlayerCharacter((2,3),'./data/img/reimu.png')
# ゲームループ
while not exit_flag:
# システムイベントの検出
cmd_move = -1
for event in pg.event.get():
if event.type == pg.QUIT: # ウィンドウ[X]の押下
exit_flag = True
exit_code = '001'
# 移動操作の「キー入力」の受け取り処理
if event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
cmd_move = 0
elif event.key == pg.K_RIGHT:
cmd_move = 1
elif event.key == pg.K_DOWN:
cmd_move = 2
elif event.key == pg.K_LEFT:
cmd_move = 3
# 背景描画
screen.fill(pg.Color('WHITE'))
# グリッド
for x in range(0, disp_w, chip_s): # 縦線
pg.draw.line(screen,grid_c,(x,0),(x,disp_h))
for y in range(0, disp_h, chip_s): # 横線
pg.draw.line(screen,grid_c,(0,y),(disp_w,y))
# 移動コマンドの処理
if cmd_move != -1:
reimu.turn_to(cmd_move)
af_pos = reimu.pos + m_vec[cmd_move] # 移動(仮)した座標
if (0 <= af_pos.x <= map_s.x-1) and (0 <= af_pos.y <= map_s.y-1) :
reimu.move_to(m_vec[cmd_move]) # 画面範囲内なら実際に移動
# 自キャラの描画
screen.blit(reimu.get_img(frame),reimu.get_dp())
# フレームカウンタの描画
frame += 1
frm_str = f'{frame:05}'
screen.blit(font.render(frm_str,True,'BLACK'),(10,10))
screen.blit(font.render(f'{reimu.pos}',True,'BLACK'),(10,20))
# 画面の更新と同期
pg.display.update()
clock.tick(30)
# ゲームループ [ここまで]
pg.quit()
return exit_code
if __name__ == "__main__":
code = main()
print(f'プログラムを「コード{code}」で終了しました。')プログラムの 第05行目 の
scale_factor を変更すると
ゲーム画面の表示倍率を変更すること ができます
(表示倍率変更は、前回講義で 演習
として取り組んでもらった内容です)。ところで、この
第05行目 と 第06行目 の変数
scale_factor と chip_s は グローバル変数 (global variable)
という位置付けになります。関数やクラスの外部で宣言
(初期化)
された変数は、プログラム内のどこからでも参照できるため「グローバル変数」と呼ばれます。逆に、関数の内部で宣言された変数は
ローカル変数 といいます。変数
scale_factor と chip_s
は、PlayerCharacter
クラスのなかでも、main
関数のなかでも参照されるため「グローバル変数」としてこの位置
(=関数やクラスの外部) で宣言 (初期化) しています。
一方で、例えば 第49行目 の変数
clock などは、main
関数のなかだけで使用する変数なので(=PlayerCharacter
クラスでは使用予定のない変数なので)
グローバル変数にはしません。もちろん、グローバル変数にすることも可能ですが
グローバル変数の使用は必要最低限に留める
という原則があります。
(プロンプト例)
Pythonプログラミングを学んでいる初心者です。グローバル変数について教えてください。また、グローバル変数の使用は必要最低限に留めるべきだ、と習ったのですが、それはなぜですか?
科目の先生は、関数やクラス内でグローバル変数の値を書き換える際の注意点について教えてくれませんでした。代わりに解説してください。
演習1: 第05行目 の変数
scale_factor
を変更して、その動作を確認してください。
演習2: キャラクタ移動がWASDキーでできるようにプログラムを変更してください。
- 実装例: 次のセクションの
06_game2_02.py
の第98行目から第106行目を参照
5 キャラクタ移動処理の改良
現在のプログラムでは、カーソルキーを押下したときキャラクタが瞬間的に1マス移動します。これを FC版ドラクエのように キャラクタが停止できる位置は1マス単位にしつつ、マスからマスの移動は滑らかにアニメーションする ように処理を改良していきます。
また、キャラクタをWASDキーで操作するように変更しています (このあと 2人目のキャラを登場させ、そちらを矢印キーで操作するため の準備)。
06_game2_02.py
というファイルを新規作成して、以下のプログラムを貼り付けて実行し、その動作を確認してください。主な変更箇所としては
第27行目~第30行目での変数の追加、第35行目~第52行目での
メソッド move_to と get_dp
の変更、メソッド update_move_process
の追加、main 関数における
第118行目、第125行目~第127行目
になります。
import pygame as pg
# 初期化処理・グローバル変数
scale_factor = 3
chip_s = int(24*scale_factor) # マップチップ基本サイズ
map_s = pg.Vector2(16,9) # マップの横・縦の配置数
# PlayerCharacterクラスの定義
class PlayerCharacter:
# コンストラクタ
def __init__(self,init_pos,img_path):
self.pos = pg.Vector2(init_pos)
self.size = pg.Vector2(24,32)*scale_factor
self.dir = 2
img_raw = pg.image.load(img_path)
self.__img_arr = []
for i in range(4):
self.__img_arr.append([])
for j in range(3):
p = pg.Vector2(24*j,32*i)
tmp = img_raw.subsurface(pg.Rect(p,(24,32)))
tmp = pg.transform.scale(tmp, self.size)
self.__img_arr[i].append(tmp)
self.__img_arr[i].append(self.__img_arr[i][1])
# 移動アニメーション関連
self.is_moving = False # 移動処理中は True になるフラグ
self.__moving_vec = pg.Vector2(0,0) # 移動方向ベクトル
self.__moving_acc = pg.Vector2(0,0) # 移動微量の累積
def turn_to(self,dir):
self.dir = dir
def move_to(self,vec):
self.is_moving = True
self.__moving_vec = vec.copy()
self.__moving_acc = pg.Vector2(0,0)
self.update_move_process()
def update_move_process(self):
assert self.is_moving
self.__moving_acc += self.__moving_vec * 3
if self.__moving_acc.length() >= chip_s:
self.pos += self.__moving_vec
self.is_moving = False
def get_dp(self):
dp = self.pos*chip_s - pg.Vector2(0,12)*scale_factor
if self.is_moving : # キャラ状態が「移動中」なら
dp += self.__moving_acc # 移動微量の累積値を加算
return dp
def get_img(self,frame):
return self.__img_arr[self.dir][frame//6%4]
# ゲームループを含むメイン処理
def main():
# 初期化処理
pg.init()
pg.display.set_caption('ぼくのかんがえたさいきょうのげーむ II')
map_s = pg.Vector2(16,9) # マップの横・縦の配置数
disp_w = int(chip_s*map_s.x)
disp_h = int(chip_s*map_s.y)
screen = pg.display.set_mode((disp_w,disp_h))
clock = pg.time.Clock()
font = pg.font.Font(None,15)
frame = 0
exit_flag = False
exit_code = '000'
# グリッド設定
grid_c = '#bbbbbb'
# 自キャラ移動関連
cmd_move = -1 # 移動コマンドの管理変数
m_vec = [
pg.Vector2(0,-1),
pg.Vector2(1,0),
pg.Vector2(0,1),
pg.Vector2(-1,0)
] # 移動コマンドに対応したXYの移動量
# 自キャラの生成・初期化
reimu = PlayerCharacter((2,3),'./data/img/reimu.png')
# ゲームループ
while not exit_flag:
# システムイベントの検出
cmd_move = -1
for event in pg.event.get():
if event.type == pg.QUIT: # ウィンドウ[X]の押下
exit_flag = True
exit_code = '001'
# 移動操作の「キー入力」の受け取り処理
if event.type == pg.KEYDOWN:
if event.key == pg.K_w:
cmd_move = 0
elif event.key == pg.K_d:
cmd_move = 1
elif event.key == pg.K_s:
cmd_move = 2
elif event.key == pg.K_a:
cmd_move = 3
# 背景描画
screen.fill(pg.Color('WHITE'))
# グリッド
for x in range(0, disp_w, chip_s): # 縦線
pg.draw.line(screen,grid_c,(x,0),(x,disp_h))
for y in range(0, disp_h, chip_s): # 横線
pg.draw.line(screen,grid_c,(0,y),(disp_w,y))
# 移動コマンドの処理
if not reimu.is_moving :
if cmd_move != -1:
reimu.turn_to(cmd_move)
af_pos = reimu.pos + m_vec[cmd_move] # 移動(仮)した座標
if (0 <= af_pos.x <= map_s.x-1) and (0 <= af_pos.y <= map_s.y-1) :
reimu.move_to(m_vec[cmd_move]) # 画面範囲内なら移動指示
# キャラが移動中ならば、移動アニメ処理の更新
if reimu.is_moving:
reimu.update_move_process()
# 自キャラの描画
screen.blit(reimu.get_img(frame),reimu.get_dp())
# フレームカウンタの描画
frame += 1
frm_str = f'{frame:05}'
screen.blit(font.render(frm_str,True,'BLACK'),(10,10))
screen.blit(font.render(f'{reimu.pos}',True,'BLACK'),(10,20))
# 画面の更新と同期
pg.display.update()
clock.tick(30)
# ゲームループ [ここまで]
pg.quit()
return exit_code
if __name__ == "__main__":
code = main()
print(f'プログラムを「コード{code}」で終了しました。')基本的な仕組みは次のようになります (1度読むだけで理解できる内容ではありまません、プログラムとあわせて繰り返し読んでください)。
まず、移動キー入力があると 第123行目 で
move_to メソッドが呼び出されます。この
move_to メソッド
(第35行目~第39行目)
は、先ほどまでのプログラム (06_game2_01.py)
とは処理が違い、すぐにキャラを移動させずに、その代わりに「移動中」というキャラ状態に移行する処理をします。キャラ状態が「移動中」であることを表すフラグが
is_moving になります。
また、move_to メソッドが引数として受け取った
vec (=pg.Vector2(1,0) や
pg.Vector2(0,1) などの移動方向ベクトル)
を、コピーして変数 __moving_vec に保持します。
また、微小移動量の累積値を保持する
__moving_acc をゼロに初期化します。
def move_to(self,vec):
self.is_moving = True
self.__moving_vec = vec.copy()
self.__moving_acc = pg.Vector2(0,0)
self.update_move_process()ここで特に重要となるのは、変数
__moving_acc であり、これは マスとマスの間の中間位置
(微小移動量の累積値)
を管理・保持するために使用します。この変数の値は
update_move_process
メソッドの内部で更新されます。
update_move_process
メソッドは、move_to メソッド内の
第39行目
で呼び出されていますが、それは初回だけで、以降は main
関数のゲームループ内の 第127行目 で
(キャラ状態が「移動中」のときに) フレームが更新されるたび
(つまり 1/30 秒毎) に呼び出されます。
キャラ状態が 移動中 (=is_moving
が True) であるとき、フレーム毎に呼び出される
update_move_process
メソッドの処理について詳しく見ていきます。
def update_move_process(self):
assert self.is_moving
self.__moving_acc += self.__moving_vec * 3
if self.__moving_acc.length_squared() >= chip_s**2:
self.pos += self.__moving_vec
self.is_moving = False第42行目 では、念のために
is_moving が True
であることを確認しています。ここでは
assert self.is_moving == True を省略して
assert self.is_moving としています。
第43行目
では、微小移動量の累積値 である
self.__moving_acc
に対する加算処理をしています。self.__moving_vec
は第37行目で設定された移動方向ベクトル
(具体的には pg.Vector2(0,-1) や
pg.Vector2(1,0) など) であり、3
は単なる係数です。この係数を大きくすると
キャラはマスからマスに素早く移動し、係数を小さくするとゆっくり移動
します。
変数 __moving_acc
は、画面上の「キャラ描画位置」を計算する
get_dp
(第48行目から第52行目)
のなかで参照されて、ゲームの実行画面に反映されます。
第44行目 では「微小移動量の累積値 __moving_acc
が、1マス分の移動量に到達したか?」を判定しています。length()
はベクトルの大きさを得るメソッドです。もし、__moving_acc
が1マス分の移動量に達していれば self.pos
(グリッドレベルのキャラ位置)
を更新して、キャラ状態の「移動中」を解除します。
実際にプログラムを実行して確認すると分かりやすいですが、キャラクタについてのグリッド座標
reimu.pos は、第45行目 によって
移動先のマスに到着してから更新
(移動完了後に更新) されること に注意してください。
演習1: 第43行目 の係数を変化させて、その実行結果について確認してください。
演習2: プログラムの読解に努めてください。しっかりと理解するためには最低でも20分以上は要するので、5分で諦めるのは早すぎます。なお、内容が理解できない部分については、その部分をコメントアウトして、プログラムにどのように影響を与えるかを確認してみてください。
プログラムを「理解する」とは…
「理解する」とは、読めることではなく、「自身でコードが書けること」あるは「他者に仕組みや処理の流れを説明できること」「最適化やリファクタリングができること」を意味します。
たとえば、AIを使用せずに「キャラの移動速度」を調整できないような状況は、「理解」という段階に到達していません。
6 キーの連続押下の検出
ここまでのプログラムでは、WASDキーを押下しつづけても キャラクタを連続的に移動させることはできませんでした。ここでは、これについて改良していきます。
改良後のプログラムを 06_game2_03.py
とします。さきほどの 06_game2_02.py の
第91行目 からを次のように書き換えます。
# システムイベントの検出
cmd_move = -1
for event in pg.event.get():
if event.type == pg.QUIT: # ウィンドウ[X]の押下
exit_flag = True
exit_code = '001'
# 移動操作の「キー入力」の受け取り処理
if event.type == pg.KEYDOWN:
if event.key == pg.K_w:
cmd_move = 0
elif event.key == pg.K_d:
cmd_move = 1
elif event.key == pg.K_s:
cmd_move = 2
elif event.key == pg.K_a:
cmd_move = 3# システムイベントの検出
for event in pg.event.get():
if event.type == pg.QUIT: # ウィンドウ[X]の押下
exit_flag = True
exit_code = '001'
# キー状態の取得
key = pg.key.get_pressed()
cmd_move = -1
cmd_move = 0 if key[pg.K_w] else cmd_move
cmd_move = 1 if key[pg.K_d] else cmd_move
cmd_move = 2 if key[pg.K_s] else cmd_move
cmd_move = 3 if key[pg.K_a] else cmd_move実際にプログラムを実行して、その動作を確認してください。
第98行目 の pg.key.get_pressed()
は、全てのキーの押下状態を一括取得して、その結果を辞書型として返す関数です。ここでは、その結果を変数
key で受け取っています。辞書 key
に対しては pg.K_d や pg.K_RIGHT
などの定数を与えてアクセス可能で、対応するキーが押下されていれば
True が返ってきます
(押下されていなければ False が返ってきます)。
get_pressed の詳細については公式リファレンスを参照してください。
第100行目から第103行目では第19回目講義で学んだ条件式 (三項演算子) を利用しています。
6.1 リファクタリング
次のセクションでは 操作可能な2人目のキャラクタ
を追加しますが、そのための準備としてキー入力受付関連の処理についてリファクタリングをしておきます。06_game2_03-a.py
というファイルを新規作成してリファクタリングしたプログラムを貼り付けて実行し、元のバージョンと完全に同じ動作をすること
を確認してください。
リファクタリングにともない変更した箇所の抜粋を以下に示します。
# 自キャラ移動関連
cmd_move = -1 # 移動コマンドの管理変数
cmd_move_km = [pg.K_w, pg.K_d, pg.K_s, pg.K_a]
m_vec = [
pg.Vector2(0,-1), # 0: 上移動
pg.Vector2(1,0), # 1: 右移動
pg.Vector2(0,1), # 2: 下移動
pg.Vector2(-1,0) # 3: 左移動
] # キー状態の取得
key = pg.key.get_pressed()
cmd_move = -1
for i, k in enumerate(cmd_move_km):
cmd_move = i if key[k] else cmd_move第78行目
では、キャラの移動コマンド( 0 から
3 ) に対応する「キー」のリストを
cmd_move_km に格納しています。
第101行目 では enumerate
を利用した繰り返しをしています。enumerate は、第08回講義で学び、小テスト❸でも出題したように、リストの「インデックス」と「要素」を得るものでした。
具体的には、初回のループにおいては i に
0 が代入され、k に pg.K_w
が代入されて 第102行目
が処理されます。次のループでは i に 1
、k に pg.K_d、さらにその次のループでは
i に 2 、k に
pg.K_s
が代入されます。つまり、この第101行目と第102行目は、元のプログラムにあった次のコードと等価になります。
cmd_move = 0 if key[pg.K_w] else cmd_move
cmd_move = 1 if key[pg.K_d] else cmd_move
cmd_move = 2 if key[pg.K_s] else cmd_move
cmd_move = 3 if key[pg.K_a] else cmd_move7 魔理沙を登場させる
「霊夢」とは独立して操作可能な2人目のキャラクタとして、「魔理沙」を登場させるようにプログラムを拡張していきます。この機能拡張には 大幅なプログラムの修正が必要になりそうですが、クラスを使ってプログラムを構造化 しているため、実際にはかなり簡単に行うことができます。
06_game2_04.py
というファイルを新規作成して、以下のプログラムを貼り付けて実行し、まず、その動作を確認してください。
import pygame as pg
# 初期化処理・グローバル変数
scale_factor = 2
chip_s = int(24*scale_factor) # マップチップ基本サイズ
map_s = pg.Vector2(16,9) # マップの横・縦の配置数
# PlayerCharacterクラスの定義
class PlayerCharacter:
# コンストラクタ
def __init__(self,name,init_pos,img_path):
self.pos = pg.Vector2(init_pos)
self.size = pg.Vector2(24,32)*scale_factor
self.dir = 2
self.name = name
img_raw = pg.image.load(img_path)
self.__img_arr = []
for i in range(4):
self.__img_arr.append([])
for j in range(3):
p = pg.Vector2(24*j,32*i)
tmp = img_raw.subsurface(pg.Rect(p,(24,32)))
tmp = pg.transform.scale(tmp, self.size)
self.__img_arr[i].append(tmp)
self.__img_arr[i].append(self.__img_arr[i][1])
# 移動アニメーション関連
self.is_moving = False # 移動処理中は True になるフラグ
self.__moving_vec = pg.Vector2(0,0) # 移動方向ベクトル
self.__moving_acc = pg.Vector2(0,0) # 移動微量の累積
def turn_to(self,dir):
self.dir = dir
def move_to(self,vec):
self.is_moving = True
self.__moving_vec = vec.copy()
self.__moving_acc = pg.Vector2(0,0)
self.update_move_process()
def update_move_process(self):
assert self.is_moving
self.__moving_acc += self.__moving_vec * 3
if self.__moving_acc.length() >= chip_s:
self.pos += self.__moving_vec
self.is_moving = False
def get_dp(self):
dp = self.pos*chip_s - pg.Vector2(0,12)*scale_factor
if self.is_moving : # キャラ状態が「移動中」なら
dp += self.__moving_acc # 移動微量の累積値を加算
return dp
def get_img(self,frame):
return self.__img_arr[self.dir][frame//6%4]
# ゲームループを含むメイン処理
def main():
# 初期化処理
pg.init()
pg.display.set_caption('ぼくのかんがえたさいきょうのげーむ II')
map_s = pg.Vector2(16,9) # マップの横・縦の配置数
disp_w = int(chip_s*map_s.x)
disp_h = int(chip_s*map_s.y)
screen = pg.display.set_mode((disp_w,disp_h))
clock = pg.time.Clock()
font = pg.font.Font(None,15)
frame = 0
exit_flag = False
exit_code = '000'
# グリッド設定
grid_c = '#bbbbbb'
# キャラ移動関連
cmd_move = []
cmd_move_km = []
m_vec = [
pg.Vector2(0,-1), # 0: 上移動
pg.Vector2(1,0), # 1: 右移動
pg.Vector2(0,1), # 2: 下移動
pg.Vector2(-1,0) # 3: 左移動
]
# キャラの生成・初期化
char_arr = []
char_arr.append(PlayerCharacter('reimu',(3,4),'./data/img/reimu.png'))
char_arr.append(PlayerCharacter('marisa',(12,4),'./data/img/marisa.png'))
for _ in char_arr:
cmd_move.append(-1)
cmd_move_km.append([])
# 各キャラの移動キーの設定 上・右・下・左
cmd_move_km[0] = [pg.K_w,pg.K_d,pg.K_s,pg.K_a]
cmd_move_km[1] = [pg.K_UP,pg.K_RIGHT,pg.K_DOWN,pg.K_LEFT]
# ゲームループ
while not exit_flag:
# システムイベントの検出
for event in pg.event.get():
if event.type == pg.QUIT: # ウィンドウ[X]の押下
exit_flag = True
exit_code = '001'
# キー状態の取得 と 各キャラの移動コマンドcmd_moveの更新
key = pg.key.get_pressed()
for p in range(len(char_arr)):
cmd_move[p] = -1
for i, k in enumerate(cmd_move_km[p]):
cmd_move[p] = i if key[k] else cmd_move[p]
# 背景描画
screen.fill(pg.Color('WHITE'))
# グリッド
for x in range(0, disp_w, chip_s): # 縦線
pg.draw.line(screen,grid_c,(x,0),(x,disp_h))
for y in range(0, disp_h, chip_s): # 横線
pg.draw.line(screen,grid_c,(0,y),(disp_w,y))
# 各キャラの移動コマンドの処理
for p, char in enumerate(char_arr):
if not char.is_moving :
if cmd_move[p] != -1:
char.turn_to(cmd_move[p])
af_pos = char.pos + m_vec[cmd_move[p]] # 移動(仮)した座標
if (0 <= af_pos.x <= map_s.x-1) and (0 <= af_pos.y <= map_s.y-1) :
char.move_to(m_vec[cmd_move[p]]) # 画面範囲内なら移動指示
# キャラが移動中ならば、移動アニメ処理の更新
if char.is_moving:
char.update_move_process()
# キャラの描画
screen.blit(char.get_img(frame), char.get_dp())
# フレームカウンタ と 各キャラのグリッド座標 の描画
frame += 1
frm_str = f'{frame:05}'
screen.blit(font.render(frm_str,True,'BLACK'),(10,10))
for p, char in enumerate(char_arr):
info = f'{char.name} = {char.pos}'
screen.blit(font.render(info,True,'BLACK'),(10,20+10*p))
# 画面の更新と同期
pg.display.update()
clock.tick(30)
# ゲームループ [ここまで]
pg.quit()
return exit_code
if __name__ == "__main__":
code = main()
print(f'プログラムを「コード{code}」で終了しました。')主な変更点をみていきます。
7.1 キャラクタの生成と移動関連変数の初期化
操作可能なキャラクタを増やすことに対応するために
第78行目 と 第79行目
において、cmd_move と cmd_move_km
をリストにしています。この時点では「空のリスト」です。
第88行目 から 第90行目
にかけて PlayerCharacter
クラスから「霊夢」と「魔理沙」のオブジェクト (インスタンス)
を生成して char_arr
というリストに格納しています。ここでは char_arr[0]
には「霊夢」のオブジェクト、char_arr[1]
には「魔理沙」のオブジェクトが格納されます。
なお、PlayerCharacter
クラスの定義のなかで、第16行目 のように新たに
name という 属性 (プロパティ)
を追加しています。そして、第89行目 と
第90行目 の コンストラクタ
の第1引数で、その name
属性にセットする文字列を与えています。この name 属性
(プロパティ)
は、第144行目から第146行目の
画面左上に各キャラの名前と共にグリッド座標を表示するための処理
で参照されています。
第91行目 から 第93行目
では、「空のリスト」であった cmd_move と
cmd_move_km
に、キャラクタの数だけ「要素」を追加して初期化しています。Python
では for _ in char_arr:
のようにループ変数にアンダースコア _
を使う場合、それは慣例的に ループ内で、そのループ変数は使用しない
(参照されない) という意味を持ちます。
なお、cmd_move と cmd_move_km を
第78行目・第79行目
の段階で初期化していないのは、3人目、4人目のキャラを登場させる場合に、最低限のプログラム変更で済むようにしているためです。
# キャラ移動関連
cmd_move = []
cmd_move_km = []
m_vec = [
pg.Vector2(0,-1), # 0: 上移動
pg.Vector2(1,0), # 1: 右移動
pg.Vector2(0,1), # 2: 下移動
pg.Vector2(-1,0) # 3: 左移動
]
# キャラの生成・初期化
char_arr = []
char_arr.append(PlayerCharacter('reimu',(3,4),'./data/img/reimu.png'))
char_arr.append(PlayerCharacter('marisa',(12,4),'./data/img/marisa.png'))
for _ in char_arr:
cmd_move.append(-1)
cmd_move_km.append([])
# 各キャラの移動キーの設定 上・右・下・左
cmd_move_km[0] = [pg.K_w,pg.K_d,pg.K_s,pg.K_a]
cmd_move_km[1] = [pg.K_UP,pg.K_RIGHT,pg.K_DOWN,pg.K_LEFT]7.2 キャラクタの移動処理と描画
第124行目から第138行目では、各キャラクタごとに移動コマンドに基づく処理をして、キャラの描画をしています。
第125行目 の
for p, char in enumerate(char_arr):
により、1回目のループでは p に
0、char に「霊夢」のキャラオブジェクト が代入されて
第126行目から第138行目が処理されます。2回のループでは
p に 1、char
に「魔理沙」のキャラオブジェクトが代入され処理されます。
# 各キャラの移動コマンドの処理
for p, char in enumerate(char_arr):
if not char.is_moving :
if cmd_move[p] != -1:
char.turn_to(cmd_move[p])
af_pos = char.pos + m_vec[cmd_move[p]] # 移動(仮)した座標
if (0 <= af_pos.x <= map_s.x-1) and (0 <= af_pos.y <= map_s.y-1) :
char.move_to(m_vec[cmd_move[p]]) # 画面範囲内なら移動指示
# キャラが移動中ならば、移動アニメ処理の更新
if char.is_moving:
char.update_move_process()
# キャラの描画
screen.blit(char.get_img(frame), char.get_dp())演習1: 3人目のキャラクタを登場させてください。
演習2: 現在、cmd_move_km は
main 関数の変数ですが、これは
PlayerCharacter
クラスの「属性」とすることが適切です。そのようにプログラムを書き換えてください。
演習3:
クラスによる構造化をしていない状態のプログラム (前回講義の
05_game2_01.py)
において、キャラクタを追加するプログラムについて検討してください。
-
クラスを使用しないと著しく再利用性や拡張性が悪くなることを確認してください。
今回で PyGame に関する内容は「終わり」です。次回の講義まで、1ヵ月以上、期間が空きます。課題8の取り組みなどを通じて定期的にプログラミングの練習をしておいてください。