0
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】 バイブコーディングで脱出ゲームを作る[ 14 ]:絵合わせ系+別ボタンギミック(時計)の実装

0
Posted at

はじめに

前回の記事では額縁の4隅タップギミックとライターの取得を実装しました。今回は room1-3 の置き時計をタップして時計部屋(room1-3-1)に入り、時計盤の針を8時20分に合わせるギミックを解いて、隠されたアイテム「ライターオイル」を取得するまでを実装します。

この記事の、絵合わせ系+トリガを別ボタン+判定ボタンで、時計ギミックを実装します

  • 絵合わせ系 ... 8時20分という見た目(絵)に合わせる
  • トリガを別ボタン ... 時計の針自体がボタンだったとしてタップさせようとすると、もう一つの針へのタップと干渉してしまうため
  • 判定ボタン ... 自動判定ですと、短針が正しい位置にある時に、長針を20分まで移動すると強制的に正解になってしまうので、判定ボタンを配置

用意した画像(Chat-GPT生成)

画像 用途
bg_stage_006_2.png 時計近接用
bg_stage_006_3.png 時計ギミック用背景画像
btn_hour.png 短針を進める用のボタン画像
btn_minute.png 長針を進める用のボタン画像
hourhand.png 短針の画像(これを回転させて時間を示します)
minutehand.png 長針の画像(これを回転させて時間を示します)
bg_stage_006_4.png まだ開いていない状態の振り子の扉
bg_stage_006_5.png ギミック正解により開いた状態の振り子の扉
item_006.png ライターオイルが振り子の下に置いてあることを示す画像
まだ所持していない状態の時に上記の画像に被せて表示
item_03_1_large.png アイテム詳細ポップアップ用のライターオイルの画像
item_03_1.png サムネイル用のライターオイル画像

1. 時計部屋への入室導線

プロンプト:
"room1-3" で中央の時計をタップしたら、"room1-3-1" に遷移。bgはbg_stage_006_2.png。backはroom1-3。

claudeはこのプロジェクトをだいぶ学習してきてますので、このような重要なキーワードだけでも理解してくれます。

bg_stage_006.png をピクセル解析して置き時計本体の位置を特定し、タップすると room1-3-1bg_stage_006_2.png)へ遷移するタップ領域を追加します。

"room1-3": [
  {
    "bg": "bg_stage_006.png",
    "touches": [
      { "rect": [875, 211, 130, 360], "stage": "room1-3-1" }
    ],
    "left": "room1-1"
  }
]

スクリーンショット 2026-08-04 12.15.31.png
ちょっとだけ寄りました


2. 時計下部(振り子)のタップ領域

プロンプト:
"room1-3-1" で時計の振り子のある箇所をタップしたら、"room1-3-2" に遷移。bgはbg_stage_006_4.png。backはroom1-3-1。

"room1-3-1": [
  {
    "bg": "bg_stage_006_2.png",
    "touches": [
      { "rect": [840, 191, 180, 360], "stage": "room1-3-2" }
    ],
    "back": "room1-3"
  }
],
"room1-3-2": [
  {
    "bg": "bg_stage_006_4.png",
    "touches": [],
    "back": "room1-3-1"
  }
]

スクリーンショット 2026-08-04 14.13.04.png
まだ開かないようです・・・


3. 時計の針を合わせるギミック(type "3")の実装

プロンプト:
room1-3-1 で時計盤をタップしたら、ポップアップ無しのギミックに遷移。時計盤の右に、上からclock/btn_hour.png, clock/btn_minute.png, btn_ok.png を配置。clock/hourhand.png, minutehand.pngを針がそれぞれ12時を指すように配置。btn_hour.pngがタップされれば1回毎にhourhand.pngを時計回りに30度回転。btn_minute.pngがタップされれば1回毎にminutehand.pngを時計回りに30度回転。btn_ok.pngがタップされれば答え合わせ。さらに、時計盤左にbtn_reload.pngを配置して、タップされればhourhand.png, minutehand.pngの位置をリセット。答えは hourhand = 8時、minutehand.png = 20分。

長々と重要キーワードとその動作を簡潔に指示しても、ある程度形にしてくれます

type: "1"(ポップアップ式ダイヤル錠)・type: "2"(額縁の4隅タップ)に続き、type: "3" として時計の針合わせギミックを追加します。ポップアップ演出は使わず、ギミック自身の bg をフル表示し、その上に針とボタンをZ軸で重ねる構成です。

針の回転はタップ回数をカウントして setRotation(count * 30) で絶対角度指定する方式にしました。reloadでカウントごとリセットできるほか、状態管理がシンプルになります。

