MitiruEngine

01 2D アクションのプロトタイプ

矩形プレイヤーをキーで動かし、壁との当たり判定、画面上の目標を拾うとスコアが上がる。すべて C++ だけで書く、もっとも基本的な作り方。

「画面の上で何かを動かす」を、純 C++ だけで作るチュートリアルです。HTML も JS もこの段階では出てきません。MitiruEngine の標準的な作り方は「ゲームの中身は C++」なので、まずはここから入るのが順当です。

時間の目安は 20 〜 30 分。事前に はじめに を済ませ、mitiru doctor が OK を返すことを確認してください。


できあがるもの

  • 黒い背景の上に動く矩形プレイヤー (32 × 32 px)
  • 矢印キー WASD で 4 方向に移動
  • 画面の端を超えない (壁との当たり判定)
  • 小さい黄色い円を 1 個出して、触れたら得点 +1 + 別の場所に再配置
  • 左上にスコア表示
完成画面: 黒背景の上に青い矩形プレイヤーと黄色い円、左上にスコア表示
完成イメージ。プレイヤーをキーで動かして、黄色い円に触れるとスコアが増える。

手順 1: ひな型を作る

純 C++ のプロトタイプなので、CEF を切った最小テンプレートを使います。

mitiru new --template native-only my-action
cd my-action

native-only テンプレートは assets/scene.html を含みません。CEF プロセスも起動せず、起動が 1 秒未満になります。

生成された my-action/ フォルダの中身: src/main.cpp, mitiru.toml, .gitignore, README.md
mitiru new 直後のフォルダ構成。触るのは src/main.cpp と mitiru.toml だけ。

手順 2: そのまま走らせて確認

中身を書き換える前に、一度走らせて初期状態を見ます。

mitiru run

初回は engine を取得して CMake configure が走るので 1 〜 2 分かかります。2 回目以降は秒で立ち上がります。

ひな型を走らせた直後の画面: 黒背景に Hello, Mitiru! の文字
ひな型は黒背景に小さなテキストが出るだけのウィンドウを開く。ここから書き換えていく。

ウィンドウのタイトルやサイズを変えたければ mitiru.toml[window] を編集します。

[window]
title = "My Action"
width = 800
height = 600
vsync = true

mitiru run をもう一度叩くと、変えた設定で開き直します。


手順 3: プレイヤーを置く

src/main.cpp を開いて、矩形プレイヤーを表示するだけの最小コードに書き換えます。

#include <mitiru/Mitiru.hpp>

class Player {
public:
    float x = 384.0f;
    float y = 284.0f;
    static constexpr float kSize = 32.0f;
};

class MyGame final : public mitiru::Game {
public:
    mitiru::Size layout(int w, int h) override { return {w, h}; }

    void update(float dt) override {
        (void)dt;
    }

    void draw(mitiru::Screen& s) override {
        const float w = static_cast<float>(s.width());
        const float h = static_cast<float>(s.height());
        s.drawRect({0, 0, w, h}, {0.06f, 0.06f, 0.10f, 1.0f});        // 背景
        s.drawRect({m_player.x, m_player.y, Player::kSize, Player::kSize},
                   {0.45f, 0.75f, 0.95f, 1.0f});                       // プレイヤー
    }

private:
    Player m_player;
};

int main() {
    mitiru::Engine engine;
    MyGame game;
    mitiru::EngineConfig cfg;
    cfg.title         = "My Action";
    cfg.windowWidth   = 800;
    cfg.windowHeight  = 600;
    cfg.enableCef     = false;
    cfg.fontAtlasRanges = mitiru::EngineConfig::FontAtlas::Latin;
    engine.run(game, cfg);
}

ここまでで分かること:

  • Game を継承して update(dt)draw(Screen&) を埋めるのが基本形。
  • 描画は Screen::drawRect(rect, color) のように C++ の関数を呼ぶ だけ。HTML も JS もいらない。
  • 色は RGBA 0.0 〜 1.0 の Colorf。RGB の感覚で書ける。

mitiru run で確認:

プレイヤーの矩形を描画した画面: 暗い背景の中央付近に水色の正方形
プレイヤー矩形だけ。まだ動かない。

手順 4: 動かす

update(dt) でキー入力を見て位置を進めます。

void update(float dt) override {
    const float speed = 240.0f;  // px / sec
    const auto& in = input();

    if (in.isKeyDown(mitiru::Key::ArrowLeft)  || in.isKeyDown(mitiru::Key::A)) m_player.x -= speed * dt;
    if (in.isKeyDown(mitiru::Key::ArrowRight) || in.isKeyDown(mitiru::Key::D)) m_player.x += speed * dt;
    if (in.isKeyDown(mitiru::Key::ArrowUp)    || in.isKeyDown(mitiru::Key::W)) m_player.y -= speed * dt;
    if (in.isKeyDown(mitiru::Key::ArrowDown)  || in.isKeyDown(mitiru::Key::S)) m_player.y += speed * dt;
}

ポイント:

  • update(dt) は毎フレーム呼ばれ、dt には前フレームからの経過秒が入ります。
  • speed * dt で動かすと、フレームレートに依存しない一定速度になります (60Hz でも 144Hz でも同じ速さ)。
  • isKeyDown押されているあいだ ずっと true。一回押した瞬間だけ欲しい場合は isKeyJustPressed を使います。

mitiru run し直すと、キーで矩形が動くようになっています。

