いのべーしょん2007をGodotに移植してみた

15年以上前の作品ですが、「いのべーしょん2007」という、もともとは D言語+SDLで作られた探索型アクションゲームを Godot Engine に移植してみました。

いのべーしょん2007 for Godot について

ソースコード

ソースコードは Github にアップロードしています。(2023.7.16 時点での最新版の Godot 4.1 を使用)

Windows版のみ実行ファイルを用意しているので、ビルドせずにゲームをプレイすることができます。

ライセンスについて

特に権利上問題となるデータはないはずなので、自由に使ってもらったり改変してもらってOKだと思います。

 本ゲームは「みんなで楽しく」やわらかいです。

 本ゲームを改造したりイジったバージョンを配布しても、何らOKです。改造たのしーい!
 改造したところや改造した人の名前を、分かりやすいところに書いておくと
 いいかもしれません。

 ナイスな改造が出来たら メールなどで教えて。俺にも遊ばせてください。

 改造とか一切してないものは好きにコピーして配布しちゃって結構結構。

readme_jp.txtより

当時「やわらかライセンス」というのが提唱されていたので、このコードとデータもそれに準拠します。(Github上では MITライセンスとしています)。

オリジナル版

オリジナル版の開発者である おめが( ゜ヮ゜)ノ@Omegamega さんのWebサイトは以下となります。

どんなゲームであるかは、Web版にも移植されているので、それをプレイするのが早いと思います。

ランカーモード専用曲

高難易度モードの「ランカーモード」では、16次元レコードから Gone square > Robiopsys をお借りしてます。

16次元レコード > Gone square > Robiopsys

原曲は 165BPM ですが、探索ゲーム用としてはビートが速すぎるので、144BPM に調整しました。

おまけ:いのべーしょん2007のメイン曲耳コピ

今作では採用しなかったですが、メインテーマ曲を耳コピしてアレンジした BGMを添付しておきます。

素材として使って問題ないです。(BPM140 – 28小節)

オリジナル版との違いについて

オリジナル版との違いは以下のとおりです。

  • 実績の実装:特定の目標(例えば収集アイテムをコンプリートする)と実績リストが更新されます
  • オプション画面:オープニング・エンディングの自動スキップのON/OFF、クイックリトライ、BGM/SEの音量の設定などができます
  • ランカーモードの調整:ランカーモードとは、プリンス・オブ・ペルシャのように、高いところから落下でダメージまたは即死する高難易度モードです。ただ、ダメージ基準がわかりにくいように感じたので補助線表示機能を実装しました。またBGMもランカーモード専用の楽曲としています

基本的なゲームデザインはオリジナル版そのままで、オリジナル版に入っていた “memo.txt” に色々と遊ばせるためのアイデアが書いてあったので、それを参考に実績を作った流れです。

ただコリジョン周りでオリジナル版通りに再現できない部分がいくつかあって、そのあたりがオリジナル版と異なります。例えばオリジナルだと1マスの横穴に入ることができないのですが、Godot版だと入ることができてしまうため少し難易度が下がっています。

開発期間

平日は通勤電車の中や仕事の休憩時に少しずつ進めつつ、土日にがっつり作業して、おおよそ2週間ほどかかりました。

正確にメモは取っていないですが、内訳としてはおおよそ以下のとおりです。

  • オリジナル版の画像を調整しながら、タイルマップを作成(3日)
  • プレイヤーやコリジョン周り、ダメージ判定の実装(2日)
  • アイテム用UIやライフゲージ、クリア判定の実装(3日)
  • 実績周りの実装と調整(3日)
  • ランカーモードの実装と調整(1日)

「いのべーしょん2007」の移植で得たもの

移植のきっかけ

そもそもなぜ移植をしようと思ったのかというと、最近 Metroidvania (メトロイドヴァニア) について調べていて、以下の記事を書いたりしました。

メトロイドヴァニアのゲームデザインまとめ

色々調べたことでメトロイドヴァニアのゲームデザインについてはだいたいわかったのですが、では何を作るべきか…と考えたところ、探索型アクションゲームの「いのべーしょん2007」があったことを思い出して、勉強がてらに移植することにしました。

いのべーしょん2007のゲームデザインについて

オリジナル版に含まれる “after.txt” を見ると以下のように書いてあります。

今回のバランスコンセプトは「誰でもクリアできる」と「知識が勝敗を決する」です。

「誰でもクリア」という点では、アクションの特性を利用できたかなぁ。アクションゲームの任意スクロールということは「制圧(戦闘)と進行(スクロール)」のループ構造。その途中に休憩を入れることで、制圧時にプラス補正が付けられるようにする。今回のゲームで戦闘はありませんでしたが、ダメージを導入し、時間回復することで一応達成できたかなと考えています。

