SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

レトロ風ゲームを作って学ぶPython入門

【作って学ぶPython】ゲームを開発してみよう!タイトル、マップ画面の実装編

レトロ風ゲームを作って学ぶPython入門 第4回


  • X ポスト
  • このエントリーをはてなブックマークに追加

マップ画面

 ここではマップ画面を作ります。補助的なモジュールは全て書きましたので、このあとは各画面の処理に集中します。

マップ画面
マップ画面

 マップ画面の処理は、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モジュールです。

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モジュールです。ここからは、少し処理が複雑になります。

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の値を更新して、heromove_rate0にリセットして、xyの値を更新します。

 またevent.check()関数の戻り値がFalseのとき(何もイベントが起きなかったとき)は、next()関数を実行します。

next()関数

 次はnext()関数です。「次回移動処理」をおこなうnext()関数では、まずは仮のxyの値を設定します。この値は現在の主人公の位置です。そしてdata.key["keep"]の値を見て、左ならx1減らし、右ならx1増やし、上ならy1減らし、下ならy1増やします。

 この加算や減算の結果、xyがマップの範囲外になった場合は、移動できないのでreturn文で処理を打ち切ります。

 もう1つ処理を打ち切るケースがあります。それは、移動先が水のマスだったときです。

 リストになっているマップの要素位置は、[x + y * map.w]で求めます。x + y * 横幅の計算は、リストでマップを表すゲームではよく出てきます。map.data[x + y * map.w]の値がmap.WATERだった場合は、return文で処理を終了します。

リストの位置 x + y * 横幅
リストの位置 x + y * 横幅

 打ち切る理由がなければ、hero.next_xhero.next_yの値をxyに更新します。

s_map/event.py

 イベントの処理をおこなう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_xlast_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_xlast_yを利用します。それぞれ関数内で値を代入するので、global文で代入可能にします。

 まず、初期化をおこなうinit()関数で、主人公の位置のhero.xhero.ylast_xlast_yに代入します。スタート位置で、いきなりイベントが発生しないようにするためです。

 そしてイベント発生を確認するcheck()関数の冒頭で、直前のマスと現在のマスが同じならreturn文でFalse(イベント発生なし)を返して終了します。

 直前と同じマスかの判定は、「last_xhero.xが同じ」かつ「last_yhero.yが同じ」ならという条件式、last_x == hero.x and last_y == hero.yでおこないます。

 andは、この演算子に左側の式と、右側の式がともにTrueのときだけTrueを返し、それ以外はFalseを返します。

 もしlast_xhero.x、あるいはlast_yhero.yがどちらかでも異なるなら、処理は打ち切られずに先に進みます。

 そのときには、主人公の位置のhero.xhero.ylast_xlast_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に代入します。このlandmap.TOWNと同じなら、街到着イベントを発生させます。ここではhpmpを最大値まで回復させて、ダイアログを表示します。

 最後に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]で処理に必要な部分の情報を抜き出して、変数に代入します。

 敵の生息地elandlandの値と異なるならcontinue文で処理を飛ばします。また、random.randrange(rate)で生成したランダムな値が0でなければcontinue文で処理を飛ばします。

 random.randrange(stop)関数は、0から始めて1ずつ大きくして、stop未満のランダムな値を生成します。ラスボスの魔王のときは、random.randrange(1)となり、0しか生成しないために必ず戦闘が発生します。また、ゴブリンでstop15なら、15回に1回の確率で戦闘が発生します。

 continue文で飛ばされなかった場合は、敵遭遇イベントが発生します。enemy.set()関数で敵のデータをセットします。そして、ダイアログを表示して、data.scene_next"battle"を代入してシーンを移動します。

 最後にreturn文でTrue(イベント発生あり)を返して終了します。

何もイベントが発生しなかったとき

 何もイベントが発生しなかったときは、False(イベント発生なし)を返して終了します。

何もイベントが発生しなかったとき
    return False    # イベントなしで終了

s_map/draw.py

 マップ画面の描画をおこなう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)  # 文字描画

 冒頭のインポート部分では、pygamedata, 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_xorigin_yは主人公の左上の位置にします。

 続いて、移動しているマス位置hero_xhero_yを計算します。

 また、0.0から1.0未満で変わるmove_rateを使い、移動途中の補正値move_xmove_yを計算します。

 最後に、これらの値を加減算してoffset_xoffset_yを求めます。

原点XY、自キャラXY、オフセットXYの模式図
原点XY、自キャラXY、オフセットXYの模式図

マップの描画

 以降は、それほど難しい処理はありません。

 マップの描画の部分を抜き出して掲載します。

マップの描画
    # マップの描画
    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):で横方向のマスです。それぞれyxの値が、描画するマスの位置です。先ほど計算したoffset_xoffset_yを使い、描画位置dxdyを計算します。

 描画する画像の範囲が画面外ならばcontinue文で処理を飛ばします。dx + Udy + Uと計算して判定しているのは、dxdyが画面外でも、画像の一部が画面内に入っている可能性があるからです。

描画範囲
描画範囲

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

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
レトロ風ゲームを作って学ぶPython入門連載記事一覧

もっと読む

この記事の著者

柳井 政和(ヤナイ マサカズ)

クロノス・クラウン合同会社 代表社員http://crocro.com/オンラインソフトを多数公開。プログラムを書いたり、ゲームを作ったり、記事を執筆したり、マンガを描いたり、小説を書いたりしています。「めもりーくりーなー」でオンラインソフト大賞に入賞。最近は、小説家デビューして小説も書いています(『裏切りのプログラム』他)。面白いことなら何でもOKのさすらいの企画屋です。 

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/19458 2024/06/10 12:22

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング