Qtフレームワークを使用したクラシックなスネークゲームの開発プロセスと実践

はじめに

スネークゲームは歴史が古く、非常に人気のあるクラシックなコンピュータゲームで、1976年の"Snake Game"にさかのぼることができます。1997年にノキアの携帯電話の内蔵ゲームとして世界的に知られるようになり、そのシンプルな操作と中毒性のあるゲーム性は世界中のプレイヤーから愛されるようになりました。スネークの人気はモバイルプラットフォームでの普及だけでなく、パソコンやゲーム機などの他のプラットフォームにも拡大し、時代を超えたコンピュータゲームの代表作となっています。

Qtフレームワークを使用してスネークゲームを開発することには多くの利点があります。Qtは強力でクロスプラットフォームなアプリケーション開発フレームワークであり、豊富なGUIコントロールとツールを提供しています。これにより、ユーザーインターフェースの設計が直感的で効率的になります。Qtのイベントドリブンアーキテクチャとネットワーク機能は、応答性の高い制御とマルチプレイヤーオンラインゲームの実現に強力なサポートを提供します。さらに、Qtのオープンソースの特性と活発なコミュニティサポートは、開発者が大量のリソースとドキュメントを利用できるようにし、開発の難易度とコストを低減します。Qtのパフォーマンス最適化と統合開発環境も開発効率を大幅に向上させ、Qtを使用してスネークゲームを開発することを理想的な選択肢にしています。

1. Qtフレームワークの概要

スネークゲームのコードでは、Qtの基本的なコンポーネントとモジュールには以下のものが含まれます:

  • QWidget: 多くのQtコントロールの基底クラスとして、ウィンドウまたはウィンドウ内の領域を表し、他のコントロールを含めたり描画したりできます。
  • QMainWindow: メインウィンドウを表し、ほとんどのアプリケーションのトップレベルウィンドウです。
  • QDialog: ダイアログを作成するために使用され、ユーザーインタラクションに使用されます。
  • QPushButton: ボタンコントロールを作成するために使用され、ユーザーがクリックしてイベントをトリガーできます。コードでは、ゲームの開始、ゲーム難易度の選択、メインインターフェースへの戻りなどの操作に使用されます。
  • QPainter: 2Dグラフィックを描画するためのクラス。paintEvent関数で使用され、ゲームインターフェース、蛇の体、食物などを描画します。
  • QIcon: アイコンを作成および処理するために使用され、アプリケーションまたはコントロールのアイコンを設定します。
  • QTimer: タイマーを作成するために使用され、ゲーム中の蛇の移動などのイベントを周期ごとにトリガーできます。
  • QSound: サウンドエフェクトを再生するために使用され、ユーザーエクスペリエンスを向上させます。
  • QFont: フォントの外観を定義するために使用され、フォントサイズ、スタイルなどです。
  • QMessageBox: 警告メッセージボックスまたはその他のメッセージを表示するために使用され、ユーザーと対話します。
  • QFileQTextStream: ファイル操作に使用され、データの読み取りまたは書き込みを行います。ゲームコードでは、ゲームスコアの記録に使用されます。
  • QShortcut: ショートカットキーを定義でき、ユーザーがキーボードショートカットを使用して操作を実行できるようにします。例えば、矢印キーを使用して蛇の方向を制御します。
  • QPainterQPixmap: 画像を描画するために一緒に使用されます。QPixmapは画像オブジェクトで、QPainterはこの画像を描画するために使用されます。
  • QListQRectF: QListはリストデータを格納するためのコンテナです;QRectFは矩形領域を表し、蛇のボディノードを定義するために使用されます。
  • QApplication: 各Qtアプリケーションのメインオブジェクトで、アプリケーションの制御フローを処理します。
  • シグナルとスロットメカニズム: Qtのシグナルとスロットはその核心機能の1つで、オブジェクト間の通信に使用されます。例えば、ボタンのclickedシグナルはカスタムスロット関数に接続でき、ボタンがクリックされたときに特定の操作を実行します。

2. スネークゲームの設計

2.1 ゲームルールとプレイ方法の紹介

スネークゲームはクラシックなコンピュータゲームで、基本的なルールとプレイ方法は以下の通りです:

  • 目標: プレイヤーはゲームエリア内で移動する蛇を制御し、ランダムに出現する食物を食べるのが目標です。
  • 制御: 通常、キーボードの矢印キーを使用して蛇の頭の移動方向を制御します(上、下、左、右)。
  • 成長: 食物を1つ食べるごとに、蛇の長さが1つ増えます。
  • 障害: 蛇は自分の体にぶつかってはいけません。そうするとゲームオーバーになります。
  • スコア: プレイヤーは食物を食べることでスコアを獲得します。通常、食物の数に基づいてスコアが累積されます。

2.2 ゲームインターフェースの設計概要

スネークゲームのインターフェース設計には通常、以下の部分が含まれます:

  1. ゲームエリア: これは蛇と食物を表示する主な領域で、通常は矩形のグリッドです。
  2. 開始/一時停止ボタン: プレイヤーがゲームを開始したり、ゲーム中に一時停止したりできるようにします。
  3. 終了ボタン: プレイヤーは終了ボタンをクリックして現在のゲームを終了できます。
  4. スコア表示: プレイヤーの現在のスコアを表示し、通常はゲームエリアの上部または隅に配置されます。
  5. ゲーム難易度選択: 食物の出現頻度や蛇の移動速度に影響する異なる難易度レベルが含まれる場合があります。
  6. 過去の戦績: プレイヤーの過去のゲームの最高スコアやその他の履歴記録を表示します。
  7. サウンドエフェクト: ゲームにはボタンクリック時のサウンドフィードバックやゲーム終了時の特別なサウンドエフェクトが含まれる場合があります。

