マップ画面
ここではマップ画面を作ります。補助的なモジュールは全て書きましたので、このあとは各画面の処理に集中します。
マップ画面の処理は、s_map/main.py
モジュールを含めて4つあります。ファイルは全てs_map
フォルダの中に作ります。
s_map/ main.py move.py event.py draw.py
s_map/main.py
マップ画面の初期化と更新をおこなうs_map/main.py
モジュールです。
import audio from . import draw, move, event # 初期化 def init(): event.init() # イベント初期化 audio.play(audio.FIELD) # BGM再生 # 更新 def update(): draw.render() # 描画 move.manage() # 移動管理
インポート部分では、audio
モジュールを読み込みます。また、同じパッケージの階層のdraw, move, event
モジュールも読み込みます。
初期化をおこなうinit()
関数では、イベント初期化と、フィールドのBGMの再生をおこないます。
更新をおこなうupdate()
関数では、描画と移動管理をおこないます。
イベント初期化、描画、移動管理の処理は、このあとプログラムを書きます。
s_map/move.py
移動の処理をおこなうs_map/move.py
モジュールです。ここからは、少し処理が複雑になります。
import pygame, data, map, hero from . import event MOVE_CYCLE = 250 # 移動サイクル last_move = -1 # 最終移動時間 # 移動管理 def manage(): global last_move # 代入可能に time = pygame.time.get_ticks() # ゲーム開始からの経過時間 if time < last_move + MOVE_CYCLE: # 移動サイクルを経過したか判定 # 移動中処理 hero.move_rate = (time - last_move) / MOVE_CYCLE # 移動比率を更新 else: # 到着処理 last_move = time # 最終移動時間を更新 hero.move_rate = 0 # 移動比率を初期化 hero.x = hero.next_x # 到着でX位置を反映 hero.y = hero.next_y # 到着でY位置を反映 if event.check() == False: # イベント発生判定 next() # 次回移動処理 # 次回移動処理 def next(): x = hero.x # 仮のX位置 y = hero.y # 仮のY位置 if data.key["keep"][pygame.K_LEFT ]: x -= 1 # 左ならXを1減らす if data.key["keep"][pygame.K_RIGHT]: x += 1 # 右ならXを1増やす if data.key["keep"][pygame.K_UP]: y -= 1 # 上ならYを1減らす if data.key["keep"][pygame.K_DOWN]: y += 1 # 下ならYを1増やす # マップの範囲外なら終了 if x < 0: return # xが0未満 if x >= map.w: return # xがマップの横幅以上 if y < 0: return # yが0未満 if y >= map.h: return # yがマップの高さ以上 # 移動先が水なら終了 if map.data[x + y * map.w] == map.WATER: return # 次回XY位置を更新 hero.next_x = x # 次回X位置 hero.next_y = y # 次回Y位置
インポート部分では、pygame, data, map, hero
を読み込みます。また同じパッケージの階層からevent
を読み込みます。
変数は、移動サイクル(1マスを移動する間隔)を表すMOVE_CYCLE
、最終移動時間を記録するlast_move
を用意します。
処理をおこなう関数は、「移動管理」をおこなうmanage()
と「次回移動処理」をおこなうnext()
の2つです。
manage()関数
移動管理をおこなうmanage()
関数では、if
文によって、移動中の処理と到着の処理を分岐します。
manage()
関数では、pygame.time.get_ticks()
関数の戻り値によって処理を分岐します。pygame.time.get_ticks()
関数は、ゲーム開始からのミリ秒を返します。
この経過時間が、最終移動時間+移動サイクル未満なら、hero.move_rate
の値だけを更新します。hero.move_rate
は0.0から始まり、1.0未満まで推移します。
経過時間が、最終移動時間+移動サイクル以上ならば到着処理をおこないます。last_move
の値を更新して、hero
のmove_rate
を0
にリセットして、x
とy
の値を更新します。
またevent.check()
関数の戻り値がFalse
のとき(何もイベントが起きなかったとき)は、next()
関数を実行します。
next()関数
次はnext()
関数です。「次回移動処理」をおこなうnext()
関数では、まずは仮のx
とy
の値を設定します。この値は現在の主人公の位置です。そしてdata.key["keep"]
の値を見て、左ならx
を1
減らし、右ならx
を1
増やし、上ならy
を1
減らし、下ならy
を1
増やします。
この加算や減算の結果、x
やy
がマップの範囲外になった場合は、移動できないのでreturn
文で処理を打ち切ります。
もう1つ処理を打ち切るケースがあります。それは、移動先が水のマスだったときです。
リストになっているマップの要素位置は、[x + y * map.w]
で求めます。x + y * 横幅
の計算は、リストでマップを表すゲームではよく出てきます。map.data[x + y * map.w]
の値がmap.WATER
だった場合は、return
文で処理を終了します。
打ち切る理由がなければ、hero.next_x
、hero.next_y
の値をx
とy
に更新します。
s_map/event.py
イベントの処理をおこなうs_map/event.py
モジュールです。街到着イベントと、敵遭遇イベントを発生させます。
import random, data, map, hero, enemy, dialog last_x = -1 # 最終X位置 last_y = -1 # 最終Y位置 # 初期化 def init(): global last_x, last_y # 代入可能に last_x = hero.x # 自キャラX位置に設定 last_y = hero.y # 自キャラY位置に設定 # イベント発生判定 def check(): # 同じマスで連続発生する対策 global last_x, last_y # 代入可能に if last_x == hero.x and last_y == hero.y: return False # 位置が同じなら処理を終了 last_x = hero.x # 最終X位置を更新 last_y = hero.y # 最終Y位置を更新 # 土地 land = map.data[hero.x + hero.y * map.w] # リストの参照位置 if land == map.TOWN: # 現在の土地が街である hero.hp = hero.hp_max # HP回復 hero.mp = hero.mp_max # MP回復 dialog.show("街に到着しました\n休息しました") # ダイアログ表示 return True # イベントありで終了 # 敵遭遇判定 for i, e in enumerate(enemy.ENEMIES): name, rate, eland = e[0:3] # 敵データ抜き出し if eland != land: continue # 土地が違う if random.randrange(rate) != 0: continue # 1/rateの確率で0になる # モンスター遭遇 enemy.set(i) # 敵の設定 dialog.show(f"{name}との\n戦闘を開始!") # ダイアログ表示 data.scene_next = "battle" # シーン変更 return True # イベントありで終了 return False # イベントなしで終了
インポート部分では、標準ライブラリのrandom
モジュール、ゲーム用に作ったdata, map, hero, enemy, dialog
モジュールを読み込みます。
同じマスでのイベント発生を防ぐ処理
プログラム前半のlast_x
、last_y
で処理をおこなう部分は、同じマスで連続でイベントを発生させないためのものです。処理を抜き出して掲載します。
last_x = -1 # 最終X位置 last_y = -1 # 最終Y位置 # 初期化 def init(): global last_x, last_y # 代入可能に last_x = hero.x # 自キャラX位置に設定 last_y = hero.y # 自キャラY位置に設定 # イベント発生判定 def check(): # 同じマスで連続発生する対策 global last_x, last_y # 代入可能に if last_x == hero.x and last_y == hero.y: return False # 位置が同じなら処理を終了 last_x = hero.x # 最終X位置を更新 last_y = hero.y # 最終Y位置を更新
連続発生を防ぐ仕掛けは、last_x
とlast_y
を利用します。それぞれ関数内で値を代入するので、global
文で代入可能にします。
まず、初期化をおこなうinit()
関数で、主人公の位置のhero.x
とhero.y
をlast_x
、last_y
に代入します。スタート位置で、いきなりイベントが発生しないようにするためです。
そしてイベント発生を確認するcheck()
関数の冒頭で、直前のマスと現在のマスが同じならreturn
文でFalse
(イベント発生なし)を返して終了します。
直前と同じマスかの判定は、「last_x
とhero.x
が同じ」かつ「last_y
とhero.y
が同じ」ならという条件式、last_x == hero.x and last_y == hero.y
でおこないます。
and
は、この演算子に左側の式と、右側の式がともにTrue
のときだけTrue
を返し、それ以外はFalse
を返します。
もしlast_x
とhero.x
、あるいはlast_y
とhero.y
がどちらかでも異なるなら、処理は打ち切られずに先に進みます。
そのときには、主人公の位置のhero.x
とhero.y
をlast_x
、last_y
に代入して更新します。ここまでが前半部分です。
街到着イベント
後半はまず、街到着イベントの判定をおこないます。処理を抜き出して掲載します。
# 土地 land = map.data[hero.x + hero.y * map.w] # リストの参照位置 if land == map.TOWN: # 現在の土地が街である hero.hp = hero.hp_max # HP回復 hero.mp = hero.mp_max # MP回復 dialog.show("街に到着しました\n休息しました") # ダイアログ表示 return True # イベントありで終了
map.data
から、現在の位置の土地を取り出して変数land
に代入します。このland
がmap.TOWN
と同じなら、街到着イベントを発生させます。ここではhp
とmp
を最大値まで回復させて、ダイアログを表示します。
最後にreturn
文でTrue
(イベント発生あり)を返して終了します。
敵遭遇イベント
次に、敵遭遇イベントの判定をおこないます。処理を抜き出して掲載します。
# 敵遭遇判定 for i, e in enumerate(enemy.ENEMIES): name, rate, eland = e[0:3] # 敵データ抜き出し if eland != land: continue # 土地が違う if random.randrange(rate) != 0: continue # 1/rateの確率で0になる # モンスター遭遇 enemy.set(i) # 敵の設定 dialog.show(f"{name}との\n戦闘を開始!") # ダイアログ表示 data.scene_next = "battle" # シーン変更 return True # イベントありで終了
for i, e in enumerate(enemy.ENEMIES):
で各敵のデータを取り出して、name, rate, eland = e[0:3]
で処理に必要な部分の情報を抜き出して、変数に代入します。
敵の生息地eland
がland
の値と異なるならcontinue
文で処理を飛ばします。また、random.randrange(rate)
で生成したランダムな値が0でなければcontinue
文で処理を飛ばします。
random.randrange(stop)
関数は、0
から始めて1
ずつ大きくして、stop
未満のランダムな値を生成します。ラスボスの魔王のときは、random.randrange(1)
となり、0
しか生成しないために必ず戦闘が発生します。また、ゴブリンでstop
が15
なら、15回に1回の確率で戦闘が発生します。
continue
文で飛ばされなかった場合は、敵遭遇イベントが発生します。enemy.set()
関数で敵のデータをセットします。そして、ダイアログを表示して、data.scene_next
に"battle"
を代入してシーンを移動します。
最後にreturn
文でTrue
(イベント発生あり)を返して終了します。
何もイベントが発生しなかったとき
何もイベントが発生しなかったときは、False
(イベント発生なし)を返して終了します。
return False # イベントなしで終了
s_map/draw.py
マップ画面の描画をおこなうs_map/draw.py
モジュールです。処理のほとんどはマップの描画です。
import pygame, data, map, hero, img from data import U, W, H, COL_W, COL_BT def render(): # オフセット位置の計算 origin_x = (W - U) // 2 # 原点X origin_y = (H - U) // 2 # 原点Y hero_x = hero.x * U # 自キャラX hero_y = hero.y * U # 自キャラY move_x = (hero.x - hero.next_x) * hero.move_rate * U # 移動途中X move_y = (hero.y - hero.next_y) * hero.move_rate * U # 移動途中Y offset_x = int(origin_x - hero_x + move_x) # オフセットX offset_y = int(origin_y - hero_y + move_y) # オフセットY # マップの描画 for y in range(map.h): for x in range(map.w): # 描画位置 dx = x * U + offset_x # 描画X位置 dy = y * U + offset_y # 描画Y位置 # 画面外なら飛ばす if dx + U < 0: continue # 描画X位置+描画単位が0未満なら if dx >= W: continue # 描画X位置が横幅以上なら if dy + U < 0: continue # 描画Y位置+描画単位が0未満なら if dy >= H: continue # 描画Y位置が高さ以上なら # 土地の種類を得て、対応する画像を描画 land = map.data[x + y * map.w] data.screen.blit(img.land[land], (dx, dy)) # キャラクターの描画 time = pygame.time.get_ticks() # ゲーム開始からの経過時間 ref = hero.iref + time // 250 % 2 # 画像参照位置 data.screen.blit(img.chara[ref], (origin_x, origin_y)) # 画像描画 # ステータスの描画 text = f" hp:{hero.hp}/{hero.hp_max} hp:{hero.mp}/{hero.mp_max} " \ + f"AT:{hero.at} DF:{hero.df} EXP:{hero.exp} LV:{hero.level} " img.font.render_to(data.screen, (U // 2, U // 2), text, COL_W, COL_BT) # 文字描画
冒頭のインポート部分では、pygame
とdata, map, hero, img
を読み込みます。また、data
モジュールから描画単位U
、横幅W
、高さH
、白色COL_W
、半透明の黒色COL_BT
を読み込みます。
オフセット位置の計算
処理の冒頭はオフセット位置の計算です。抜き出して掲載します。
# オフセット位置の計算 origin_x = (W - U) // 2 # 原点X origin_y = (H - U) // 2 # 原点Y hero_x = hero.x * U # 自キャラX hero_y = hero.y * U # 自キャラY move_x = (hero.x - hero.next_x) * hero.move_rate * U # 移動途中X move_y = (hero.y - hero.next_y) * hero.move_rate * U # 移動途中Y offset_x = int(origin_x - hero_x + move_x) # オフセットX offset_y = int(origin_y - hero_y + move_y) # オフセットY
マップ画面では、主人公を画面中央に表示して、マップを動かします。そのため、原点の座標origin_x
、origin_y
は主人公の左上の位置にします。
続いて、移動しているマス位置hero_x
、hero_y
を計算します。
また、0.0から1.0未満で変わるmove_rate
を使い、移動途中の補正値move_x
、move_y
を計算します。
最後に、これらの値を加減算してoffset_x
、offset_y
を求めます。
マップの描画
以降は、それほど難しい処理はありません。
マップの描画の部分を抜き出して掲載します。
# マップの描画 for y in range(map.h): for x in range(map.w): # 描画位置 dx = x * U + offset_x # 描画X位置 dy = y * U + offset_y # 描画Y位置 # 画面外なら飛ばす if dx + U < 0: continue # 描画X位置+描画単位が0未満なら if dx >= W: continue # 描画X位置が横幅以上なら if dy + U < 0: continue # 描画Y位置+描画単位が0未満なら if dy >= H: continue # 描画Y位置が高さ以上なら # 土地の種類を得て、対応する画像を描画 land = map.data[x + y * map.w] data.screen.blit(img.land[land], (dx, dy))
マップの描画は、外側のfor y in range(map.h):
で縦方向のマス、内側のfor x in range(map.w):
で横方向のマスです。それぞれy
、x
の値が、描画するマスの位置です。先ほど計算したoffset_x
、offset_y
を使い、描画位置dx
、dy
を計算します。
描画する画像の範囲が画面外ならばcontinue
文で処理を飛ばします。dx + U
、dy + U
と計算して判定しているのは、dx
やdy
が画面外でも、画像の一部が画面内に入っている可能性があるからです。
マップの描画は、最後にmap.data
から土地の数値land
を取りだして、img.land[land]
の画像を描画します。
キャラクターの描画
次はキャラクターの描画です。
# キャラクターの描画 time = pygame.time.get_ticks() # ゲーム開始からの経過時間 ref = hero.iref + time // 250 % 2 # 画像参照位置 data.screen.blit(img.chara[ref], (origin_x, origin_y)) # 画像描画
キャラクターの描画では、まずpygame.time.get_ticks()
関数で、ゲーム開始からの経過時間を変数time
に代入します。
次の行のtime // 250 % 2
の部分は、250ミリ秒ごとに0、1、0、1、……という値を繰り返す式です。この0、1の値をhero.iref
に足すことで、250ミリ秒ごとに、描画する画像を切り替えることができます。
主人公を含めて、各キャラクターは2枚ずつ画像を用意しているので、これでアニメーションをおこなえます。
ステータスの描画
最後はステータスの描画です。表示するテキストtext
を作り、img.font
のrender_to()
関数で描画します。
# ステータスの描画 text = f" hp:{hero.hp}/{hero.hp_max} hp:{hero.mp}/{hero.mp_max} " \ + f"AT:{hero.at} DF:{hero.df} EXP:{hero.exp} LV:{hero.level} " img.font.render_to(data.screen, (U // 2, U // 2), text, COL_W, COL_BT) # 文字描画
長くなりすぎた処理を途中で改行するときは、行の末尾に\
(バックスラッシュ)を書いて改行します。ここでは、ステータスを表すテキストが長いため改行しています。
まとめと次回
実際にゲームの処理を書いていきました。基本は、前回までに学んだ知識の組み合わせです。あとはゲームによくある処理です。どんな部品が必要か、どう部品を組み合わせるかを把握していると、短い時間でゲームを作れます。
次回は、バトルの処理を作ります。作成する3つの画面のうち、最後の画面です。すでに必要な補助的なモジュールは全て作成済みです。そのため処理に集中することができます。