見出し画像

【Pyxel】レトロRPG制作メモ(2024/6/18)

 空いた時間でコツコツ作ってます。
 今回はマップとイベントスクリプトについて書こうと思います。


1.マップ

 以前に書いた記事で、Pyxel付属のエディタではフィールドマップを再現しきれない問題について触れました。

既定のタイルマップではサイズが足りない

 何か良い方法は無いかと調べましたが、有力そうな情報は得られず。
 他の制作者さんたちの記事を見ていたところ、こちらの記事を見つけました。

 「これは…っ!」と早速エディタをダウンロードし、フィールドマップを作り直しました。

Tiledエディタでのマップ作成

 使い勝手が良く、すぐに操作に慣れました。マウスホイールをクリックしながら動かすと編集マップが縦横斜めと自由にスクロールするのがめっちゃ楽(´ー`)
 マップにマウスを当てると左下のフッターにX、Y座標、タイルのIDが表示されたり、右クリックしながらドラッグした範囲のタイルをそのままコピペする機能もあって本当に使いやすいです✨

 Tiledで作ったマップの読み込み、描画、当たり判定の処理を作りました。
今までbltm()で描画していましたが、blt()を使って描画するようになります。
 ※後でPyxel公式のサイトを見たらTMXのサンプルがあることに気づきました(;´Д`) 余裕があればこっちで実装してみようかな…。

 所々端折っていますが、ソースは以下のような感じです。ソース内のTiledクラスは、前述したしゅん様の記事 (以下、参考記事と記載します) に記載されているものと同じです。

■Mapクラス

"""
マップクラス
"""
class Map:
    """
    初期化
    """
    def __init__(self):
        self.tiled = Tiled(f"assets/{self.currentTileMap}.tmx")

    """
    描画処理
    """
    def draw(self):
        # タイルマップ
        # pyxel.bltm(0, 0, self.currentTileMap, self.offsetPx, self.offsetPy, pyxel.width, pyxel.height, 0)

        w = CommonConst.TILE_WIDTH
        h = CommonConst.TILE_HEIGHT
        tmpX = -w
        tmpY = -h
        if (self.isMapScroll):
            if self.mapScrollDirection == CommonConst.DIRECTION_UP or self.mapScrollDirection == CommonConst.DIRECTION_DOWN:
                tmpY += self.scrollSum % CommonConst.TILE_HEIGHT * -1
            elif self.mapScrollDirection == CommonConst.DIRECTION_RIGHT or self.mapScrollDirection == CommonConst.DIRECTION_LEFT:
                tmpX += self.scrollSum % CommonConst.TILE_WIDTH * -1

        for layer in self.tiled.layers:
            for j in range(CommonConst.SCREEN_HEIGHT_TILES + 2):
                for i in range(CommonConst.SCREEN_WIDTH_TILES + 2):
                    val = layer.get(i - 1 + tmpTileX, j - 1 + tmpTileY)
                    if val == 0:
                        continue
                    val -= 1

                    x = i * w
                    y = j * h
                    u = (val % CommonConst.TILESET_WIDTH_TILES) * w
                    v = math.floor(val / CommonConst.TILESET_HEIGHT_TILES) * h
                    pyxel.blt(x + tmpX, y + tmpY, CommonConst.IMAGE_BANK_TILE, u, v, w, h, CommonConst.TRANSPARENT_COLOR)

    """
    障害物との当たり判定を行う
    @param tileX 移動先X座標(単位:タイル)
    @param tileY 移動先Y座標(単位:タイル)
    @return 判定結果(True:判定あり/False:判定なし)
    """
    def isHitWall(self, tileX, tileY):
        isHit = False
        for layer in self.tiled.layers:
            val = layer.get(tileX, tileY)
            if val > 128:
                isHit = True
                break
        
        return isHit

 描画や当たり判定はレイヤーの数だけループして処理するようにします。参考記事ではスクロール処理なし、タイルマップの全範囲を描画するソースとなっているため、画面の領域部分のみを表示・スクロールするよう修正しました。
 TIledエディタではレイヤーを切り替えて編集できるため、試しに扉を開ける処理を実装してみました。