if (hourBtnRect.containsPoint(localBase))
{
    *hourCount = (*hourCount + 1) % 12;
    auto* hand = baseNode->getChildByTag(270);
    if (hand)
        hand->setRotation(*hourCount * 30.0f);
    return true;
}
// minuteBtnRect も同様に30度刻みで回転
if (reloadBtnRect.containsPoint(localBase))
{
    *hourCount = 0;
    *minuteCount = 0;
    baseNode->getChildByTag(270)->setRotation(0.0f);
    baseNode->getChildByTag(271)->setRotation(0.0f);
    return true;
}
if (okBtnRect.containsPoint(localBase))
{
    if (*hourCount == def.collectHour && *minuteCount == def.collectMinute)
    {
        // 正解処理へ
    }
    return true;
}

30度刻みなので、8時は8タップ(240度)、20分は文字盤の「4」の目盛り=4タップ(120度)で表現できます。

"clockHands": {
  "type": "3",
  "bg": "bg_stage_006_2.png",
  "hourHand": "clock/hourhand.png",
  "minuteHand": "clock/minutehand.png",
  "clockCenter": [930, 651],
  "collectHour": 8,
  "collectMinute": 4,
  "touches": [],
  "stage": "room1-3-1",
  "stageStatus": 1,
  "goStage": "room1-3-1"
}

ボタン配置は clockCenter からの相対オフセットで計算し、右列(hour→minute→ok)と左(reload)をコードで組み立てます。

constexpr float btnGapX = 250.0f;
constexpr float btnGapY = 110.0f;

auto makeClockButton = [&](const std::string& image, const Vec2& pos) -> Rect {
    auto sp = Sprite::create(image);
    sp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    sp->setPosition(pos);
    base->addChild(sp, 1);
    auto cs = sp->getContentSize();
    return Rect(pos.x - cs.width / 2, pos.y - cs.height / 2, cs.width, cs.height);
};

Rect hourBtnRect   = makeClockButton("clock/btn_hour.png",   Vec2(cx + btnGapX, cy + btnGapY));
Rect minuteBtnRect = makeClockButton("clock/btn_minute.png", Vec2(cx + btnGapX, cy));
Rect okBtnRect     = makeClockButton("btn_ok.png",            Vec2(cx + btnGapX, cy - btnGapY));
Rect reloadBtnRect = makeClockButton("btn_reload.png",        Vec2(cx - btnGapX, cy));

4. ギミック背景をズーム画像に切り替え、レイアウトを再調整

ギミック時の背景画像をしていなかったので、改めて指示しました。AIを使わずに修正するとなるとモチベーションが下がる修正ですが、この辺りもしっかりやってもらえます。

プロンプト:
ギミック時はbg_stage_006_3.pngを背景として使用。それに合わせてボタン位置、サイズを調整。

bg_stage_006_3.png は時計盤を大きくズームした構図のため、clockCenter をピクセル解析で再計測し、ボタンの間隔とスケールも拡大します。

"bg": "bg_stage_006_3.png",
"clockCenter": [922, 410],
constexpr float btnGapX  = 440.0f;
constexpr float btnGapY  = 220.0f;
constexpr float btnScale = 1.8f;

auto makeClockButton = [&](const std::string& image, const Vec2& pos) -> Rect {
    auto sp = Sprite::create(image);
    sp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    sp->setPosition(pos);
    sp->setScale(btnScale);
    base->addChild(sp, 1);
    auto cs = sp->getContentSize();
    float w = cs.width * btnScale;
    float h = cs.height * btnScale;
    return Rect(pos.x - w / 2, pos.y - h / 2, w, h);
};

スクリーンショット 2026-08-04 13.33.13.png
[HOUR]で短針、[MINUTE]で長針を進めるギミック

スクリーンショット 2026-08-04 13.33.23.png
8時20分に合わせると正解の設定です


5. 正解状態の針を汎用「layer」機能で表示

プロンプト:
room1-3-1のステップ2でclock/hourhand.pngを8時、clock/minutehand.pngを20分に合わせて表示してください。layerというキーで設定

アイテムのような「所持状態で表示/非表示」ではなく、常に固定表示したい装飾画像向けに、StageDef へ新しく layer という汎用フィールドを追加します。画像・位置・回転角を指定できます。

struct LayerImage {
    std::string image;
    ax::Vec2    pos;
    float       rotation = 0.0f;
};

struct StageDef {
    // ...
    std::vector<StageItem>  items;
    std::vector<LayerImage> layer;
};
// layer: 固定装飾画像(回転指定可)を背景上に重ねて表示
for (const auto& li : def.layer)
{
    auto layerSp = Sprite::create(li.image);
    if (layerSp)
    {
        layerSp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
        layerSp->setPosition(li.pos);
        layerSp->setRotation(li.rotation);
        sp->addChild(layerSp, 1);
    }
}
"layer": [
  { "image": "clock/hourhand.png", "pos": [922, 410], "rotation": 240 },
  { "image": "clock/minutehand.png", "pos": [922, 410], "rotation": 120 }
]