これらのインターフェース要素はQtの様々なコンポーネントを使用して実装されます。例えば、QPushButtonを使用してボタンを作成し、QPainterとQPixmapを使用してゲームエリアのグラフィックを描画し、QTimerを使用してゲームのリフレッシュレートと蛇の移動速度を制御します。さらに、インターフェースのレイアウトとスタイルはQtのレイアウトマネージャーとスタイルシートを使用して設計され、インターフェースの美しさとユーザーフレンドリーさを確保します。

3. 核心コード解析

3.1 メインインターフェース(GameHall)

3.1.1 レイアウトと機能の紹介

GameHallクラスはスネークゲームのメインインターフェースを表し、プレイヤーがゲームに入るための起点を提供します。以下はメインインターフェースの主なレイアウトと機能です:

  • ウィンドウサイズ: メインインターフェースは固定サイズ(例:1000x800ピクセル)に設定され、十分な表示領域を提供します。
  • ウィンドウアイコン: ウィンドウアイコンが設定され、アプリケーションのプロフェッショナルな外観が強化されます。
  • ウィンドウタイトル: ウィンドウのタイトルが表示され、ユーザーにアプリケーションの名前を伝えます。
  • 開始ゲームボタン: インターフェースには目立つ「開始ゲーム」ボタンがあり、これがプレイヤーがゲームに入る主なエントリーポイントです。ボタンにはツールチップがあり、その機能とショートカット操作方法を説明しています。
  • 描画エリア: paintEvent関数をオーバーライドすることで、メインインターフェースはカスタムの描画ロジックを含むことができ、背景画像またはゲームロビーの静的画像を表示できます。

3.1.2 コード実装の分析

コードレベルでは、GameHallクラスの実装には以下の重要な部分が含まれます:

GameHall::GameHall(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::GameHall)
{
    ui->setupUi(this);

    this->setFixedSize(1000, 800); // ウィンドウサイズを設定
    this->setWindowIcon(QIcon(":res/ico.jpg")); // ウィンドウアイコンを設定
    this->setWindowTitle("スネークゲーム");

    QPushButton* startBtn = new QPushButton(this);
    startBtn->move(410, 500);
    startBtn->setText("ゲーム開始");
    startBtn->setToolTip("これはゲーム開始ボタンで、マウスクリックまたはEnterキーで入ります!");
    startBtn->setToolTipDuration(3000);
    ButtonStyle::setCommonStyle_green(startBtn, 30);

    // ボタンにEnterショートカットを設定
    startBtn->setShortcut(Qt::Key_Return);

    GameSelect* gameSelect = new GameSelect;
    connect(startBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameSelect->setGeometry(this->geometry()); // 2番目のウィンドウと最初のウィンドウのサイズを同じにする
        gameSelect->show();
    });
}
  1. ボタン作成とレイアウト: QPushButtonインスタンスを作成し、その位置、テキスト、ツールチップ、スタイルを設定します。ボタンの位置はmoveメソッドで配置され、テキストは「開始ゲーム」に設定され、ツールチップは追加の使用説明を提供します。
  2. ボタンスタイル設定: ButtonStyle::setCommonStyle_green関数を使用してボタンのスタイルを設定し、例えば背景色、ボーダー、フォントサイズなどを設定します。これによりボタンは統一された外観と感覚を与えます。
  3. シグナル接続: Qtのシグナルとスロットメカニズムを使用して、「開始ゲーム」ボタンのclickedシグナルをスロット関数に接続し、ボタンがクリックされたときに実行します。このスロット関数では、クリック音を再生し、現在のメインインターフェースを閉じ、GameSelectウィンドウを表示します。これはゲームモードを選択するインターフェースです。
  4. 描画イベント処理 (GameHall::paintEvent(QPaintEvent *event))): paintEventをオーバーライドしてカスタム描画ロジックを実装します。QPainterとQPixmapを使用してウィンドウに背景画像を描画し、より魅力的な視覚効果を作成します。
void GameHall::paintEvent(QPaintEvent *event)
{
     Q_UNUSED(event); // コンパイラにeventパラメータが未使用であることを通知
    // 画家をインスタンス化
    QPainter painter(this);

    // 描画デバイスをインスタンス化
    QPixmap pix(":res/game_hall.jpg");

    // 描画
    painter.drawPixmap(0,0, this->width(), this->height(), pix);
}
  1. デストラクタ (GameHall::~GameHall()): GameHallクラスによって割り当てられたリソースをクリーンアップし、特にuiメンバー(ユーザーインターフェースのレイアウトオブジェクト)を削除します。
GameHall::~GameHall()
{
    delete ui;
}

3.2 ゲーム選択インターフェース(GameSelect)

3.2.1 機能の紹介

GameSelectクラスはスネークゲームの選択インターフェースを構成し、プレイヤーが好みに応じて異なるゲーム難易度を選択できるようにします。このインターフェースには通常、以下の重要な機能が含まれます:

  • ウィンドウサイズとアイコン: ウィンドウのサイズとアイコンを設定し、メインインターフェースと同様に一貫したユーザーエクスペリエンスを提供します。
  • ゲームモードボタン: 「簡単モード」、「通常モード」、「困難モード」などの異なる難易度レベルのボタンを提供し、それぞれが特定の蛇の移動速度に対応します。
  • 過去の戦績ボタン: プレイヤーが以前のゲームスコア記録を確認できるようにします。
  • 戻るボタン: いつでもゲームの起点に戻れるようにする戻るボタン。