「知識が勝敗」の方は、もう隠し通路と隠しブロックと隠し(変態)システムでカバー。あまりその手の知恵が無かったんで、紫雨飯店さんのチェルシーさん(略)をものすごい勢いでパクったようなマップ構成になりました。ごめんなさいorz
あとは、スタート地点近くに隠しアイテムを置くことで、リトライしやすいようにしたりとか、多めにポーション置いたりとかー…

(中略)

あとは、ポーションなしでいける場所を制限したり。リリース後のプレイ感想で「いける場所が広すぎて迷った」みたいな話があったので。そこは制限をかけてみました。とはいえ、二段ジャンプで縛るしかないのですが。

after.txtより引用

本作のアップグレードは「ジャンプ回数の上昇」「最大ライフの上昇」の2つのみというシンプルなメカニクスです。

システムだけ聞くと「それだけの要素で面白くなるのか…?」と思いますが、実際にプレイしてみるとレベルデザインが巧妙で、繰り返し遊ばせる仕組みが良くできています。

クリアするだけなら簡単ですが、アイテムを全回収したり、最短ルートを見つけようとすると繰り返しのプレイが必要になる…、といったバランス調整がしっかりまとまっています。開発期間が限られていたこともあって、既存のゲームのアイデアをうまく流用してデザインしたとことです。

私の方で全体マップの構成をまとめたのが以下の画像です。

真っ黒が移動不可な場所で、灰色が「初期ジャンプ力(=1段ジャンプ)」で移動可能な場所です。そして水色の部分は「隠し通路を見つけないと行けない場所」で、緑色は「2段ジャンプが必要」な場所です。

灰色・水色がおおよそ同じくらいで、緑色がそれよりも少し少なめ…という配分でしょうか。「初期能力で移動可能な場所を制限した」という考えがこのあたりに反映されていそうです。

なお、マップ全体の広さはおおよそ「108 x 52」です。一画面に入るタイル数は「20 x 16」なので、横が 5画面ぶん、縦が3.25画面くらいでしょうか。この広さで10分〜30分くらいは迷う (さらに全体を把握するにはもう少し時間がかかる) ので、マップの広さを決めるときにはこの情報が役立つかもしれません。

そしてmemo.txt にはアイテム配置についての考え方も書かれています。

・配置を考える
 アイテム総数
  ふじ4種、たか4種、なす4種、くそげー6種。
  パワーアップ2個 の予定。
  
  結局、パワー3個、隠し1個で計22個。
 
 マップ構成
  (0)パワーアップなしで行ける領域
  (1)パワーアップ1で行ける領域
  (2)パワーアップ2、または、パワーアップ1+変態テクで行ける領域
  (3)変態テクでしか行けない領域。
  (4)隠しフィールド。

memo.txtより引用

先程図にしたように、レベルの作成方法として移動可能なエリアを明確に分けて設計していくのが、探索型アクションゲームを作る上でやはり大切なのは間違いなさそうです。

一方通行で流れを作る

マップを分析してみたわかったのですが、マップには何らかの流れがあります。ここで言う流れとはプレイヤーが進みたくなる方向で端的に言うと「一方通行」です。「いのべーしょん2007」における一方通行は「ベルト床」や「落下した先からジャンプで戻れないような場所」で構成されています。

この図の黄色の矢印が「一方通行」に該当する部分です。この部分をプレイヤーが引き返すことはできません。

そして私のプレイした印象ですが、おおよそこのような方針で探索をしたくなります。

緑の矢印が追加した部分です。なんとなくですが、狭い一本道を見つけるとそこを通りたくなる印象を受けました。

それと狭い道を抜けた後にアイテムがあると、その先に行きたくなる気がします。

このあたりの心理の説明は難しそうですが、結果として、基本的にスタート地点を基準に時計回りでぐるぐる回る…という流れになっています。このように探索型はどのようにプレイヤーを誘導させたいのかを「一方通行」「アイテムの配置」「一本道」などで流れを作ることができるのかもしれない…というような気がしました。

おおまかに構造を決める

レベルデザインとしては基本かもしれませんが、大きなエリアでレベルを構造化するのもレベル設計をやりやすく方法になるかもと思いました。

小さな部屋はいくつかありますが、大きくは最上階エリア、中間のエリア、最下層のエリアと、3つの大きなエリアに分かれているようです。

バックトラックについて

探索型アクションゲームのレベルデザイン特有の要素として Backtrack (バックトラック) というものがあります。バックトラックとは「来た道を引き返す」という意味ですが、いのべーしょん2007については「隠れた道を探す」という遊びに集約されている印象です。

