2024-2I プログラミング1 第24回 講義資料

2025年01月10日(金)1・2時限

1 連絡事項

1.1 ツール紹介

2 定着確認

次回、第25回講義 では、オブジェクト指向プログラミング関連の「小テスト❻」を実施します。以下、小テストの練習問題です (このままの内容や形式で出題されるわけではありません)。


Pythonにおけるオブジェクト指向プログラミングに関する説明として、十分に適切な文章となるように括弧【 】にあてはまる語を答えよ


Pythonにおけるオブジェクト指向プログラミングに関する説明として、最も適切な文章となるように 括弧【 】のなかからカンマで区切られた語を選択せよ


次の クラス基礎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 今回講義の概要

前回講義で作成したゲームプログラムの改良を通してオブジェクト指向プログラミングについて理解を深めるとともに、グローバル変数などの新しい要素についても学んでいきます。

img

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_factorchip_sグローバル変数 (global variable) という位置付けになります。関数やクラスの外部で宣言 (初期化) された変数は、プログラム内のどこからでも参照できるため「グローバル変数」と呼ばれます。逆に、関数の内部で宣言された変数ローカル変数 といいます。変数 scale_factorchip_s は、PlayerCharacter クラスのなかでも、main 関数のなかでも参照されるため「グローバル変数」としてこの位置 (=関数やクラスの外部) で宣言 (初期化) しています。

一方で、例えば 第49行目 の変数 clock などは、main 関数のなかだけで使用する変数なので(=PlayerCharacter クラスでは使用予定のない変数なので) グローバル変数にはしません。もちろん、グローバル変数にすることも可能ですが グローバル変数の使用は必要最低限に留める という原則があります。


演習1: 第05行目 の変数 scale_factor を変更して、その動作を確認せよ。

演習2: キャラクタ移動がWASDキーでできるようにプログラムを変更せよ。実装例: 次のセクションの 06_game2_02.py の第99行目から第106行目を参照

5 キャラクタ移動処理の改良

現在のプログラムでは、カーソルキーを押下したときキャラクタが瞬間的に1マス移動します。これを FC版ドラクエのように キャラクタが停止できる位置は1マス単位にしつつ、マスからマスの移動は滑らかにアニメーションする ように処理を改良していきます。

また、キャラクタをWASDキーで操作するように変更しています (このあと 2人目のキャラを登場させ、そちらを矢印キーで操作するため の準備)。

06_game2_02.py というファイルを新規作成して、以下のプログラムを貼り付けて実行し、その動作を確認してください。主な変更箇所としては 第27行目第30行目での変数の追加、第35行目第52行目での メソッド move_toget_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 秒毎) に呼び出されます。

# キャラが移動中ならば、移動アニメ処理の更新
if reimu.is_moving:
  reimu.update_move_process()

キャラ状態が 移動中 (=is_movingTrue) であるとき、フレーム毎に呼び出される 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_movingTrue であることを確認しています。ここでは 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行目 によって 移動先のマスに到着してから更新 (移動完了後に更新) されること に注意してください。

img

演習1: 第43行目 の係数を変化させて、その実行結果について確認せよ。

演習2: プログラムの読解に努めよ。しっかりと理解するためには最低でも20分以上は要するので、5分で諦めるのは早すぎる。なお、内容が理解できない部分については、その部分をコメントアウトしてみて、プログラムにどのように影響を与えるかを確認してみるとよい。

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_dpg.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回講義で学び、小テスト❷でも出題したように、リストの「インデックス」と「要素」を得るものでした。

具体的には、初回のループにおいては i0 が代入され、kpg.K_w が代入されて 第102行目 が処理されます。次のループでは i1kpg.K_d、さらにその次のループでは i2kpg.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_move

7 魔理沙を登場させる

「霊夢」とは独立して操作可能な2人目のキャラクタとして、「魔理沙」を登場させるようにプログラムを拡張していきます。この機能拡張には 大幅なプログラムの修正が必要になりそうですが、クラスを使ってプログラムを構造化 しているため、実際にはかなり簡単に行うことができます。

img

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_movecmd_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_movecmd_move_km に、キャラクタの数だけ「要素」を追加して初期化しています。Python では for _ in char_arr: のようにループ変数にアンダースコア _ を使う場合、それは慣例的に ループ内で、そのループ変数は使用しない (参照されない) という意味を持ちます。

なお、cmd_movecmd_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回目のループでは p0char「霊夢」のキャラオブジェクト が代入されて 第126行目から第138行目が処理されます。2回のループでは p1char に「魔理沙」のキャラオブジェクトが代入され処理されます。

# 各キャラの移動コマンドの処理
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_kmmain 関数の変数であるが、これは PlayerCharacter の属性とすることが適用である。そのようにプログラムを書き換えよ。

演習3: クラスによる構造化をしていない状態のプログラム (前回講義05_game2_01.py) において、キャラクタを追加するプログラムについて検討せよ。