3.2.2 コード実装の分析

コードレベルでは、GameSelectクラスの実装には以下の重要な部分が含まれます:

GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
    this->setFixedSize(1000, 800); // ウィンドウサイズを設定
    this->setWindowIcon(QIcon(":res/ico.jpg"));
    this->setWindowTitle("ゲーム選択インターフェース");

    // 戻るボタンの位置と設定
    QPushButton* backBtn = new QPushButton(this);
    backBtn->move(80, 50);
    backBtn->setIcon(QIcon(":res/back.png"));
    backBtn->setToolTip("このボタンをクリックするか、キーボードのEscキーを押してゲームロビーインターフェースに戻ります");
    backBtn->setShortcut(Qt::Key_Escape);
    backBtn->setIconSize(QSize(50, 50)); // アイコンサイズを設定

    // スタイルシートを設定
    ButtonStyle::setCommonStyle_write(backBtn, 26);

    connect(backBtn, &QPushButton::clicked, [=](){
        this->close();
        QSound::play(":res/clicked.wav");
        GameHall* gameHall = new GameHall;
        gameHall->show();
    });


    GameRoom* gameRoom = new GameRoom;

    QPushButton* easyBtn = new QPushButton(this);
    easyBtn->move(420, 300);
    easyBtn->setText("簡単モード");
    easyBtn->setToolTip("このボタンをクリックするか、数字キー1を押して簡単モードに入ります");
    easyBtn->setShortcut(Qt::Key_1);
    ButtonStyle::setCommonStyle_green(easyBtn, 26);
    connect(easyBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(300);
    });


    QPushButton* normalBtn = new QPushButton(this);
    normalBtn->move(420, 380);
    normalBtn->setText("通常モード");
    normalBtn->setToolTip("このボタンをクリックするか、数字キー2を押して通常モードに入ります");
    normalBtn->setShortcut(Qt::Key_2);
    ButtonStyle::setCommonStyle_green(normalBtn, 26);
    connect(normalBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(200);
    });


    QPushButton* hardBtn = new QPushButton(this);
    hardBtn->move(420, 460);
    hardBtn->setText("困難モード");
    hardBtn->setToolTip("このボタンをクリックするか、数字キー3を押して困難モードに入ります");
    hardBtn->setShortcut(Qt::Key_3);
    ButtonStyle::setCommonStyle_green(hardBtn, 26);
    connect(hardBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(100);
    });

    QPushButton* historyBtn = new QPushButton(this);
    historyBtn->move(420, 560);
    historyBtn->setText("過去の戦績");
    historyBtn->setToolTip("このボタンをクリックするか、キーボードのHを押して履歴を確認します");
    historyBtn->setShortcut(Qt::Key_H);
    ButtonStyle::setCommonStyle_write(historyBtn, 26);

    connect(historyBtn, &QPushButton::clicked, [=](){
        QWidget* widget = new QWidget;
        widget->setWindowTitle("過去の戦績");
        widget->setFixedSize(500, 300);

        QTextEdit* edit = new QTextEdit(widget);
        QFont font("メイリオ", 24);
        edit->setFont(font);
        edit->setFixedSize(500, 300);

        QFile file("D:/Acode/class111/test_-qt/Qt/greedy_snake/snake/history.txt");
        file.open(QIODevice::ReadOnly);

        QTextStream in(&file);
        int data = in.readLine().toInt();

        edit->append("スコア: ");
        edit->append(QString::number(data));
        widget->show();
    });
}
  1. ボタン作成: 各ゲーム難易度のためにQPushButtonインスタンスを作成し、それらの位置、テキスト、ツールチップ、ショートカットキーを設定します。これらのボタンは、プレイヤーがクリックまたは対応する数字キーを押すことでゲームモードを選択できるようにします。
  2. ボタンスタイル設定: ButtonStyle::setCommonStyle_green関数を使用してゲームモードボタンのスタイルを設定し、異なる色とフォントサイズを使用してボタンを区別します。
  3. シグナル接続: 各ゲームモードボタンのclickedシグナルをスロット関数に接続します。これらのスロット関数では、クリック音を再生し、現在の選択インターフェースを閉じ、選択された難易度に基づいて蛇の移動速度を設定し、ゲームルームインターフェースを起動します。
  4. 過去の戦績機能: 過去の戦績ボタンにシグナル接続を設定し、クリック時にファイルに保存されている最高スコアを読み取り、新しいウィンドウに表示します。
  5. 描画イベント処理 (GameSelect::paintEvent(QPaintEvent *event))): paintEventをオーバーライドしてインターフェースに背景画像を描画し、視覚効果を強化します。
void GameSelect::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this); // 描画オブジェクトをインスタンス化

    QPixmap pix(":res/game_select.jpg");
    painter.drawPixmap(0, 0, this->width(), this->height(), pix);
}
  1. ショートカットキー設定: 戻るボタンと各ゲームモードボタンにショートカットキーを設定し、ショートカット操作方法を提供します。
  2. サウンドエフェクト: インターフェースの変換やボタンクリック時にサウンドエフェクトを再生し、インタラクション体験を強化します。