プレイヤーがキー入力で移動している様子: 元の位置から右上に動いた水色矩形
キーを押している方向にプレイヤーが進む。

手順 5: 画面の端でぶつかる

このままだと画面外まで行ってしまうので、update の末尾で位置をクランプします。

void update(float dt) override {
    // (上のキー入力処理はそのまま)

    const float maxX = 800.0f - Player::kSize;
    const float maxY = 600.0f - Player::kSize;
    if (m_player.x < 0)     m_player.x = 0;
    if (m_player.x > maxX)  m_player.x = maxX;
    if (m_player.y < 0)     m_player.y = 0;
    if (m_player.y > maxY)  m_player.y = maxY;
}

これだけで「壁との当たり判定」になります。プロトタイプ段階の衝突はだいたいこのレベルで足りる、というのも覚えておくといいです。


手順 6: 目標を出してスコアを足す

目標 (黄色い円) とスコアを追加します。クラスのメンバに目標とスコアを足し、update で接触判定、draw で円とテキストを出します。

#include <mitiru/Mitiru.hpp>
#include <random>
#include <cstdio>

class MyGame final : public mitiru::Game {
public:
    mitiru::Size layout(int w, int h) override { return {w, h}; }

    void onStart() override {
        respawnTarget();
    }

    void update(float dt) override {
        const float speed = 240.0f;
        const auto& in = input();
        if (in.isKeyDown(mitiru::Key::ArrowLeft)  || in.isKeyDown(mitiru::Key::A)) m_x -= speed * dt;
        if (in.isKeyDown(mitiru::Key::ArrowRight) || in.isKeyDown(mitiru::Key::D)) m_x += speed * dt;
        if (in.isKeyDown(mitiru::Key::ArrowUp)    || in.isKeyDown(mitiru::Key::W)) m_y -= speed * dt;
        if (in.isKeyDown(mitiru::Key::ArrowDown)  || in.isKeyDown(mitiru::Key::S)) m_y += speed * dt;

        m_x = std::clamp(m_x, 0.0f, 800.0f - kSize);
        m_y = std::clamp(m_y, 0.0f, 600.0f - kSize);

        // 矩形 (プレイヤー) と円 (目標) の中心距離で当たり判定
        const float cx = m_x + kSize * 0.5f;
        const float cy = m_y + kSize * 0.5f;
        const float dx = cx - m_targetX;
        const float dy = cy - m_targetY;
        if (dx * dx + dy * dy < (kSize * 0.5f + 14.0f) * (kSize * 0.5f + 14.0f)) {
            ++m_score;
            respawnTarget();
        }
    }

    void draw(mitiru::Screen& s) override {
        const float w = static_cast<float>(s.width());
        const float h = static_cast<float>(s.height());
        s.drawRect({0, 0, w, h}, {0.06f, 0.06f, 0.10f, 1.0f});
        s.drawCircle({m_targetX, m_targetY}, 14.0f, {1.0f, 0.85f, 0.30f, 1.0f});
        s.drawRect({m_x, m_y, kSize, kSize}, {0.45f, 0.75f, 0.95f, 1.0f});

        char buf[32];
        std::snprintf(buf, sizeof(buf), "Score: %d", m_score);
        s.drawTextInRect(buf, {16, 12, 200, 24}, {1, 1, 1, 1});
    }

private:
    void respawnTarget() {
        static std::mt19937 rng{std::random_device{}()};
        std::uniform_real_distribution<float> rx(40.0f, 760.0f), ry(40.0f, 560.0f);
        m_targetX = rx(rng);
        m_targetY = ry(rng);
    }

    static constexpr float kSize = 32.0f;
    float m_x = 384.0f, m_y = 284.0f;
    float m_targetX = 0.0f, m_targetY = 0.0f;
    int   m_score = 0;
};

int main() {
    mitiru::Engine engine;
    MyGame game;
    mitiru::EngineConfig cfg;
    cfg.title         = "My Action";
    cfg.windowWidth   = 800;
    cfg.windowHeight  = 600;
    cfg.enableCef     = false;
    cfg.fontAtlasRanges = mitiru::EngineConfig::FontAtlas::Latin;
    engine.run(game, cfg);
}

mitiru run で動作確認:

完成画面: 左上に Score 表示、プレイヤー矩形と目標の黄色い円
左上にスコア、画面のどこかに目標 (黄色い円)。プレイヤーが触れるとスコアが増えて目標が別の位置に出る。

何が分かったか

  • Game::update(dt)Game::draw(Screen&) の 2 つに C++ だけで ゲームの中身が書ける。
  • 入力 / 状態 / 描画は別レイヤーじゃなく、同じクラスのメソッドとして並ぶ。プロトタイプ段階ではこれで充分。
  • HTML / CSS / JS は このチュートリアルでは 1 行も書いていない。MitiruEngine の標準形はこの方向です。

CEF (HTML / CSS) は HUD やメニューの 見た目 を凝りたくなったときに使います。それは 02 画面に UI を出す で扱います。

次の一歩

  • プレイヤーを 画像 にする: Screen::drawSprite に置き換える。
  • 時間制限 を入れる: Game::onStart で開始時刻を覚え、update で残り時間を計算、draw で表示。
  • シーン遷移: タイトル → ゲーム → 結果の 3 シーンに分けたい場合は SceneRouter を使います。examples/hello_scene/ が短い実例です。