1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Axmol Engine】 バイブコーディングで脱出ゲームを作る[ 13 ]:順番押し系ギミックの実装

1
Posted at

Axmol Engine で脱出ゲームを作る ── 額縁ギミック(4隅タップ)とライター取得

はじめに

今回は room1-1-1 に配置した肖像画の額縁をタップし、4隅を正しい順番でタップする割と古典的な謎解きギミックを実装します。
順番押し系のギミックに属します。
ついでに、解いた先には隠し扉(壁の窪み)とアイテム「ライター」を配置します。

この記事の、順番押し系(重複有り)+部品自身がトリガ+自動判定で、額縁タップギミックを実装します


用意した画像(Chat-GPT)

額縁近接[bg_stage_007.png]
bg_stage_007.png

額縁を外した後[bg_stage_007_2.png]
bg_stage_007_2.png

ライターが置いてある画像(上記の画像に被せるように表示)[item_007.png]
item_007.png

ライターのサムネイル[item_02_1.png]
item_02_1.png

ライターの詳細表示用[item_02_1_large.png]
item_02_1_large.png

1. 額縁へ近接

このステージの左の壁にある額縁をタップすると、額縁に近接するようにします。

スクリーンショット 2026-08-03 13.05.52.png

プロンプト:
"room1-1"のbg_stage_004.pngの左の額縁付近をタップしたら、room1-1-1のステージへ遷移。bgはbg_stage_007.png, backは room1-1。

ステージ構成を決めるjsonの構造をclaudeが掌握しているため、このような簡潔なプロンプトでも汲み取って、ステージの作成や遷移をコーディングしてくれます。

スクリーンショット 2026-08-03 13.03.07.png

2. 額縁タップギミック(type "2")の実装

プロンプト:
room1-1-1 でbg_stage_007.png内の額縁をタップしたら、gimmickに遷移。ただしポップアップ形式ではなく、room1-1-1での表示そのままで。ギミックはgimmicks.jsonにpictureFrameとして定義を追加。bg_stage_007.pngをbgとして、そのZ軸上にframe/base.pngを配置。base.pngの4隅に透過の四角領域のボタンをそれぞれ配置。左上→右下→左下→右上→左上→左下と順番にタップしたら正解。正解後はroom1-1-1のステップを進める。room1-1-1のステップ2では、bgはbg_stage_007_2.pngを表示。

既存のダイヤル錠ギミック(type: "1")は暗転オーバーレイ+frame_popup.pngのポップアップ形式でしたが、今回は「今の画面表示のまま」という要件のため type: "2" という新しいギミック種別を追加します。ダーク背景・ポップアップ枠・reload/okボタンは使わず、ギミック自身の bg をステージ背景と同じスケール・位置でフル表示し、その上に frame/base.png を重ねる構成です。

"pictureFrame": {
  "type": "2",
  "bg": "bg_stage_007.png",
  "frame": "frame/base.png",
  "frameRect": [717, 141, 426, 580],
  "touches": [
    { "rect": [717, 641, 80, 80] },
    { "rect": [1063, 641, 80, 80] },
    { "rect": [717, 141, 80, 80] },
    { "rect": [1063, 141, 80, 80] }
  ]
  "sequence": [0, 3, 2, 1, 0, 2],
  "stage": "room1-1-1",
  "stageStatus": 1,
  "goStage": "room1-1-1"
}

touches はTL・TR・BL・BRの4隅(それぞれ0〜3)、sequence は「左上→右下→左下→右上→左上→左下」を添字で表したものです。

struct GimmickDef {
    // ...
    std::string              frame;
    ax::Rect                 frameRect;
    std::vector<int>         sequence;
};
// showGimmick() 内
if (def.type == "2")
{
    auto overlay = Node::create();
    overlay->setContentSize(visibleSize);
    overlay->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
    overlay->setTag(200);
    this->addChild(overlay, 10);

    auto base = Sprite::create(def.bg);
    base->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    float scale = visibleSize.height / base->getContentSize().height;
    base->setScale(scale);
    base->setPosition(Vec2(visibleSize.width / 2, visibleSize.height / 2));
    base->setTag(250);
    overlay->addChild(base, -1);

    auto frameSp = Sprite::create(def.frame);
    frameSp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    frameSp->setPosition(Vec2(def.frameRect.origin.x + def.frameRect.size.width / 2,
                              def.frameRect.origin.y + def.frameRect.size.height / 2));
    frameSp->setTag(260);
    base->addChild(frameSp, 1);
    // 4隅のtouches判定、sequenceとの一致でgoStageへ
}

3. ガイドメッセージ帯の追加

プロンプト:
gimmicksのpictureFrameにmessageを追加して「額縁の4隅を順番通りにタップしましょう」を定義。ギミック表示時に、画面一番上に幅100%、高さは表示フォントの1.5倍で、黒い透過50%の帯を表示して、その上に白いフォントでこのメッセージを表示。

"message": "額縁の4隅を順番通りにタップしましょう"
if (!def.message.empty())
{
    constexpr float fontSize     = 28.0f;
    const float     bannerHeight = fontSize * 1.5f;
    auto banner = LayerColor::create(Color32(0, 0, 0, 128), visibleSize.width, bannerHeight);
    banner->setPosition(Vec2(0, visibleSize.height - bannerHeight));
    overlay->addChild(banner, 2);

    auto label = Label::createWithSystemFont(def.message, "Arial", fontSize);
    label->setTextColor(Color32(255, 255, 255, 255));
    label->setAlignment(TextHAlignment::CENTER);
    label->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    label->setPosition(Vec2(visibleSize.width / 2, visibleSize.height - bannerHeight / 2));
    overlay->addChild(label, 3);
}

スクリーンショット 2026-08-03 13.03.16.png