3.3 ゲームルーム(GameRoom)

3.3.1 ゲームロジックと核心アルゴリズムの解析

GameRoomクラスはスネークゲームロジックを実装する核心で、蛇の移動、食物の生成、ゲーム失敗条件の検出が含まれます。

  1. 蛇の状態の初期化: コンストラクタで、蛇の位置と状態を初期化します。蛇の体はQList<QRectF>で表され、QRectFは蛇の体の各ノードを定義する矩形領域です。
  2. 食物生成: createNewFood()関数を使用してゲームエリア内にランダムに食物を生成します。食物もQRectFオブジェクトです。
  3. 蛇の移動: 蛇の移動はmoveUp()、moveDown()、moveLeft()、moveRight()関数で実現され、これらの関数は蛇の現在の移動方向に基づいて蛇の体の位置を更新します。
  4. ゲームループ: QTimerを使用してゲームループと蛇の移動頻度を制御します。timerのtimeoutシグナルがトリガーされると、蛇は現在の方向に移動し、食物を食べたか、自分や壁にぶつかったかをチェックします。
  5. ゲーム失敗条件: checkFail()関数を使用して蛇が自分や壁にぶつかったかをチェックし、そうであればゲームオーバーになります。
  6. ゲーム要素の描画: paintEvent()でQPainterとQPixmapを使用して蛇の体、食物、ゲーム背景を描画します。

3.3.2 コード実装の詳細

  1. コンストラクタ (GameRoom::GameRoom(QWidget *parent)):
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
    this->setFixedSize(1000, 800); // ウィンドウサイズを設定
    this->setWindowIcon(QIcon(":res/ico.jpg")); // ウィンドウアイコンを設定
    this->setWindowTitle("スネークゲーム");

    // スネークを初期化
    snakeList.push_back(QRectF(this->width() / 2, this->height() / 2, kSnakeNodeWidth, kSnakeNodeHeight));

    moveUp();
    moveUp();

    // 食物を作成
    createNewFood();

    timer = new QTimer(this);

    connect(timer, &QTimer::timeout, [=](){
        int cnt = 1; // デフォルトで1つ移動
        if (snakeList.front().intersects(foodRect)) // 蛇の頭が食物と交差しているかを判断
        {
            createNewFood();
            cnt++; // 食物が交差している場合は2つ移動
        }

        while(cnt--)
        {
            switch (moveDirect) {
            case SnakeDirect::UP:
                moveUp();
                break;
            case SnakeDirect::DOWN:
                moveDown();
                break;
            case SnakeDirect::LEFT:
                moveLeft();
                break;
            case SnakeDirect::RIGHT:
                moveRight();
                break;
            default:
                break;
            }
        }

        snakeList.pop_back(); // 最後のノードを削除
        update();
    });

    // ゲーム開始 一時停止
    QPushButton* startPauseBtn = new QPushButton(this);
    startPauseBtn->move(840, 120);
    startPauseBtn->setText("開始/一時停止");
    startPauseBtn->setToolTip("このボタンをクリックするか、キーボードのPキーを押して、ゲームの開始と一時停止を切り替えます");
    startPauseBtn->setShortcut(Qt::Key_P);
    ButtonStyle::setCommonStyle_green(startPauseBtn, 20);

    connect(startPauseBtn, &QPushButton::clicked, [=](){
        if (isGameStart == false)
        {
            isGameStart = true;
            timer->start(moveTimeout);
            sound = new QSound(":res/Trepak.wav");
            sound->play();
            sound->setLoops(-1);
        }
        else
        {
            isGameStart = false;
            timer->stop();
            sound->stop();
        }
    });

    QPushButton* backBtn = new QPushButton(this);
    backBtn->move(855, 200);
    backBtn->setIcon(QIcon(":res/back.png"));
    backBtn->setToolTip("このボタンをクリックするか、キーボードのEscキーを押してゲームロビーインターフェースに戻ります");
    backBtn->setShortcut(Qt::Key_Escape);
    backBtn->setIconSize(QSize(50, 50)); // アイコンサイズを設定
    // スタイルシートを設定
    ButtonStyle::setCommonStyle_write(backBtn, 26);
    connect(backBtn, &QPushButton::clicked, [=](){
        this->close();
        QSound::play(":res/clicked.wav");
        GameSelect* gameSelect = new GameSelect;
        gameSelect->show();

        if (isGameStart == true) {
            isGameStart = false;
            timer->stop();
            sound->stop();
        }
    });


    // 方向制御
    QPushButton* upBtn = new QPushButton(this);
    QPushButton* downBtn = new QPushButton(this);
    QPushButton* leftBtn = new QPushButton(this);
    QPushButton* rightBtn = new QPushButton(this);

    // 各ボタンにショートカットキーを作成
    QShortcut* upShortcut = new QShortcut(QKeySequence(Qt::Key_Up), this);
    QShortcut* downShortcut = new QShortcut(QKeySequence(Qt::Key_Down), this);
    QShortcut* leftShortcut = new QShortcut(QKeySequence(Qt::Key_Left), this);
    QShortcut* rightShortcut = new QShortcut(QKeySequence(Qt::Key_Right), this);

    // ショートカットキーのactivatedシグナルをボタンのclickedシグナルに接続
    connect(upShortcut, SIGNAL(activated()), upBtn, SLOT(animateClick()));
    connect(downShortcut, SIGNAL(activated()), downBtn, SLOT(animateClick()));
    connect(leftShortcut, SIGNAL(activated()), leftBtn, SLOT(animateClick()));
    connect(rightShortcut, SIGNAL(activated()), rightBtn, SLOT(animateClick()));


    upBtn->move(875, 405);
    downBtn->move(875,455);
    leftBtn->move(810,455);
    rightBtn->move(930,455);

    upBtn->setText("↑");
    ButtonStyle::setCommonStyle_green(upBtn,20);

    downBtn->setText("↓");
    ButtonStyle::setCommonStyle_green(downBtn,20);

    leftBtn->setText("←");
    ButtonStyle::setCommonStyle_green(leftBtn,20);

    rightBtn->setText("→");
    ButtonStyle::setCommonStyle_green(rightBtn,20);

    connect(upBtn, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::DOWN)
        {
            moveDirect = SnakeDirect::UP;
        }
    });

    connect(downBtn, &QPushButton::clicked,[=](){
        if (moveDirect != SnakeDirect::UP)
        {
            moveDirect = SnakeDirect::DOWN;
        }
    });

    connect(leftBtn, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::RIGHT)
        {
            moveDirect = SnakeDirect::LEFT;
        }
    });

    connect(rightBtn, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::LEFT)
        {
            moveDirect = SnakeDirect::RIGHT;
        }
    });


    // ゲーム終了
    QPushButton* exitBtn = new QPushButton(this);
    exitBtn->move(860, 700);
    exitBtn->setText("終了");
    ButtonStyle::setCommonStyle_green(exitBtn, 20);

    QMessageBox* messageBox = new QMessageBox(this);
    QPushButton* okBtn = new QPushButton("OK");
    QPushButton* cancelBtn = new QPushButton("キャンセル");

    messageBox->addButton(okBtn, QMessageBox::AcceptRole);
    messageBox->addButton(cancelBtn, QMessageBox::RejectRole);

    messageBox->setWindowTitle("警告");
    messageBox->setText("本当に終了しますか!");
    messageBox->setIcon(QMessageBox::Warning);



    connect(exitBtn, &QPushButton::clicked, [=](){
        messageBox->show();
        messageBox->exec(); // イベントループ
        QSound::play(":res/clicked.wav");

        if(messageBox->clickedButton() == okBtn)
        {
            this->close();
        }
        else
        {
            messageBox->close();
        }
    });
}
  1. 蛇の移動: 移動関数は、蛇の頭の新しい位置を計算し、snakeListを更新することで蛇の移動ロジックを実現します。移動後、蛇の尾部ノードが削除され、蛇の移動効果がシミュレートされます。