レイヤー表示の切り替え

 レイヤー1では床のタイルを配置し、レイヤー2では床タイルの上に扉のタイルを配置します。
 プレイヤーキャラが「レイヤー2の扉タイル」にぶつかったらレイヤー2の扉タイルを消す、の流れで扉を開けることができます。Pyxel付属のマップエディタを使っていた時は(多分)レイヤーが使えなかったので、扉タイルにぶつかったら扉タイルを適切なタイルに置き換える必要があると考えていました。ちょっと日本語が難しいですが、例えば上の画像では扉の下は床ですが、マップによっては草や土である場合があります。これを各マップエリア (もしくは座標) 毎に判定・タイルを置き換えるのはかなり面倒だと思っていました。レイヤーであれば消すだけで良いのでとても楽です。

扉を開ける

 上の画像ではプレイヤーキャラが扉に触れたら開くという処理ですが、以下のようにイベント中に扉を開ける場合もあります。(ちょっと長いです)

イベント中に扉を開ける

 ↑↑ はボガードの小屋のイベントです。YouTubeでプレイ動画を見ると、扉を開ける流れは単に「扉タイルを消す」だけではありませんでした。

①扉を開ける効果音を鳴らす
②扉タイルを消す
③扉を開ける時、一瞬動きが止まる「とっかかり」がある
④アイテムの「カギ」が無い場合や、特定のイベントが起きていない場合は扉を開けられず、「ひらかない」のメッセージを表示する

 上記が必要なので、扉を開けるスクリプトを作る事にしました。

2.スクリプト

 以下のソース内で変数scpirtListに詰め込んでいるのがスクリプトです。「MSG」や「SWITH_OFF」などは以前の記事に詳細が書いてありますが、もっと掘り下げた記事を後で書きたいと思います。

■Scriptクラス

"""
スクリプトクラス
"""
class Script:
    """
    初期化
    """
    def __init__(self):
        self.scriptList.append([])
        self.scriptList.append([
            f"MSG,1,2,{self.NAME0}「あのォ ボガードってひとを## さがしているんですが…##ろうじん「なにもはなすことはない!",
            f"IF,SWITCH_OFF,{Event.HEROINES_CRISIS_3},2,3",
            "END",
            f"EVENT_ID,24,{CommonConst.CHARA_ID_NPC_9}",
            "END",
        ])
        self.scriptList.append([
            "MSG,1,2,ろうじん「なにもおしえん!",
            f"EVENT_ID,25,{CommonConst.CHARA_ID_NPC_9}",
            "END",
        ])
        self.scriptList.append([
            "MSG,1,2,ろうじん「わ わァー うるさーい!## なにもきかない きこえなーい!!##……おお おなごのむねにあるのは…/"
            f" マナのペンダントではないか!##{self.NAME1}「やっぱり## あなたがジェマのきしですか?/"
            f"ボガード「うむ そうじゃ##{self.NAME1}「これはわたしがうまれたとき## からもっていた ははのかたみです/"
            "ボガード「かつてマナのちからを## てにいれた バンドールていこく…/"
            "ぼうりょくで せかいを##しはいしようとした あくのくに/"
            "われわれは やすらぎとへいわを とり##もどすためにバンドールとたたかった/"
            "でもバンドールには はがたたなかった##マナのちからはそれほど##きょだいだったのだ/"
            "そのときに せんとうにたって##われわれをげんきづけてくれた##じょせいがいた/"
            "ジェマのきしをみちびき…ゆうかんに##たたかったじょせいのむねには##そのペンダントがひかっていたんじゃ…/"
            "どうやら こんかいのマナのきのききも##その おなごのペンダントが##かぎをにぎっているらしい/"
            "おくのへやのマトックをつかえ!",
            "EXEC_EVENT_ID,26",
            "END",
        ])
        self.scriptList.append([
            "MSG,1,2,やまづたいに ひがしへいくと##どうくつがある…ゆくてをふさぐ いわ##をこわせば みちがひらかれるだろう/"
            "ウェンデルのまちでシーバにあうんだ##かれもジェマのどうし…/"
            "ペンダントのひみつを##あかしてくれるだろう",
            f"EVENT_ID,26,{CommonConst.CHARA_ID_NPC_9}",
            f"IF,SWITCH_ON,{Event.CONTACT_BOGARD},3,4",
            "END",
            f"SE,PLAY,{CommonConst.SE_INDEX_DOOR}",
            f"DOOR,{CommonConst.DIRECTION_UP}",
            f"SWITCH,1,{Event.CONTACT_BOGARD}",
            "END",
        ])
        # 扉イベント(開かない)
        self.scriptList.append([
            "MSG,1,2,ひらかない",
            "END",
        ])
        # 扉イベント
        self.scriptList.append([
            "WAIT,4",
            f"DOOR,-1",
            f"SE,PLAY,{CommonConst.SE_INDEX_DOOR}",
            "END",
        ])
        # 宝箱イベント
        self.scriptList.append([
            "WAIT,2",
            f"SE,PLAY,{CommonConst.SE_INDEX_TREASURE}",
            "ITEM,32,1",
            f"MSG,1,2,{self.ITEM}をてにいれた",
            "END",
        ])

 ↑↑ 作ったスクリプトを基に、MapクラスのdoEvent()で実行しています。
 以下は作成中のイベント実行処理の一部です。

