マップ画面
ここではマップ画面を作ります。補助的なモジュールは全て書きましたので、このあとは各画面の処理に集中します。
マップ画面の処理は、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つの画面のうち、最後の画面です。すでに必要な補助的なモジュールは全て作成済みです。そのため処理に集中することができます。