void GameRoom::moveUp()
{
    QPointF leftTop; // 左上座標
    QPointF rightBottom; // 右下座標

    auto snakeNode = snakeList.front(); // 頭
    int headX = snakeNode.x();
    int headY = snakeNode.y();

    if (headY < 0) // 壁を突き抜けた
    {
        leftTop = QPointF(headX, this->height() - kSnakeNodeHeight);
    }
    else
    {
        leftTop = QPointF(headX, headY - kSnakeNodeHeight);
    }

    rightBottom = leftTop + QPointF(kSnakeNodeWidth, kSnakeNodeHeight);

    snakeList.push_front(QRectF(leftTop, rightBottom));
}
  1. 食物生成: createNewFood()関数はランダムな位置に食物を生成し、食物が蛇の体と重ならないことを确保します。
void GameRoom::createNewFood()
{
    foodRect = QRectF(qrand() % (800/ kSnakeNodeWidth) * kSnakeNodeWidth,
                      qrand() % (this->height() / kSnakeNodeHeight) * kSnakeNodeHeight,
                      kSnakeNodeWidth,
                      kSnakeNodeHeight);
}
  1. タイマーとゲームループ: QTimerのtimeoutシグナルを使用して、蛇の移動を周期ごとにトリガーします。蛇が食物を食べると、長さが増え、新しい食物が生成されます。
// 食物を作成
createNewFood();

timer = new QTimer(this);

connect(timer, &QTimer::timeout, [=](){
    int cnt = 1; // デフォルトで1つ移動
    if (snakeList.front().intersects(foodRect)) // 蛇の頭が食物と交差しているかを判断
    {
        createNewFood();
        cnt++; // 食物が交差している場合は2つ移動
    }

    while(cnt--)
    {
        switch (moveDirect) {
        case SnakeDirect::UP:
            moveUp();
            break;
        case SnakeDirect::DOWN:
            moveDown();
            break;
        case SnakeDirect::LEFT:
            moveLeft();
            break;
        case SnakeDirect::RIGHT:
            moveRight();
            break;
        default:
            break;
        }
    }

    snakeList.pop_back(); // 最後のノードを削除
    update();
});
  1. ゲーム失敗条件: checkFail()関数は、蛇の各ノードを比較して、自己衝突または壁への衝突があるかどうかを検出します。
bool GameRoom::checkFail()
{
    // 蛇の体のノードを走査し、重複があるかをチェック
    for (int i = 0; i < snakeList.size(); ++i)
    {
        for (int j = i + 1; j < snakeList.size(); ++j)
        {
            if (snakeList.at(i) == snakeList.at(j))
            {
                // 重複が見つかった場合、ゲーム失敗
                return true;
            }
        }
    }
    // 重複が見つからない場合、ゲーム続行
    return false;
}
  1. 描画ロジック (GameRoom::paintEvent(QPaintEvent *event)):
