1 連絡事項
- GAFA (Google, Amazon, Facebook (現Meta),
Apple)
の「エンジニアの年収」に関して紹介しているブログ記事があったのでリンクを掲載しておきます。夢がありますね✨
- GAFA米国本社のエンジニアの年収をジョブレベル別に比較してみた @ 本気のアメリカ就職
- https://honkiku.com/gafa-salary/
- 2024年3月までに「OMUメールのセットアップ (スマホアプリの設定)」と「gmail/GoogleDrive のバックアップ」をしておく必要があります。詳しくはGoogleClassroomを参照してください。
2 課題08 (自由作品)
現在までの Pythonプログラミングに関する学び (9カ月弱) の集大成 として、何らかのPythonアプリ(自由作品) を作成し、そのプログラムを GitHub で公開するとともに、課題07で作成した「ポートフォリオ」のなかに作品を掲載してください。
提出物・提出先 : GitHubリポジトリのURL をGoogleClassroomに提出してください。「 追加または作成」から「 リンク」を選んで提出してください。
- ポートフォリオの内容は、以前に課題07で提出いただいたURLから確認します。もし、URLが変更されている場合は、個別にLINEで連絡をください。
提出期限 : 2024年01月14日 (日) 23時
- 取組みの目安となる時間: 14時間以上 (=1日1時間 x 冬休み日数)。
- 本科目には中間試験や期末試験などの筆記試験がありません。それらの試験勉強時間の代替として本課題に取り組んでください。
評価 :「8点」を標準として「10点満点」で評価します (学年末の成績評価の際には「重み」が付けられます)。特に今回の課題はウェイトが大きいので注意してください。
提出された課題は知能情報コースの学生と教員、保護者に公開します。
ポートフォリオに作品として掲載する際、必ず、次の内容を含めてください。これらに漏れがあると 6点以下の評価 になります。細かな書式や形式については自由です。
- 制作時期として「2023年12月」「2024年1月」「2023年12月~2024年1月」のような情報を明記してください。日付のフォーマットは自由です (例えば「2023/12」や「December 2023」などでもOKです)。この制作時期の情報から、それが本科目の「課題08」であることを識別します。
- 作品の「タイトル」と「説明文章」を書いてください。説明文章は 200文字以上 を記載してください。なお、タイトルに「課題08」のような言葉は不要です。
- 3枚以上のスクリーンショットを載せてください。スクリーンショットの代わりに動画やGIFアニメーションなどでも可です。ただし、GitHubには 25MB を超える動画はアップできないので注意してください (必要に応じてYouTubeなどにアップして、それをページに埋め込みしてください)。
- GitHubリポジトリへのリンクを載せてください。
2.1 Q&A
その他、不明点があればLINEで相談・確認をしてください。
- Q:
フリー素材を使ってアプリを作成しようと考えています。しかし、プロジェクトに使用した「フリー素材」を
GitHubリポジトリにアップするとフリー素材の「利用規約」で禁止されている「再配布」に該当してしまうかもしれません。どうしたらよいですか。
- A: 画像や音楽などのデータについては、必ずしも
GitHub にアップする必要はありません。第16回講義でも紹介しているように
.gitignoreで、それらのデータをGit管理から除外してしください。なお、「アプリ実行画面のスクリーンショットや動画もウェブ公開が不可」となる素材は使用しないでください。
- A: 画像や音楽などのデータについては、必ずしも
GitHub にアップする必要はありません。第16回講義でも紹介しているように
- Q: Jupyter / GoogleColab
のノートブック環境で動作するPythonプログラムでもよいですか。
- A: 問題ありません。
- Q:
ポートフォリオの「トップページ」ではないところ
(トップページからリンクしている別ページ)
に、作品を掲載する形式でもよいですか。
- A: ポートフォリオのトップページからリンクで辿れるページであれば問題ありません。もし、見つけられない場合は個別連絡するので、その場合は、作品を掲載しているページのURLを返信してください。
- Q:
Python以外の言語で開発したアプリでもよいですか。
- A: 今回は「不可」です。
- Q:
前々回講義や、前回/今回講義のなかで作成したゲームアプリを改良した作品でもよいですか。
- A: 問題ありません。
3 定着確認
次回、第25回講義 (2024/01/15) では、オブジェクト指向プログラミング関連の「小テスト❸」を実施します。以下、小テストの練習問題です (このままの内容や形式で出題されるわけではありません)。
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】個以上の引数を持つように定義し、その第1引数には慣例的に【Y】という名前を付ける。なお、ここで「基本的には」とは「静的メソッドやクラスメソッドを例外として」という意味である。答え:
【X】1。【Y】
self。 - 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歳)
4 今回講義の概要
前回講義で作成したゲームプログラムの改良を通してオブジェクト指向プログラミングについて理解を深めるとともに、グローバル変数などの新しい要素についても学んでいきます。
- 今回の講義では前々回に構築した開発環境や前処理した画像を使用します。また、前回の内容について、ある程度は理解していることを前提とします。
- 今回の講義でも「spellyon氏」が「点睛集積 (http://dispell.net/)」で配付している画像素材を利用します。改めて点睛集積の 利用規約 http://dispell.net/rule_material.html を確認して、承諾のうえ利用してください。また「管理人nko氏」が「DOT ILLUST (https://dot-illust.net/)」で配付している画像素材も利用します、DOT ILLUST の 利用規約 https://dot-illust.net/terms/ を確認して、承諾のうえ利用してください。
5 前回プログラムのリファクタリングとグローバル変数
前々回に構築した開発環境のなかに
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
クラスでは使用予定のない変数なので)
グローバル変数にはしません。もちろん、グローバル変数にすることも可能ですが
グローバル変数の使用は必要最低限に留める
という原則があります。
- GhatGPT: Pythonにおけるグローバル変数について ← 読んでおいてください。
演習1: 第05行目 の変数
scale_factor を変更して、その動作を確認せよ。
演習2: キャラクタ移動がWASDキーでできるようにプログラムを変更せよ。実装例:
次のセクションの 06_game2_02.py
の第99行目から第106行目を参照
6 キャラクタ移動処理の改良
現在のプログラムでは、カーソルキーを押下したときキャラクタが瞬間的に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分で諦めるのは早すぎる。なお、内容が理解できない部分については、その部分をコメントアウトしてみて、プログラムにどのように影響を与えるかを確認してみるとよい。
7 キーの連続押下の検出
ここまでのプログラムでは、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回目講義で学んだ条件式 (三項演算子) を利用しています。
7.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_move8 魔理沙を登場させる
「霊夢」とは独立して操作可能な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}」で終了しました。')主な変更点をみていきます。
8.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]8.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)
において、キャラクタを追加するプログラムについて検討せよ。