スクリーンショット 2026-08-04 14.20.09.png


6. 正解後の遷移構造を整理(room1-3-3の新設)

プロンプト:
room1-3-1のステップ2は、bg_stage_006_2.pngにして、時計盤をタップした場合は、room1-3-3に遷移してください。layerはroom1-3-3に移行してください。

room1-3-1 のステップ2は通常表示(bg_stage_006_2.png)に戻し、時計盤をタップしたときだけズーム画像+針の layer 表示(room1-3-3)に遷移する構成に整理します。

"room1-3-1": [
  { "...": "ステップ1: 通常表示 + clockHandsギミック" },
  {
    "bg": "bg_stage_006_2.png",
    "touches": [
      { "rect": [850, 576, 160, 160], "stage": "room1-3-3" },
      { "rect": [840, 191, 180, 360], "stage": "room1-3-2" }
    ],
    "back": "room1-3"
  }
],
"room1-3-3": [
  {
    "bg": "bg_stage_006_3.png",
    "touches": [],
    "layer": [
      { "image": "clock/hourhand.png", "pos": [922, 410], "rotation": 240 },
      { "image": "clock/minutehand.png", "pos": [922, 410], "rotation": 120 }
    ],
    "back": "room1-3-1"
  }
]

7. ギミック成功時に複数ステージを同時に進める

プロンプト:
clockHandsのギミックを解いた際に、room1-3-2のステップを進めてください。bgはbg_stage_006_5.pngで。

既存の GimmickDef は成功時の保存先ステージを1つ(stage/stageStatus)しか持てなかったため、saveStage2/saveStatus2 を追加し、2つのステージを同時に進められるようにします。

struct GimmickDef {
    // ...
    std::string saveStage;
    int         saveStatus = 0;
    std::string saveStage2;
    int         saveStatus2 = 0;
};
auto* ud = ax::UserDefault::getInstance();
if (!def.saveStage.empty())
    ud->setIntegerForKey(("stageData_" + def.saveStage).c_str(), def.saveStatus);
if (!def.saveStage2.empty())
    ud->setIntegerForKey(("stageData_" + def.saveStage2).c_str(), def.saveStatus2);
ud->flush();
"clockHands": {
  "type": "3",
  "stage": "room1-3-1",
  "stageStatus": 1,
  "stage2": "room1-3-2",
  "stageStatus2": 1,
  "goStage": "room1-3-1"
}
"room1-3-2": [
  { "...": "ステップ1: bg_stage_006_4.png" },
  {
    "bg": "bg_stage_006_5.png",
    "touches": [],
    "back": "room1-3-1"
  }
]

8. アイテム「ライターオイル」の追加

プロンプト:
アイテム"3"に「ライターオイル」を追加。サムネイルは、item_03_1*.png。room1-3-2のステップ2の時、item_006.pngを表示して、該当箇所をタップでアイテム取得できるように

"3": {
  "name": "ライターオイル",
  "image": "items/item_03_1.png",
  "image_large": "items/item_03_1_large.png"
}

item_006.png はステージ背景と同じ1870×841のフル透過画像です。ピクセル解析でオイル缶の描画位置を特定し、タップ判定用の rect をそこに合わせます。

{
  "bg": "bg_stage_006_5.png",
  "touches": [],
  "back": "room1-3-1",
  "items": [
    { "image": "item_006.png", "itemNo": "3", "rect": [913, 392, 110, 59] }
  ]
}

スクリーンショット 2026-08-04 13.33.41.png
時計のギミックに正解することで振り子の扉を開くことができました
何かあります・・・

スクリーンショット 2026-08-04 13.34.05.png
ライターオイルでした!


成果物

あからさまに怪しい時計・・・
時計の針を8時20分に合わせると、振り子の扉が開き、ライターオイルを獲得!


まとめ

実装内容 対応ファイル
時計部屋への入室・振り子タップ導線 stages.json
時計の針合わせギミック(type "3") gimmicks.json / MainScene.cpp
ギミック背景ズームに合わせたレイアウト再調整 gimmicks.json / MainScene.cpp
固定装飾画像用の layer 機能 MainScene.cpp / stages.json
正解後遷移の構造整理(room1-3-3) stages.json
ギミック成功時の複数ステージ同時更新 MainScene.cpp / gimmicks.json
ライターオイルアイテムの追加 items.json / stages.json

type: "1" のポップアップ式ダイヤル錠から始まり、type: "2" のポップアップ無し4隅タップ、そして今回の type: "3" の針合わせと、ギミックの表現の幅が広がりました。座標やタップ領域は毎回Pythonでの画像解析(Image.crop + グリッド描画、alpha_composite での事前合成確認)を通して実際の描画位置に合わせています。

0
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
0
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?