移動系のアップグレードが「ジャンプ回数の上昇」のみなので、このあたりの遊びの幅を増やしたい場合には、別のアップグレードが必要となりそうです。

今作の「いのししは止まれない」というキャラクターコンセプトを活かすアイデアを少し考えてみました。

  • ジャンプ台:ジャンプ力アップのギミックを配置する
  • Dive: 空中から急降下して、落下地点のオブジェクトを使って何かをする。例えばジャンプ台を踏みつけるとジャンプ力アップ。着地先のブロックを破壊できる、など
  • ダッシュ:加速してジャンプ距離を延ばす。ダッシュでベルト床を通過できる。またはダッシュで特定のブロックに衝突するとそれを破壊できる、スパイクを破壊できる、ダッシュで木を倒して橋にする、など
  • 移動床:常にプレイヤーが移動するので、横幅が長め床がいいかもしれない
  • ちくわ床:プレイヤーが乗ると落下する床。一定時間で復活する
  • 重力反転:特定のエリアだけ重力が逆転する。またはスイッチにより重力反転の有効・無効を切り替えられる
  • スイッチ+扉:スイッチを押すと対応する扉が一定時間だけ開く
  • 水中:横方向の移動力低下、空中ジャンプできない。ただしジャンプ力はアップする

Godot Engine周りで得た知見

タイルマップ周り

今回はタイルマップ周りを色々と使って、知識が身についたので紹介です。

なお基本的な使い方(カスタムデータやタイルの置き換え、Physics Layerの設定など)は以下の記事に書いています。

【Godot4.x】タイルマップの基本的な使い方

アニメーションの付け方

タイルマップにはアニメーションをつけることも可能です。

方法はタイルセットから、対象のリソースを選び対象のタイルを選んだ後、「選択 > アニメーション」で Frames の部分を増やすとアニメーションができます。

アニメの速さは「速さ」の値を大きくすると速くなります。なお縦方向のパターンでなければいけないような印象です。

一方通行床

以下のような One-Way-Platform (一方通行床) ですが、コリジョンのパラメータで設定することが可能です。

タイルに「物理 > Physics Layer」という項目がある(※事前に Physics Layerの追加は必要です)ので、ここから Polygon のところにある “One Way” にチェックを入れれば一方通行床となります。

ただ困ったのが、この一方通行床からの飛び降りです。対処方法として、コリジョンレイヤーを複数用意して、一方通行床から飛び降りた場合は、コリジョンレイヤーのビットをOFFにすることで対処しました。

具体的には “Player.gd” の以下のコードです。

## コリジョンレイヤーの更新.
func _update_collision_layer() -> void:
	var oneway_bit = Common.get_collision_bit(Common.eCollisionLayer.ONEWAY)
	if _is_fall_through:
		# 飛び降り中なのでビットを下げる.
		collision_mask &= ~oneway_bit
	else:
		collision_mask |= oneway_bit

そしてさらなる問題として、Godot のコリジョンシステムではコリジョンレイヤーのビット切り替えは良くないのか、以下のように “is_on_floor” が落下中でも trueとなる現象が発生しました。

どうも横に移動し続けると、move_and_slide() が壁を床と判定してしまっているためなのか、この問題が発生しているようです。結局は一方通行床からの飛び降り時は横移動を無効にすることで対処しました。

	elif _check_fall_through():
		# 飛び降り開始.
		_set_fall_through(true)
		# X方向の速度を0にしてしまう.
		# ※これをしないと is_on_floor() が falseにならない.
		velocity.x = 0
		return

「いのししは止まれない」というコンセプトに反してしまうのですが、ゲームデザインに大きなインパクトはない(なくはないけれども許容範囲)ということでこの対処としました。

もう少し詳しい説明については、別途記事を書きました。

【Godot4.x】タイルマップに一方通行床を設定する方法

サウンド周り

BGM/SEの音量設定対応

テスト中は音を小さくしてテストプレイをしたいために、BGM/SEの音量設定を対応しました。実装は簡単でまずはオーディオの Layout Bus を追加していきます。

そして AudioStreamPlayer に Bus の名前を設定しておきます。(プログラムで細かく制御したので、あえて GDScriptで生成しています)。

		# AudioStreamPlayerをあらかじめ作っておく.
		## BGM.
		if _bgm == null:
			_bgm = AudioStreamPlayer.new()
			add_child(_bgm)
			_bgm.bus = "BGM"
		## SE.
		for i in range(MAX_SOUND):
			var snd = AudioStreamPlayer.new()
			#snd.volume_db = -4
			snd.bus = "SE"
			add_child(snd)
			_snds.append(snd)

