【Pyxel】レトロRPG制作メモ(2024/6/18)
空いた時間でコツコツ作ってます。
今回はマップとイベントスクリプトについて書こうと思います。
1.マップ
以前に書いた記事で、Pyxel付属のエディタではフィールドマップを再現しきれない問題について触れました。
何か良い方法は無いかと調べましたが、有力そうな情報は得られず。
他の制作者さんたちの記事を見ていたところ、こちらの記事を見つけました。
「これは…っ!」と早速エディタをダウンロードし、フィールドマップを作り直しました。
使い勝手が良く、すぐに操作に慣れました。マウスホイールをクリックしながら動かすと編集マップが縦横斜めと自由にスクロールするのがめっちゃ楽(´ー`)
マップにマウスを当てると左下のフッターに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
この記事が気に入ったらサポートをしてみませんか?