見出し画像

【Pyxel】レトロRPG制作メモ(2023/7/23)

 制作が少し進んだので、記事を書きます。


1.グラフィックの作成

 前回からグラフィックをメインに作り始めてます。グラフィックはゲームのプレイ動画を一時停止&拡大しながらポチポチと写経しております(;´Д`)
一応、イメージの各バンクは以下の割り当てで作っていくつもりです。

バンク0:キャラクター(主人公、雑魚敵、ボス、NPC)
バンク1:フィールド、ダンジョンのタイル
バンク2:その他(エフェクトや文字など)

 そろそろイベントを作ろうかと思っていて、最初のボス「ジャッカル」をこれから作る予定です。

バンク0:キャラクター

 序盤の町やフィールドのタイルを作り、原作のマップを一部再現してみました。

バンク1:タイル
序盤のフィールドマップ作成

 以下を見てもらうと分かるように、原作のフィールドマップを再現しようとすると既定のタイルマップだとサイズが足りませんでした。

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

 タイルマップの端まで来たら、バンクを切り替える処理が必要かと思ってましたが、Pyxel作者様によると「タイルマップのサイズは任意に変えられる」とのことでしたので、その必要は無さそうです。(教えてくださったfrenchbreadさんに感謝✨)

実はTilemapは自分で任意のサイズのインスタンスを作って使うことができます。
ただ、ツール側が可変サイズに対応していないのでツールで編集したい場合は問題ですね

Pyxel作者 Kitao様より

 ツールでは可変サイズに対応してないとのことなので、ソース上でタイルマップデータを作ることになるのかな? 後で調べてみよう。
 町やダンジョンなどは、フィールドのように画面スクロールではなく画面切り替えなので、マップを他のバンクに作る方向で問題なさそうですね。

 剣以外の武器は追々作っていきます。
 文字は0~9の数字、アルファベットは一応26文字分のスペースを取っていますがゲーム内で使うのは一部のみになりそうです。
 平仮名、カタカナ、記号も用意しました。

バンク2:その他

2.ウィンドウ

 セリフなどを表示するウィンドウを作ってみました。

2-1.ウィンドウと文字の表示

ウィンドウ(セリフ)
ウィンドウ(レベルアップ)

 イメージバンク2にウィンドウ枠のグラフィックを作り、ソースで表示しています。

 ウィンドウを表示した後はセリフやステータスなどの文字を表示します。
セリフや一部のメッセージは1文字ずつ時間差で表示し、ステータス画面などでは一気に全ての文字を表示します。
 セリフの途中で決定ボタンを押すと、読み飛ばし(1行を瞬時に表示)するようにしました。

セリフの表示

2-2.改行とページ

 改行は、あまり使うことが無い「#」を割り当てています。
上記のセリフの例では、ソースで以下のように文字をセットしています。

self.msgList = []
self.msgList.append("ああああああああああ##あああああああああああ##あああああああああああ")
self.msgList.append("いいいいいいいいいいいい##いいいいいいいいいいいい")
self.msgList.append("うううううううううううう")

 セリフは最大で3行表示し、1行分の行間を空けて表示しています。行間は恐らく可視性や濁点などのスペース確保の為だと思われます。なのでセリフを作る際は改行は「#」を2つ続けて指定します。
 ページは、msgListにappend()した数となります。上記のソースだと3回append()してるので、3ページ分ということになります。

2-3.濁点、半濁点

 「ば」や「ぱ」などの文字グラフィックを用意しようかと考えましたが、めんどくさいリソースをなるべく抑えたいためプログラムで付け足すようにしました。

濁点をどう表現するか

 濁点と半濁点のグラフィックを用意し、濁音の場合は「”」などを文字の上に付け足して画面に表示します。

2-4.ソース

 ちょっとゴチャゴチャして申し訳ないですが、ソースはこんな感じです。

■commonUtil.py
 ・getCharPoint():指定された文字のイメージリソースのX、Y座標と文字の種類を返す。
 ・drawString():文字を描画する。
■window.py
 ウィンドウクラス。ウィンドウと文字の描画、更新を行う。
■map.py
 フィールドやダンジョンなどのマップを管理するクラス。
 ウィンドウを生成する。

import pyxel
import math

from commonConst import CommonConst

CHAR_WIDTH = 8      # 文字の幅
CHAR_HEIGHT = 8     # 文字の高さ

CHAR_TYPE_0 = 0     # 文字タイプ(通常)
CHAR_TYPE_1 = 1     # 文字タイプ(濁音)
CHAR_TYPE_2 = 2     # 文字タイプ(半濁音)

"""
共通ユーティリティクラス
"""
class CommonUtil:

    """
    文字画像の座標(x, y)を取得する
    @param char 文字
    @return srcX、srcY、文字タイプ
    """
    def getCharPoint(char):
        if char == "0":
            return (CHAR_WIDTH * 0, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "1":
            return (CHAR_WIDTH * 1, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "2":
            return (CHAR_WIDTH * 2, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "3":
            return (CHAR_WIDTH * 3, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "4":
            return (CHAR_WIDTH * 4, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "5":
            return (CHAR_WIDTH * 5, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "6":
            return (CHAR_WIDTH * 6, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "7":
            return (CHAR_WIDTH * 7, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "8":
            return (CHAR_WIDTH * 8, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "9":
            return (CHAR_WIDTH * 9, CHAR_HEIGHT * 12, CHAR_TYPE_0)
        elif char == "a":
            return (CHAR_WIDTH * 0, CHAR_HEIGHT * 13, CHAR_TYPE_0)
        elif char == "b":
            return (CHAR_WIDTH * 1, CHAR_HEIGHT * 13, CHAR_TYPE_0)
        elif char == "c":
            return (CHAR_WIDTH * 2, CHAR_HEIGHT * 13, CHAR_TYPE_0)
        :
        (中略)
        :
        elif char == "あ":
            return (CHAR_WIDTH * 0, CHAR_HEIGHT * 14, CHAR_TYPE_0)
        elif char == "い":
            return (CHAR_WIDTH * 1, CHAR_HEIGHT * 14, CHAR_TYPE_0)
        elif char == "う":
            return (CHAR_WIDTH * 2, CHAR_HEIGHT * 14, CHAR_TYPE_0)
        elif char == "え":
            return (CHAR_WIDTH * 3, CHAR_HEIGHT * 14, CHAR_TYPE_0)
        elif char == "お":
            return (CHAR_WIDTH * 4, CHAR_HEIGHT * 14, CHAR_TYPE_0)
        :
        (中略)
        :
        # 濁音
        elif char == "が":
            return (CHAR_WIDTH * 5, CHAR_HEIGHT * 14, CHAR_TYPE_1)
        elif char == "ぎ":
            return (CHAR_WIDTH * 6, CHAR_HEIGHT * 14, CHAR_TYPE_1)
        elif char == "ぐ":
            return (CHAR_WIDTH * 7, CHAR_HEIGHT * 14, CHAR_TYPE_1)
        elif char == "げ":
            return (CHAR_WIDTH * 8, CHAR_HEIGHT * 14, CHAR_TYPE_1)
        elif char == "ご":
            return (CHAR_WIDTH * 9, CHAR_HEIGHT * 14, CHAR_TYPE_1)
        :
        (中略)
        :
        # 半濁音
        elif char == "ぱ":
            return (CHAR_WIDTH * 25, CHAR_HEIGHT * 14, CHAR_TYPE_2)
        elif char == "ぴ":
            return (CHAR_WIDTH * 26, CHAR_HEIGHT * 14, CHAR_TYPE_2)
        elif char == "ぷ":
            return (CHAR_WIDTH * 27, CHAR_HEIGHT * 14, CHAR_TYPE_2)
        elif char == "ぺ":
            return (CHAR_WIDTH * 28, CHAR_HEIGHT * 14, CHAR_TYPE_2)
        elif char == "ぽ":
            return (CHAR_WIDTH * 29, CHAR_HEIGHT * 14, CHAR_TYPE_2)
        :
        (略)

    """
    文字列を描画する
    @param msg 文字列
    @param px X座標
    @param py Y座標
    """
    def drawString(msg, px, py):
        msgs = msg.split("#")
        for i in range(len(msgs)):
            for j in range(len(msgs[i])):
                c = msgs[i][j]
                p = CommonUtil.getCharPoint(c)
                tmpPx = px + j * CHAR_WIDTH
                tmpPy = py + i * CHAR_HEIGHT

                # 濁点
                if p[2] == CHAR_TYPE_1:
                    pyxel.blt(tmpPx, tmpPy - CHAR_HEIGHT, CommonConst.IMAGE_BANK_OTHER,
                        CHAR_WIDTH * 10, CHAR_HEIGHT * 17,
                        CHAR_WIDTH, CHAR_HEIGHT, CommonConst.TRANSPARENT_COLOR)
                # 半濁点
                elif p[2] == CHAR_TYPE_2:
                    pyxel.blt(tmpPx, tmpPy - CHAR_HEIGHT, CommonConst.IMAGE_BANK_OTHER,
                        CHAR_WIDTH * 11, CHAR_HEIGHT * 17,
                        CHAR_WIDTH, CHAR_HEIGHT, CommonConst.TRANSPARENT_COLOR)

                pyxel.blt(tmpPx, tmpPy, CommonConst.IMAGE_BANK_OTHER,
                    p[0], p[1],
                    CHAR_WIDTH, CHAR_HEIGHT, CommonConst.TRANSPARENT_COLOR)
import pyxel

from input import Input
from commonConst import CommonConst
from commonUtil import CommonUtil

# ラグ間隔
LAG_INTERVAL = 8

"""
ウィンドウクラス
"""
class Window:

    """
    初期化
    @param px X座標
    @param py Y座標
    @param wTiles 横方向のタイル数(1タイル8ドット)
    @param hTiles 縦方向のタイル数(1タイル8ドット)
    """
    def __init__(self, px, py, wTiles, hTiles):
        self.visible = False
        self.px = px
        self.py = py
        self.msgPx = 0
        self.msgPy = 0
        self.wTiles = wTiles
        self.hTiles = hTiles
        self.pageIndex = 0
        self.msgIndex = 0
        self.dispMsg = ""
        # 最後に描画した時のフレーム
        self.lastDrawFrame = 0
        # メッセージの時間差表示フラグ
        self.isLag = False
        # メッセージリスト(要素1つ = メッセージ1ページ)
        self.msgList = []

    """
    描画処理
    """
    def draw(self):

        if not self.visible:
            return

        # ウィンドウ表示
        srcX = 0
        srcY = CommonConst.TILE_HEIGHT * 18
        for row in range(self.hTiles):
            for col in range(self.wTiles):
                if row == 0:
                    if col == 0:
                        srcX = 0
                    elif col == self.wTiles - 1:
                        srcX = 2
                    else:
                        srcX = 1
                elif row == self.hTiles - 1:
                    if col == 0:
                        srcX = 5
                    elif col == self.wTiles - 1:
                        srcX = 7
                    else:
                        srcX = 6
                else:
                    if col == 0:
                        srcX = 3
                    elif col == self.wTiles - 1:
                        srcX = 4
                    else:
                        srcX = 8

                pyxel.blt(self.px + col * CommonConst.TILE_WIDTH, self.py + row * CommonConst.TILE_HEIGHT, CommonConst.IMAGE_BANK_OTHER,
                    srcX * CommonConst.TILE_WIDTH, srcY,
                    CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT, CommonConst.TRANSPARENT_COLOR)

        # メッセージ表示
        CommonUtil.drawString(self.dispMsg, self.msgPx, self.msgPy)

    """
    更新処理
    @param inputs 入力
    """
    def update(self, inputs):

        if not self.visible:
            return

        if self.isLag:
            if CommonConst.DECISION in inputs:
                # 1ページが全て表示されている場合
                if len(self.msgList[self.pageIndex]) == self.msgIndex:
                    # 最終ページの場合
                    if self.pageIndex == len(self.msgList) - 1:
                        # ウィンドウを閉じる
                        self.close()
                        return
                    else:
                        # 次のページ
                        self.pageIndex += 1
                        self.msgIndex = 0
                else:
                    # 1行を瞬時に表示
                    msgs = self.msgList[self.pageIndex].split("##")
                    tmpLenSum = 0
                    for i, msg in enumerate(msgs):
                        # 2行改行(##)分を加算
                        tmpCount = i * 2 if i < len(msgs) else 0

                        if self.msgIndex < tmpLenSum + len(msg):
                            self.msgIndex = tmpLenSum + len(msg) + tmpCount
                            break
                        else:
                            tmpLenSum += len(msg) + tmpCount

            frameDiff = pyxel.frame_count - self.lastDrawFrame
            if not len(self.msgList[self.pageIndex]) == self.msgIndex:
                if frameDiff > LAG_INTERVAL:
                    self.msgIndex += 1
                    if (self.msgIndex >= len(self.msgList[self.pageIndex])):
                        self.msgIndex = len(self.msgList[self.pageIndex])

                    self.dispMsg = self.msgList[self.pageIndex][0:self.msgIndex]

                    self.lastDrawFrame = pyxel.frame_count
            else:
                self.dispMsg = self.msgList[self.pageIndex]
        else:
            self.dispMsg = self.msgList[self.pageIndex]

    """
    ウィンドウを表示する
    @param msgList 表示文字列
    @param px 文字列のX座標
    @param py 文字列のY座標
    @param isLag メッセージの時間差表示フラグ
    """
    def show(self, msgList, px, py, isLag):
        self.msgList = msgList
        self.msgPx = px
        self.msgPy = py
        self.isLag = isLag
        self.pageIndex = 0
        self.msgIndex = 0
        self.visible = True

    """
    ウィンドウを閉じる
    """
    def close(self):
        self.lastDrawFrame = 0
        self.msgList = []
        self.pageIndex = 0
        self.msgIndex = 0
        self.dispMsg = ""
        self.visible = False

    """
    ウィンドウを表示しているか判別する
    @return 表示フラグ(True:表示中/False:非表示)
    """
    def isShow(self):
        return self.visible
"""
マップクラス
"""
class Map:

    """
    初期化
    """
    def __init__(self):

        self.windowList.append(Window(0, 0, 20, 8))
        msgList = []
        msgList.append("ああああああああああ##あああああああああああ##あああああああああああ")
        msgList.append("いいいいいいいいいいいい##いいいいいいいいいいいい")
        msgList.append("うううううううううううう")
        self.windowList[0].show(msgList, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT * 2, True)

3.タイルアニメーション

 Pyxelで使用できるbltm()では、付属のエディタで作ったタイルマップを表示しますが、恐らくこのままではタイルをアニメーションさせることができないと思い、プログラムで再現してみました。

滝や水面の表現

 ↑↑ 物語の序盤にある滝。先にbltm()関数でタイルマップを表示し、その後でblt()関数で滝や水面のイメージを表示しています。

3-1.タイルを用意

 エディタで滝と水面のアニメーション用のタイルを用意します。

 水面を例にすると、用意した水面のタイルを以下のように右から左へ1ドットずつずらして参照します。滝も同様に下から上へとずらしていきます。
赤枠は8×8ドットです。赤枠が左へずれていき、番号で言うと「8」まで表示したら、また「1」の位置へ戻します。これを一定間隔で繰り返すことで、左から右へ流れる水面を表現できます。

 実際にソースを組むことを考えると、マップ上のタイルのうちどれが水面のタイルか調べる必要があります。
 ソースを修正して水面の部分を赤く塗りつぶすようにしてみました。
6行目が水面のタイルかどうかを判別するif文です。
 画面外にも水面や滝のタイルがありますが、アニメーションさせるのは現在表示されている画面内のみにします(下記ソースのrowとcolのfor文の部分)。
 また、今回の修正ではタイルマップを表示した上からタイルの画像を表示していますが、マップスクロール時に1フレームくらいタイルマップとタイルがズレて表示されてしまいます。なのでマップスクロール時はアニメーションしないようにします。(もっといい方法無いかな…(;´・ω・))

1  |    # タイルアニメーション
2  |    tmpTileX = CommonUtil.pixelsToTiles(self.offsetPx)
3  |    tmpTileY = CommonUtil.pixelsToTiles(self.offsetPy)
4  |    for row in range(CommonConst.SCREEN_HEIGHT_TILES):
5  |        for col in range(CommonConst.SCREEN_WIDTH_TILES):
6  |            if (pyxel.tilemap(0).pget(tmpTileX + col, tmpTileY + row) == (1, 8)):
7  |                pyxel.rect(col * CommonConst.TILE_WIDTH, row * CommonConst.TILE_HEIGHT, CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT, pyxel.COLOR_RED)
"""
共通定数クラス
"""
class CommonConst:

    SCREEN_WIDTH = 160      # 画面の幅
    SCREEN_HEIGHT = 144     # 画面の高さ
    TILE_WIDTH = 8          # タイルの幅
    TILE_HEIGHT = 8         # タイルの高さ
    SCREEN_WIDTH_TILES = 20     # 画面の幅のタイル数
    SCREEN_HEIGHT_TILES = 16    # 画面の高さのタイル数

 ↑↑ 川に沿って赤く塗りつぶされているので、判別はうまくいったみたいですね(*'▽')
 あとは塗り潰した部分をアニメーションさせる処理にすればいけそうです。

3-2.ソースを修正

 mapクラスのdraw()関数を修正します。

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

    # オフセットX座標
    offsetPx = 0
    # オフセットY座標
    offsetPy = 0

    # マップスクロールフラグ(True:スクロール中/False:停止)
    isMapScroll = False
    # スクロール方向
    mapScrollDirection = CommonConst.DIRECTION_RIGHT
    # スクロール速度
    scrollSpeed = CommonConst.TILE_WIDTH / 2
    # スクロールしたドットの合計値
    scrollSum = 0

    # タイルアニメーション用カウント
    tileAnimeCount = 0

    # 最後に描画した時のフレーム
    lastDrawFrame = 0

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

        # タイルアニメーション
        if (not self.isMapScroll):
            tmpTileX = CommonUtil.pixelsToTiles(self.offsetPx)
            tmpTileY = CommonUtil.pixelsToTiles(self.offsetPy)
            for row in range(CommonConst.SCREEN_HEIGHT_TILES):
                for col in range(CommonConst.SCREEN_WIDTH_TILES):
                    # 水面
                    if (pyxel.tilemap(0).pget(tmpTileX + col, tmpTileY + row) == (1, 8)):
                        pyxel.blt(col * CommonConst.TILE_WIDTH, row * CommonConst.TILE_HEIGHT, CommonConst.IMAGE_BANK_TILE,
                            CommonConst.TILE_WIDTH - self.tileAnimeCount, 8 * CommonConst.TILE_HEIGHT,
                            CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT, CommonConst.TRANSPARENT_COLOR)
                    # 滝1
                    elif (pyxel.tilemap(0).pget(tmpTileX + col, tmpTileY + row) == (0, 11)):
                        pyxel.blt(col * CommonConst.TILE_WIDTH, row * CommonConst.TILE_HEIGHT, CommonConst.IMAGE_BANK_TILE,
                            0, 11 * CommonConst.TILE_HEIGHT - self.tileAnimeCount,
                            CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT, CommonConst.TRANSPARENT_COLOR)
                    # 滝2
                    elif (pyxel.tilemap(0).pget(tmpTileX + col, tmpTileY + row) == (1, 11)):
                        pyxel.blt(col * CommonConst.TILE_WIDTH, row * CommonConst.TILE_HEIGHT, CommonConst.IMAGE_BANK_TILE,
                            CommonConst.TILE_WIDTH, 11 * CommonConst.TILE_HEIGHT - self.tileAnimeCount,
                            CommonConst.TILE_WIDTH, CommonConst.TILE_HEIGHT, CommonConst.TRANSPARENT_COLOR)
        :
        (略)

    """
    更新処理
    @param inputs 入力
    """
    def update(self, inputs):

        frameDiff = pyxel.frame_count - self.lastDrawFrame
        if frameDiff > 6:
            self.tileAnimeCount += 1
            if self.tileAnimeCount >= CommonConst.TILE_WIDTH:
                self.tileAnimeCount = 0

            self.lastDrawFrame = pyxel.frame_count
        :
        (略)

 update()関数でtileAnimeCountを増やし、draw()関数でアニメーションを表示します。アニメーションする間隔は、本家を見ると大体6フレームくらい?なので6フレームにしてます。

4.おわりに

 今回は長くなってしまいましたが、ご覧くださりありがとうございました!

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