あとは AudioServer.set_bus_volume_db() で音量を設定します。

なお、dBという単位は “0.0” がデフォルトの音量です。間違ってこの値をプラス方向に大きくしてしまうと「スピーカーが壊れるくらいの大音量(音割れノイズ)になってしまう」可能性があるので、ちゃんと実装できるまでは PCの音量を小さめでテストすることをおすすめします…。(Masterの Bus に “Limiter” エフェクト(音量を制限するエフェクト)を刺しておいても良いかもしれません)。

var bgm_volume:float = 1.0:
	get:
		return bgm_volume
	set(v):
		var db = 64.0 * (v - 1)
		AudioServer.set_bus_volume_db(eAudioBus.BGM, db)
		bgm_volume = v
var se_volume:float = 1.0:
	get:
		return se_volume
	set(v):
		var db = 64.0 * (v - 1)
		AudioServer.set_bus_volume_db(eAudioBus.SE, db)
		AudioServer.set_bus_volume_db(eAudioBus.SE2, db)
		se_volume = v
追記:2023.7.21

今回、音量は 0.0dB〜-64dB のリニアで計算してしまいましたが、正確には dB(デシベル) の計算は常用対数 (=log(10))で行われるようです。

公式ドキュメントにもデシベルスケールについての説明があります。

  • デシベル(dB)スケールは相対的なスケールです。これは、比率の基数10対数の20倍(20 × log10(P/P0))を使用して、サウンドパワーの比率を表します。
  • 6 dBごとに、音の振幅は2倍または半分になります。 12 dBは4倍、18 dBは8倍、20 dBは10倍、40 dBは100倍などを表します。
  • スケールは対数であるため、真のゼロ(オーディオなし)は表現できません。
  • 0 dBは、デジタルオーディオシステムで可能な最大振幅です。この制限は人間の制限ではなく、サウンドハードウェアの制限です。0 dB未満で適切に表現するには高すぎる振幅のオーディオは、クリッピングと呼ばれる一種の歪みを生み出します。
  • クリッピングを避けるために、サウンドミックスはマスターバスの出力(後で詳しく説明します)が0dBを超えないように配置する必要があります。
  • 0dBの限界を6dB下回るごとに、音エネルギーは半分になります。つまり、-6 dBの音量は0dBの半分の大きさです。-12 dBは-6dBの半分の大きさです。
  • デシベルで作業する場合、音はもはや-60 dBから-80 dBの間で聞こえないと見なされます。これにより、作業範囲は一般的に-60 dBから0 dBになります
GODOT DOCS > Audio buses

追記: 2023.8.3 (db_to_linear / linear_to_db について)

db_to_linear() / linear_to_db() という関数を使うと “0.0 〜 1.0” の値を dB に相互変換してくれるようです(※未確認)

  • db_to_linear(dB): dBを 0.0〜1.0 に変換
  • linear_to_db(linear): 0.0〜1.0 を dBに変換

BGMのダッキング対応

細かいところですが、「いのべーしょん2007」はアイテムゲット時にアイテムUIを表示して BGM を止める、という演出をしているのですが、オプションでアイテムUIを非表示にしたときにBGMとSEが重なってしまうため、BGMを停止せずにダッキングで音を小さくするようにしました。

設定としては簡単で「Compressor」エフェクトを追加して、”Sidechain” の項目に対象となる Bus を指定するとできるようになります。

詳しくは以下の記事で解説しています。

【Godot4.x】サイドチェインの設定方法

UI周り

UIとキーボード入力の共存方法

地味に悩んだのが、UI (Controlノード派生)の入力と、キーボード入力の共存です。

例えばタイトル画面では「ランカーモードの切り替え」に “CheckButton” を使っていて、これにフォーカスが当たっていると Spaceキーを切り替えボタンと認識して、正常に動作しなくなります。(正確には、CheckButtonの切り替え後に Spaceキー入力判定が行われ、意図した設定でなくなってしまう)。

この問題を回避するために toggled シグナルで、release_focus() を呼び出すことで、UIにフォーカスを取られないようにしました。

## ランカーモード切り替え.
func _on_check_lunker_toggled(b: bool) -> void:
	Common.is_lunker = b
	Common.to_save()
	# SPACEキで切り替わってしまうの、フォーカスを外す.
	_check_lunker.release_focus()

カメラの不具合

どうしても直せなかったのが、ゲーム開始時にカメラが一瞬別の場所を写してしまうことですね…。

正直微妙なので直したかったのですが、色々試して分からなかったので保留にしています…。