脱出ゲームではユーザーさんが何をしていいか迷わないように、このようにある程度ナビゲーションをしてあげると喜ばれます。


4. タップ時の額縁シェイク演出

プロンプト:
額縁をタップしたとき、base.pngを軽く左右に揺らしてください

プロンプト:
左右に軽く回転するように

最初は MoveBy による左右移動で実装しましたが、回転の要望を受けて RotateBy に変更しました。角のタップ判定に使うため frameSptag(260) を付与し、タップのたびに取得して再生します。

frameSp->setTag(260);
auto* frameNode = baseNode->getChildByTag(260);
if (frameNode)
{
    constexpr int   kShakeTag  = 999;
    constexpr float shakeAngle = 5.0f;
    constexpr float shakeDur   = 0.05f;
    frameNode->stopActionByTag(kShakeTag);
    frameNode->setRotation(0.0f);
    auto shake = Sequence::create(
        RotateBy::create(shakeDur, -shakeAngle),
        RotateBy::create(shakeDur, shakeAngle * 2),
        RotateBy::create(shakeDur, -shakeAngle * 2),
        RotateBy::create(shakeDur, shakeAngle),
        nullptr);
    shake->setTag(kShakeTag);
    frameNode->runAction(shake);
}

連続タップで揺れが重なっても位置がずれないよう、再生前に必ず stopActionByTagsetRotation(0.0f) でリセットします。


5. 失敗時の挙動

プロンプト:
sequencesサイズ分、タップして失敗したら、完全にroom1-1-1に戻ってください(もしくは再度遷移)

sequence の長さ分(6回)タップし終えるまでは間違えても継続し、6回使い切って不一致なら失敗という仕様にしました。失敗時は単に removeChildByTag(200) でギミックを閉じるだけでなく、_stage_stageStatus を明示的に再設定して showStageBg() を呼び、正式に room1-1-1 へ再遷移します。

auto tapHistory = std::make_shared<std::vector<int>>();
// ...
tapHistory->push_back(i);

if (!def.effect.empty())
    AudioEngine::play2d(def.effect, false);

if (!def.sequence.empty() && tapHistory->size() >= def.sequence.size())
{
    if (*tapHistory == def.sequence)
    {
        auto* ud = ax::UserDefault::getInstance();
        if (!def.saveStage.empty())
            ud->setIntegerForKey(("stageData_" + def.saveStage).c_str(), def.saveStatus);
        ud->flush();
        if (!def.effectSuccess.empty())
            AudioEngine::play2d(def.effectSuccess, false);
        this->removeChildByTag(200);
        _stage = def.goStage;
        ax::UserDefault::getInstance()->setStringForKey("stage", _stage);
        ax::UserDefault::getInstance()->flush();
        showStageBg();
    }
    else
    {
        if (!def.effectReset.empty())
            AudioEngine::play2d(def.effectReset, false);
        this->removeChildByTag(200);
        _stage = !def.saveStage.empty() ? def.saveStage : def.goStage;
        _stageStatus = 0;
        ax::UserDefault::getInstance()->setStringForKey("stage", _stage);
        ax::UserDefault::getInstance()->flush();
        showStageBg();
    }
}

入力に失敗したら、失敗したと分かりやすいようにしておくと良いです。


6. アイテム「ライター」の追加

プロンプト:
items.jsonに"2"を追加して、「ライター」という名前で、items/item_02_* の画像を設定してください

"2": {
  "name": "ライター",
  "image": "items/item_02_1.png",
  "image_large": "items/item_02_1_large.png"
}

プロンプト:
room1-1-1のステップ2で、アイテム"2"を設定。未取得時の画像はitem_007.png。

item_007.png はステージ背景と同じ1870×841のフル透過画像で、窪みの中の実際の位置にライターが描かれています。既存の item_002.png(郵便受けの鍵)と同じ設計方針で、rect は表示位置ではなくタップ判定専用です。

{
  "bg": "bg_stage_007_2.png",
  "touches": [],
  "back": "room1-1",
  "items": [
    { "image": "item_007.png", "itemNo": "2", "rect": [905, 211, 95, 110] }
  ]
}

額縁を外すとライターが、、、
スクリーンショット 2026-08-03 13.14.06.png

タップしてライターを取得
スクリーンショット 2026-08-03 13.14.16.png

オイルが無いので、火が点かないようです。。。


成果物


claudeが画像解析に使用したツール

今回、画像解析のために下記をclaudeがインストールして利用していました。

額縁やアイテムの座標をコードに落とし込む際、目視確認だけでなくピクセル単位の解析も行いました。Python 3とPillow(PIL)で Image.crop() を使い、ステージ背景の一部を切り出して ImageDraw で座標グリッドを重ねて描画することで、額縁やアイテムの正確なピクセル範囲を特定しています。Image.alpha_composite() では透過PNG(frame/base.pngitem_007.png)を背景画像に実際に合成し、算出した座標が視覚的にズレていないかを確認しました。macOS標準コマンドの sips は画像ファイルのピクセル幅・高さを素早く取得するのに使っています。

座標系の変換にも注意が必要でした。Axmol(cocos2d系)の Rect は左下原点・Y軸上向きなのに対し、Pillowで読み込んだ画像は左上原点・Y軸下向きです。bottom_left_y = 画像高さ - top_left_y - height で相互変換しながら検証しています。


まとめ

実装内容 対応ファイル
額縁4隅タップギミック(type "2") gimmicks.json / MainScene.cpp
額縁画像差し替えに伴う座標再計算 gimmicks.json
ガイドメッセージ帯 gimmicks.json / MainScene.cpp
額縁の回転シェイク演出 MainScene.cpp
失敗時のroom1-1-1再遷移 MainScene.cpp
ライターアイテムの追加 items.json / stages.json
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?