ぽんたのワナワナアドベンチャー|2D横スクロールアクションゲーム 開発記録
はじめに
はじめまして!山本翔太(shotayamamoto1016)と申します。
以前Qiitaで紹介した育成型シューティングゲーム「Sky Invaders」に続いて、
今回は約1ヶ月半かけて横スクロールアクションゲーム
「ぽんたのワナワナアドベンチャー」 を個人開発しました!
本記事では実装の工夫や苦労した点を紹介します。
ゲーム紹介
ぽんたのワナワナアドベンチャー
▶️ Unityroomでプレイ
📁 GitHubリポジトリ
「常識を捨てろ。このゲームに正解はない。」をコンセプトにした
理不尽系横スクロールアクションゲームです。
スーパーマリオブラザーズとしょぼんのアクションを合わせたような作品で、キノコを食べると死んでしまったり、土管から大量のころもちが出現したりとプレイヤーの常識を次々と裏切るゲームになっています。
ゲームの特徴
- 全5ステージ+エンディングの構成
- 20種類以上の個性的な敵キャラクター
- 裏ルート・隠し通路が各ステージに存在
- ラスボス「もち天さま」との激しいボス戦
開発の背景・きっかけ
前作「Sky Invaders」はアセットストアの素材を活用したシューティングゲームでしたが、今回はゲームプランナーを目指すうえで
「ゲームの企画・設計・実装すべてを自分の手で作り切る」 という目標のもと開発をスタートしました。
特に以下の3点をこだわりとして掲げました。
- プレイヤーが思わず「え!?」と声を上げるような理不尽な罠の設計
- 敵キャラクターそれぞれに個性と行動パターンを持たせること
- UIや演出にとことんこだわり、見た目のクオリティを高めること
また今回はキャラクター画像・背景・BGMにいたるまでGoogle AI(Gemini)・Suno AIを積極的に活用し、一人での開発でも統一感のある世界観を実現することを目指しました。
コンセプト
キャッチコピー
「常識を捨てろ。このゲームに正解はない。」
3つのコアバリュー
① 理不尽さ
見た目と効果が真逆のアイテム・罠・ギミックがプレイヤーの常識を次々と裏切ります。
「キノコ=パワーアップ」「コイン=安全」といったゲームの常識がことごとく逆転します。
② 違った思考でのゴールの目指し方
一見正解に見えるルートが実は罠で、一見おかしいルートが正解という逆転の発想が求められます。「普通にプレイする」ことをやめた瞬間に新しい道が見えてきます。
③ 発見と達成感
裏ルートの発見・ボス撃破・ゴール到達時の達成感が次のプレイへの意欲を生みます。
何度も死んで覚えて、ようやくクリアできた時の喜びを大切にしました。
使用技術
| 技術 | 用途 |
|---|---|
| Unity 6.0 | ゲームエンジン |
| C# | スクリプト全般 |
| DOTween | UIアニメーション・演出 |
| Cinemachine | カメラ追従 |
| Unity Tilemap | ステージ地形の構築 |
| TextMeshPro | UI文字表示 |
| Google AI (Gemini) | キャラクター・背景・UI素材の生成および手動加筆用 |
| Suno AI | BGMの生成・構成編集用 |
| Git / GitHub | バージョン管理 |
工夫したポイント
1. 「可愛い見た目×殺意の高い罠」のギャップ演出
① デザインの一貫性とコミカルな死亡演出
主人公「ぽんた」の可愛らしさを維持しつつ、死亡時はマリオ風の「万歳ジャンプ」で画面外まで跳ね上がるコミカルな演出を実装しました。「かわいいのに死ぬ」「理不尽なのに笑える」というギャップがプレイヤーのストレスをユーモアに変える効果を狙っています。
② 心理的盲点を突く罠設計
「キノコ=パワーアップ」というゲーマーの固定観念を逆手に取り、
毒キノコ・凍りキノコ・混乱キノコといった多彩なデバフを用意しました。
さらに「踏めそうな敵」が実は踏めない(竹の子スナイパー)など、
プレイヤーが無意識に持っている「ゲームのお約束」を次々と裏切る体験を設計しています。
2. 生成AI(Gemini / Suno AI)の制作補助としての徹底活用
本作のアセット制作には、制作効率化のための補助ツールとして生成AIを活用しています。AIによる生成素材をそのまま使用するのではなく、全アセットに対して手動での加筆・修正・編集を徹底することで、ゲームとしてのクオリティと世界観の一貫性を担保しました。
① アセット制作の効率化と「質の担保」
キャラクターのスプライト、背景、UI、BGMのベースをAIで生成しました。特に「和風」「レッサーパンダ」「可愛らしい」といったキーワードをプロンプトで統一し、世界観のブレを防いでいます。
しかし、AI生成素材には特有の課題もあり、以下の手動加工工程を全ての素材に対して行っています。
- 画像素材: 背景透過処理、AI特有の不要なアーティファクト(ロゴやノイズ)の除去、歩行アニメーションの一貫性を保つためのパーツ単位での描き込み・合成、Unityでの利用に最適化したレイアウト調整。
- 音声素材: Suno AIで生成した楽曲の構成編集、ゲームに合わせたシームレスなループ再生のための波形カット、各シーンに合わせた音量バランスの最適化。
単なる「自動生成」に留まらず、開発者の手で細部を調整することで、一人開発でありながら統一感のあるビジュアルを実現しました。
② 世界観に合わせたUIの独自開発
スライダーのつまみを「ぽんたの手」にしたり、音量調整バーを「竹筒」で表現したりと、既存の汎用UIキットを使わず、世界観に合わせた素材を自作・加工しました。
「和風×理不尽×かわいい」というゲームのトーンをUIの隅々まで徹底させるため、AI生成素材をパーツとして分解し、Unity上で再構築する手法をとっています。
3. ユーザー体験(UX)への配慮
① リトライの快適さ
理不尽なゲーム性だからこそ、
死んだ瞬間の暗転からチェックポイント復帰までのテンポを最速化しました。
CinemachineのForceCameraPositionを活用し、
復活直後のカメラ遅延によるストレスを排除しています。
IEnumerator ResetCamera(Transform playerTransform, Vector3 targetPos)
{
vcam.Follow = null;
vcam.ForceCameraPosition(cameraTargetPos, Quaternion.identity);
yield return null;
vcam.Follow = playerTransform;
yield return null;
yield return null;
vcam.ForceCameraPosition(cameraTargetPos, Quaternion.identity);
}
② 情報の可視化
未開放ステージの木札をRGB(125, 125, 125)で暗くする処理や、
ストーリーのタイピング演出など、視覚的に状況を伝える工夫を随所に盛り込みました。
「プレイヤーが迷わないUI設計」を意識して開発しています。
4. IResettableインターフェースによるステージリセット管理
チェックポイントから復活する際、各オブジェクトを初期状態に戻す必要がありました。
最初はStageResetManagerが個別のクラスを直接FindObjectsByTypeで探してリセットしていましたが、
敵やブロックの種類が増えるたびにStageResetManagerを修正しなければならず、管理が大変でした。
そこでIResettableインターフェースを導入しました。
public interface IResettable
{
void ResetObject();
}
各スクリプトにIResettableを実装するだけで、
StageResetManagerが自動的に検出してリセットしてくれるようになりました。
// StageResetManager.cs
MonoBehaviour[] allObjects = FindObjectsByType<MonoBehaviour>(
FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var obj in allObjects)
{
if (obj is IResettable resettable)
{
resettable.ResetObject();
}
}
このパターンにより新しい敵やギミックを追加する際も
IResettableを実装してResetObject()を書くだけで
StageResetManagerを一切変更せずに済むようになりました。
また「うらめし提灯」はチェックポイント通過前に倒した場合は復活しない仕様のため、
CheckpointManagerに倒した提灯のIDをHashSetで記録し、
復活時にその情報を参照して個別に制御しています。
// チェックポイント通過時点での倒済みIDを記録
public void SetCheckpoint(Vector3 position, bool isBoss = false)
{
checkpointPosition = position;
hasCheckpoint = true;
isBossCheckpoint = isBoss;
// スナップショット保存
chochinIdsAtCheckpoint = new HashSet<int>(defeatedChochinIds);
}
5. MainLoopパターンによる敵の行動管理
マキコミガエルや溶岩もっちのような「決まったパターンを繰り返す敵」は
MainLoopコルーチンで管理しています。
// MakikomiFrog.cs
IEnumerator MainLoop()
{
while (!isDead)
{
yield return StartCoroutine(IdleSequence()); // 待機
yield return StartCoroutine(JumpSequence()); // 1回目ジャンプ
yield return new WaitUntil(() => isGrounded);
yield return StartCoroutine(JumpSequence()); // 2回目ジャンプ
yield return new WaitUntil(() => isGrounded);
yield return StartCoroutine(TongueAttack()); // 舌攻撃
}
}
yield return StartCoroutine()で各アクションが完全に終わるまで
次のアクションに進まないため、タイミング管理がシンプルになります。
6. ラスボス「もち天さま」の状態管理
ラスボスは以下のような複雑な状態遷移を持っています。
Waiting → Rising → Floating
↓(トゲ攻撃間隔経過)
SpikeAttack(ランダムにブロックからトゲ発動)→ Floating
↓(時間経過)
StompAttack(じらし→踏みつけ)→ Stunned(攻撃チャンス)
↓(踏まれた場合)
Hit(混乱アニメーション)→ Rising → Floating
↓(3回踏まれたら)
DefeatSequence(撃破演出)
HP段階(3→2→1)に応じてステータスが変化し、
踏みつけ攻撃の連続回数も変化します(HP3:1回 / HP2:2回 / HP1:3回)。
isDeadフラグを各コルーチンの要所に配置することで、
撃破演出が他のコルーチンに割り込まれないよう工夫しました。
// 各コルーチン内で
if (isDead) yield break;
苦労したポイント
1. AI画像の一貫性とクオリティ維持
歩行アニメーションの生成
1枚の画像から「右足前」と「左足前」の歩行コマを
別々に生成させるのが非常に難しく、
同じキャラクターなのに全く別のデザインになって返ってきてしまうことが多々ありました。
最終的に「2枚同時アップロードして差分だけ変えるよう指示する」
「ペイントでの左右反転」「UnityのSpriteRenderer.FlipXを活用する」
といったアナログとデジタルを組み合わせた手法で解決しました。
2. Unity 6(6000.0)への適応
① 物理エンジンの仕様変更
Rigidbody2D.velocityが非推奨になり、
最新のlinearVelocityへの書き換えが必要になりました。
また一部APIの挙動が変わっており、
既存のサンプルコードがそのままでは動かないケースに何度も遭遇しました。
② WebGLビルドの難航
ビルド時のメモリ不足(OutOfMemory)や、
プロジェクトフォルダ名にスペースが含まれていることによるパスエラーなど、
最新環境特有の挙動に悩まされました。
一つずつログを丁寧に解析して解消することで、
デバッグスキルが大きく向上しました。
3. 物理演算と当たり判定の精度との格闘
① ゴースト衝突(ブロックの継ぎ目での引っかかり)
Tilemapを使ってブロックを並べると、
ブロックの継ぎ目にプレイヤーが引っかかって止まってしまう現象が発生しました。
対策として以下の2点を組み合わせて解決しました。
-
Box Collider 2D→Capsule Collider 2Dへの変更(角の丸み) - 摩擦係数0の物理マテリアル(No Friction)を全コライダーに適用
② 踏みつけ判定の複雑さ
横から当たったか上から当たったかの判定にContactPoint2D.normalを使っていますが、
斜めから接触した際に意図しない判定が起きることがありました。
無敵状態・混乱状態・各敵ごとの特殊ルールも絡んでくるため、
PlayerControllerのOnCollisionEnter2D内で
条件分岐を丁寧に整理することで安定した動作を実現しました。
void OnCollisionEnter2D(Collision2D collision)
{
if (isDead) return;
if (collision.gameObject.CompareTag("Enemy"))
{
if (isInvincible) { Destroy(collision.gameObject); return; }
// 各敵ごとのスキップ判定
if (collision.gameObject.GetComponent<OtasukeTurtle>() != null) return;
if (collision.gameObject.GetComponent<MakikomiFrog>() is { isDizzy: true }) return;
foreach (ContactPoint2D contact in collision.contacts)
{
if (contact.normal.y > 0.5f) return; // 上から踏んだ場合はスキップ
}
Die();
}
}
4. 竹の子スナイパーの山なり弾
弾を「山なり」に飛ばす処理が思ったより難しく、
重力スケールや初速度の計算を何度も調整しました。
最終的に以下のアプローチで解決しました。
void ShootBullet()
{
float vx = direction.normalized.x * bulletSpeed;
float vy = Mathf.Abs(direction.normalized.y * bulletSpeed) + bulletHeight;
bulletRb.linearVelocity = new Vector2(vx, vy);
}
弾のGravity ScaleとbulletHeightの値を組み合わせて調整することで、
プレイヤーの頭上を通過するような自然な放物線を実現しました。
5. ふすまの転移処理
同一シーン内のふすまA→ふすまBへの転移で、
転移直後に逆のふすまBからすぐ戻ってしまう問題がありました。
StartCooldown()メソッドで転移先のふすまに
一定時間のクールタイムを設けることで解決しました。
// 転送先のふすまにクールタイムを設定
Fusuma destinationScript = destinationFusuma.GetComponent<Fusuma>();
if (destinationScript != null)
{
destinationScript.StartCooldown(cooldownAfterArrival);
}
ゲームサイズと開発規模
| 項目 | 数値 |
|---|---|
| 総スクリプト数 | 約40ファイル |
| ステージ数 | 全5ステージ+エンディング |
| 敵キャラクター種類 | 20種類以上 |
| 開発期間 | 約1ヶ月半 |
おわりに
今回の開発を通じて以下のことを学びました。
- インターフェースを使った拡張性の高い設計(IResettable)
- Coroutineを使った複雑な状態管理(ボス戦・敵のMainLoop)
- AIツールを活用したアセット制作の効率化
- Cinemachineによる本格的なカメラ制御
- Unity 6最新環境への適応力
前作「Sky Invaders」よりもゲームの規模が大きく
実装の難しさも格段に上がりましたが、
完成した時の達成感はひとしおでした。
ゲームプランナーを目指して
これからも作品作りを続けていきます!
ぜひプレイしてみてください!