void GameRoom::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this); // 画家オブジェクトをインスタンス化
    QPixmap pix;

    pix.load(":res/game_room.jpg");

    painter.drawPixmap(0, 0, 800, 800, pix);

    pix.load(":res/bg1.jpeg");
    painter.drawPixmap(800, 0, 200, 800, pix);

    // 蛇を描画:蛇の頭 + 蛇の体 + 蛇の尾
    // 蛇の頭を描画:上 下 左 右
    if(moveDirect == SnakeDirect::UP)
    {
        pix.load(":res/up.png");
    }
    else if (moveDirect == SnakeDirect::DOWN)
    {
        pix.load(":res/down.png");
    }
    else if (moveDirect == SnakeDirect::LEFT)
    {
        pix.load(":res/left.png");
    }
    else if (moveDirect == SnakeDirect::RIGHT)
    {
        pix.load(":res/right.png");
    }

    auto snakeHead = snakeList.front();
    painter.drawPixmap(snakeHead.x(), snakeHead.y(), snakeHead.width(), snakeHead.height(), pix);

    // 蛇の体を描画
    pix.load(":res/Bd.png");
    for (int i = 1; i < snakeList.size()-1; ++i)
    {
        auto node = snakeList.at(i);
        painter.drawPixmap(node.x(), node.y(), node.width(), node.height(), pix);
    }

    // 蛇の尾を描画
    auto snakeTail = snakeList.back();
    painter.drawPixmap(snakeTail.x(), snakeTail.y(), snakeTail.width(), snakeTail.height(), pix);

    // 食物を描画
    pix.load(":res/food.png");
    painter.drawPixmap(foodRect.x(), foodRect.y(), foodRect.width(), foodRect.height(), pix);

    // スコアを描画
    pix.load(":res/sorce_bg.png");
    painter.drawPixmap(this->width()*0.85, this->height()*0.06, 90, 40, pix);

    QPen pen;
    pen.setColor(Qt::black);
    painter.setPen(pen);
    QFont font("メイリオ");
    font.setPixelSize(24); // フォントサイズを20ピクセルに設定
    painter.setFont(font);
    painter.drawText(this->width()*0.9, this->height()*0.1, QString("%1").arg(snakeList.size()));


    // ファイルにスコアを書き込む
    int score = snakeList.size();
    QFile file("D:/Acode/class111/test_-qt/Qt/greedy_snake/snake/history.txt");
    if (file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream out(&file);
        out << score;
        qDebug() << score;
        file.close();
    } else {
         qDebug() << "ファイルを開けません";
    }

    // ゲーム失敗効果を描画
    if (checkFail())
    {
        pen.setColor(Qt::red);
        painter.setPen(pen);
        QFont font("メイリオ", 50);
        painter.drawText(this->width()*0.45, this->height()*0.5, QString("GAME OVER!"));
        timer->stop();
        QSound::play(":res/gameover.wav");
    }
}
  1. スコアとゲーム状態の表示: ゲームインターフェースに現在のスコアを描画し、通常は蛇の長さに基づいています。ゲームが失敗した場合、「GAME OVER!」を表示し、ゲームループを停止します。

3.4 ボタンスタイル(ButtonStyle)

3.4.1 コード実装

  1. クラス定義 (buttonstyle.h): 2つの静的メソッドを宣言し、異なるスタイルのボタンを設定するために使用されます。
class ButtonStyle {
public:
    static void setCommonStyle_green(QPushButton *button, size_t font_size);
    static void setCommonStyle_write(QPushButton *button, size_t font_size);
};
  1. スタイル設定 (buttonstyle.cpp): 上記の宣言されたメソッドを実装し、Qtのスタイルシート(styleSheet)を使用してボタンの視覚スタイルを定義します。
void ButtonStyle::setCommonStyle_green(QPushButton *button, size_t font_size) {
    QString styleSheet = R"(
        QPushButton {
            background-color: #4CAF50;
            border: 2px solid #4CAF50;
            border-radius: 10px;
            font: bold %1px 'メイリオ';
            color: white;
            padding: 8px;
            padding-left: 20px;
            padding-right: 20px;
        }
        QPushButton:hover {
            background-color: #45a049;
        }
        QPushButton:pressed {
            background-color: #3e8e41;
        }
    )";
    button->setStyleSheet(styleSheet.arg(font_size));
}

void ButtonStyle::setCommonStyle_write(QPushButton *button, size_t font_size) {
    QString styleSheet = R"(
        QPushButton {
            font: bold %1px 'メイリオ';
            border: 2px solid #a3a3a3;
            border-radius: 16px;
            background-color: rgba(255, 255, 255, 0);
            padding: 8px;
            padding-left: 20px;
            padding-right: 20px;
        }
        QPushButton:hover {
            background-color: rgba(180, 180, 180, 0.3);
        }
        QPushButton:pressed {
            background-color: rgba(180, 180, 180, 0.4);
        }
    )";
    button->setStyleSheet(styleSheet.arg(font_size));
}