■Mapクラス

"""
マップクラス
"""
class Map:

    """
    イベント実行処理
    """
    def doEvent(self):
        statement = self.script.getStatement(self.currentEventId)

        # セリフ
        if statement.split(',')[0] == CommonConst.SCRIPT_MSG:
            window = self.windowList[CommonConst.WND_TYPE_MSG]
            if window.isReady():
                window.isActive = True
                sprite = self.spriteList[0]
                tmpList = statement.split(',')[3].split('/')
                msgList = []
                for msg in tmpList:
                    msg = msg.replace(Script.NAME0, self.heroName)
                    msg = msg.replace(Script.NAME1, self.heroineName)
                    msg = msg.replace(Script.ITEM, self.eventGetItemName)
                    msgList.append(msg)

                val = int(statement.split(',')[2])
                isLag = True if int(statement.split(',')[1]) == 1 else False
                if val == 0:
                    window.setCoords(0, 0)
                    window.show(msgList, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT * 2, isLag)
                elif val == 1:
                    window.setCoords(0, CommonConst.TILE_HEIGHT * 10)
                    window.show(msgList, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT * 12, isLag)
                else:
                    # プレイヤーの位置によりウィンドウ表示位置をセットする
                    if sprite.py < CommonConst.TILE_HEIGHT * 8:
                        window.setCoords(0, CommonConst.TILE_HEIGHT * 10)
                        window.show(msgList, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT * 12, isLag)
                    else:
                        window.setCoords(0, 0)
                        window.show(msgList, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT * 2, isLag)
 
            elif window.isEnd():
                window.setReady()
                self.script.nextStatement()

        # ドアを開く
        elif statement.split(',')[0] == CommonConst.SCRIPT_DOOR:
            direction = int(statement.split(',')[1])
            if direction == -1:
                self.__openDoor(self.spriteList[0].moveDirection)
            else:
                self.__openDoor(int(statement.split(',')[1]))

            self.script.nextStatement()

        # SE(効果音)
        elif statement.split(',')[0] == CommonConst.SCRIPT_SE:
            # 再生
            if statement.split(',')[1] == CommonConst.SCRIPT_PLAY:
                pyxel.play(CommonConst.SE_CH, int(statement.split(',')[2]), loop=False)
                self.script.nextStatement()

            # 停止
            elif statement.split(',')[1] == CommonConst.SCRIPT_STOP:
                pyxel.stop(CommonConst.SE_CH)
                self.script.nextStatement()

        # イベントスイッチ
        elif statement.split(',')[0] == CommonConst.SCRIPT_SWITCH:
            flag = True if int(statement.split(',')[1]) == 1 else False
            index = int(statement.split(',')[2])
            Event.eventSwitchMap[index] = flag
            self.script.nextStatement()

        # 条件分岐
        elif statement.split(',')[0] == CommonConst.SCRIPT_IF:
            if statement.split(',')[1] == CommonConst.SCRIPT_RET:
                if self.retConfirm == 0:
                    self.script.setCurrentIndex(int(statement.split(',')[2]))
                else:
                    self.script.setCurrentIndex(int(statement.split(',')[3]))
            elif '>' in statement.split(',')[1]:
                if CommonConst.SCRIPT_MONEY in statement.split(',')[1]:
                    sprite = self.spriteList[0]
                    if sprite.money > int(statement.split(',')[1].split('>')[1]):
                        self.script.setCurrentIndex(int(statement.split(',')[2]))
                    else:
                        self.script.setCurrentIndex(int(statement.split(',')[3]))
            elif statement.split(',')[1] == CommonConst.SCRIPT_SWITCH_ON:
                if Event.eventSwitchMap[int(statement.split(',')[2])]:
                    self.script.setCurrentIndex(int(statement.split(',')[3]))
                else:
                    self.script.setCurrentIndex(int(statement.split(',')[4]))
            elif statement.split(',')[1] == CommonConst.SCRIPT_SWITCH_OFF:
                if not Event.eventSwitchMap[int(statement.split(',')[2])]:
                    self.script.setCurrentIndex(int(statement.split(',')[3]))
                else:
                    self.script.setCurrentIndex(int(statement.split(',')[4]))
            else:
                self.script.nextStatement()

        # 指定イベント実行
        elif statement.split(',')[0] == CommonConst.SCRIPT_EXEC_EVENT_ID:
            self.currentEventId = int(statement.split(',')[1])
            self.script.setCurrentIndex(0)

        # イベント終了
        elif statement.split(',')[0] == CommonConst.SCRIPT_END:
            self.endEvent()

    """
    ドアを開ける
    @param direction 方向
    """
    def __openDoor(self, direction):
        tmpTileX = CommonUtil.pixelsToTiles(self.offsetPx)
        tmpTileY = CommonUtil.pixelsToTiles(self.offsetPy)
        layer = self.tiled.layers[1]

        print(f"dir = {direction} / tmpTileX = {tmpTileX} / tmpTileY = {tmpTileY}")
        if direction == CommonConst.DIRECTION_UP:
            for row in range(tmpTileY - 2, tmpTileY + 2):
                for col in range(tmpTileX + 8, tmpTileX + 12):
                    layer.set(col, row, 0)
        elif direction == CommonConst.DIRECTION_DOWN:
            for row in range(tmpTileY + 14, tmpTileY + 18):
                for col in range(tmpTileX + 8, tmpTileX + 12):
                    layer.set(col, row, 0)
        elif direction == CommonConst.DIRECTION_LEFT:
            pass
        elif direction == CommonConst.DIRECTION_RIGHT:
            pass

 「DOOR」のスクリプト実行時や、プレイヤーキャラが進む方向に扉タイルがあった場合、openDoor()が呼ばれます。openDoor()では扉タイルを「0」(タイルなし)にしています。「0」にする範囲は現在のエリアと向こうのエリアの扉タイルになります。扉は東西南北で必ず壁の中央部分に位置するので、決まった位置のタイルを消すだけで良さそうです。

反対側の扉タイルも消す必要あり

3.今後の作業

 沼の洞窟やビンケットの館に手を付けようと思います。その間にバトルアックス、くさりがま、チェーンフレイルなど新しい武器が手に入るのでその制作やマップのギミックも併せて進めたいと思います。うーん、楽しい…!

4.おわりに

 ここまでご覧くださり、ありがとうございました!m(_ _)m

この記事が気に入ったらサポートをしてみませんか?