3.4.2 実装効果

  1. 緑色ボタンスタイル (setCommonStyle_green): このメソッドは緑色テーマのボタンスタイルを設定し、以下の特徴があります:
    • 背景色は緑色(#4CAF50)です。
    • ボーダーは緑色で、幅は2pxです。
    • フォントは太字で、メイリオフォントを使用し、フォントサイズはfont_sizeパラメータで決定されます。
    • ボタンテキストの色は白色です。
    • ボタンには丸みと内側の余白があります。
    • マウスがボタン上にホバーすると、背景色が暗くなります(#45a049)、押すとさらに暗くなります(#3e8e41)。
  2. 白色ボタンスタイル (setCommonStyle_write): このメソッドは白色半透明テーマのボタンスタイルを設定し、以下の特徴があります:
    • フォントとサイズの設定は緑色ボタンと同じです。
    • ボーダーは灰色で、幅は2pxです。
    • 背景色は白色で、透明度があります(rgba(255, 255, 255, 0))。
    • ボタンにはより大きな丸みと内側の余白があります。
    • マウスがホバーすると、背景色は半透明の薄い灰色になります(rgba(180, 180, 180, 0.3))、押すと透明度が高くなります(rgba(180, 180, 180, 0.4))。

ButtonStyleクラスを使用することで、スネークゲームのすべてのボタンに統一され、プロフェッショナルな外観を簡単に適用でき、ユーザーインターフェースの一貫性と美しさを向上させます。

4. グラフィックとサウンド

4.1 グラフィックリソース

スネークゲームのグラフィックリソースには主に以下のものが含まれます:

  1. ゲーム背景: これはゲームの主な背景画像で、プレイヤーに視覚的なコンテキスト環境を提供します。
  2. 蛇の体: 蛇の体は通常、一連の矩形または円形のグラフィックで構成され、これらのグラフィックは蛇の移動に応じてゲームエリア内で位置が更新されます。
  3. 食物: 食物のグラフィックは通常、プレイヤーの注意を引く小さなアイコンで、蛇が収集する必要がある目標を表します。
  4. ボタンとアイコン: ゲームインターフェースのボタン、例えば「開始ゲーム」、「一時停止ゲーム」、「終了ゲーム」など、およびそれらのアイコンは、グラフィックリソースが必要です。

4.2 サウンドリソース

サウンドリソースはゲームのインタラクション性と没入感を強化し、以下が含まれます:

  1. クリック音: プレイヤーがボタンをクリックしたり選択したりしたときに再生されるサウンドエフェクト。
  2. ゲーム音楽: バックグラウンド音楽で、ゲームに連続的なオーディオ体験を提供します。
  3. ゲーム終了: プレイヤーのゲームが失敗したときに再生される特別なサウンドエフェクト。

4.3 Qtでのリソースのロードと使用

Qtでは、グラフィックとサウンドリソースは以下の方法でロードおよび使用できます:

  1. リソースファイル(.qrc): Qtはリソースファイルを使用して画像、音楽などのリソースを管理することをサポートしています。リソースファイルはXMLファイルで、すべてのリソースのパスとアクセス方法がリストされています。リソースファイルはアプリケーションと一緒にコンパイルおよびデプロイできるため、リソースをアプリケーションと一緒にパッケージ化できます。Qt Creatorでリソースファイルを作成し、<file>タグを使用してリソースのパスを指定します。リソースファイル内の画像にアクセスするには、":/res/image.png"のような構文を使用します。
  2. QPixmap: 画像リソースをロードおよび表示するために使用されます。例えば、QPixmap(":res/game_background.jpg")を使用してゲーム背景画像をロードします。
  3. QPainterとQWidget: QPainterオブジェクトを使用してQWidgetまたはそのサブクラス上に画像を描画します。paintEvent関数では、drawPixmapメソッドを呼び出して画像リソースを描画できます。
  4. QSound: サウンドエフェクトリソースを再生するために使用されます。例えば、QSound(":res/click.wav")を使用してクリックサウンドエフェクトをロードし、playメソッドを呼び出して再生します。
  5. QMediaPlayer(オプション): より高度なオーディオ制御が必要な場合は、QMediaPlayerクラスを使用してバックグラウンド音楽をロードおよび再生できます。
  6. シグナルとスロット: グラフィックとサウンドリソースのロードをゲームロジックと組み合わせます。例えば、ボタンのclickedシグナルをサウンドエフェクトを再生するスロット関数に接続します。
  7. パフォーマンス考慮: パフォーマンスを向上させるために、画像とサウンドリソースは事前にロードしてキャッシュし、ゲーム実行中に頻繁にリソースをロードしないようにします。

5. デバッグと最適化

5.1 デバッグテクニック

  1. Qt Creatorのデバッガを使用する: Qt Creatorに組み込まれたデバッガを使用して、ブレークポイントの設定、ステップ実行、変数の監視、呼び出しスタックの表示を行います。
  2. ログ出力: コードにログ出力ステートメントを追加して、プログラムの実行フローと変数の状態を記録します。qDebug()関数を使用できます。
  3. 単体テスト: 各関数とモジュールの正確性を検証するための単体テストを記述し、コードの変更が新しいエラーを導入しないことを確認します。
  4. グラフィカルデバッグ: グラフィックとインターフェース関連の問題については、Qt Creatorのグラフィカルデバッグツール、例えばピクセルパーフェクトモードとDOMインスペクターを使用します。
  5. パフォーマンス分析: ValgrindやQt Creatorのパフォーマンスアナライザーなどのパフォーマンス分析ツールを使用して、メモリリークとパフォーマンスボトルネックを検出します。
  6. 複雑な関数の簡素化: 複雑な関数をより小さく、より管理しやすい関数に分解して、問題の特定に役立ちます。

5.2 パフォーマンス最適化の提案

  1. 再描画領域の削減: 必要な場合にのみupdate()またはrepaint()を呼び出し、更新の範囲をできるだけ小さくします。
  2. 描画ロジックの最適化: paintEventで不必要な計算と描画操作を減らします。例えば、描画結果をキャッシュして重複描画を避ける。
  3. より効率的なデータ構造の使用: 必要に応じて適切なデータ構造を選択し、検索、挿入、削除操作の時間複雑度を減らします。
  4. オブジェクト作成の削減: 頻繁に呼び出される関数でオブジェクトの作成と破棄を避けます。オブジェクトの作成と破棄のコストは高いためです。
  5. タイマーの適切な使用: タイマーの間隔を適切に設定し、過剰なトリガーによるCPU使用率の過度な上昇を避けます。
  6. リソースのロードとキャッシュ: ゲームで使用されるすべてのリソースを事前にロードし、メモリにキャッシュして、ゲーム実行中に頻繁にディスクを読み取るのを避けます。

5.3 コードリファクタリングと改善方法

  1. コードレビュー: 定期的にコードレビューを行い、潜在的な設計問題を発見し、コード品質を改善します。
  2. 重複コードの抽出: 重複したコードブロックを独立した関数またはクラスにリファクタリングして、コードの冗長性を減らします。
  3. デザインパターンの使用: 必要に応じてオブザーバーパターン、シングルトンパターン、ファクトリーパターンなどのデザインパターンを適用し、コードの保守性と拡張性を向上させます。
  4. インターフェースと抽象クラス: 明確なインターフェースと抽象クラスを定義して、コードの低結合と高凝集を促進します。
  5. 依存性注入: 依存性注入を使用してクラス間の依存関係を減らし、コードの柔軟性とテスト可能性を向上させます。
  6. リファクタリングツール: Qt CreatorやIDEのリファクタリングツールを使用して、安全にリネーム、移動、コード構造の変更を行います。
  7. コードフォーマット: コードスタイルの一貫性を維持し、Qt Creatorのコードフォーマット機能や.editorconfigファイルを使用してコードを自動フォーマットします。
  8. ドキュメントとコメント: コードに十分なドキュメントとコメントを追加し、他の人がコードを理解し、保守しやすくします。

6. 拡張機能と将来の方向性

6.1 拡張機能

  1. ハイスコアボード: ハイスコア機能を統合し、プレイヤーのスコアを記録し、ゲーム終了後にスコアをアップロードできるようにします。ローカルおよびオンラインのハイスコアボードを提供し、最高スコアと過去の最高記録を表示します。
  2. 異なるテーマ: プレイヤーが宇宙、森、海などの異なるゲームテーマを選択できるようにします。各テーマにはユニークなグラフィックとサウンドエフェクトがあります。テーマパックを通じてゲームを拡張し、ユーザーは新しいテーマパックをダウンロードしてインストールできます。
  3. カスタムスキン: プレイヤーが蛇に異なるスキンや色を選択できるようにし、パーソナライズされたゲーム体験を提供します。
  4. 強化されたサウンドと音楽: より豊富なサウンドエフェクトとバックグラウンド音楽オプションを提供し、プレイヤーが好みの音楽スタイルを選択できるようにします。
  5. 実績システム: 特定のタスクを完了したり、特定のマイルストーンに達したりしたプレイヤーに報酬を与える実績システムを実装します。
  6. チュートリアルとヘルプ: 新しいプレイヤーがすぐにゲームを始められるように、ゲームチュートリアルとヘルプシステムを提供します。

6.2 将来の方向性

  1. ネットワーク対戦: ネットワーク対戦機能を開発し、プレイヤーがオンラインで競争または協力できるようにします。マッチングシステムを実装し、プレイヤーのスコアやスキルレベルに基づいてマッチングを行います。
  2. ソーシャル機能: ソーシャルメディア共有機能を統合し、プレイヤーが自分のスコアや実績を共有できるようにします。
  3. クロスプラットフォームゲーム: ゲームがPC、モバイルデバイス、ゲーム機などの異なるデバイスとプラットフォームで実行できることを確認します。
  4. クラウドストレージ: クラウドサービスを使用してプレイヤーのスコアと進捗を保存し、プレイヤーが異なるデバイスでゲームを続行できるようにします。
  5. 人工知能対戦相手: 人工知能を持つ対戦相手を開発し、プレイヤーにより挑戦的なゲーム体験を提供します。
  6. ゲーム内購入: 特殊なスキン、テーマパック、またはその他の仮想商品などのゲーム内購入オプションを提供します。
  7. 多言語サポート: 多言語サポートを追加し、ゲームの対象範囲を拡大します。
  8. 拡張現実(AR)と仮想現実(VR): ゲームをARまたはVRプラットフォームに拡張する可能性を探求し、没入型のゲーム体験を提供します。

6.3 技術実装

  • ハイスコアボード: データベースまたはバックエンドサービスを使用してスコアを保存し、スコアのアップロードと検索を実装します。
  • ネットワーク対戦: ネットワークプログラミング技術を使用してクライアント間の通信を実装し、ゲームセッションを調整する中央サーバーが必要になる場合があります。
  • クラウドストレージ: 华为云、阿里云、腾讯云などのクラウドサービスAPIを統合し、データの同期と保存を実現します。
  • 人工知能: 機械学習アルゴリズムを適用し、異なる戦略を持つAI対戦相手を作成します。

タグ: Qt C++ ゲーム開発 GUI スネークゲーム

8月3日 01:55 投稿