Skip to main content

3D避けゲー

3D避けゲームを作ってみよう

体験入学用教材(30~40分)

今回は、3本のレーンを左右に移動しながら、前から来る敵を避ける3Dゲームを作ります。

完成しているプログラムに、少しずつ穴埋めをしていきます。
1か所直したら、必ずビルドして実行し、ゲーム画面の変化を確認しましょう。


今日の目標

今日の体験では、次のことを学びます。

  1. プログラムから3Dモデルを読み込む方法
  2. 数学の式を使って、移動の速さを求める方法
  3. 速さ × 時間で、そのフレームの移動距離を求める方法
  4. キー入力でキャラクターを動かす方法
  5. 時間に応じてスコアを増やす方法
  6. 敵に当たったときにゲームオーバーにする方法

今日作るゲーム

このゲームでは、次のようなことが起こります。

  • プレイヤーが3Dで表示される
  • 道路が前へ流れる
  • 敵が前から来る
  • 左右キーで3本のレーンを移動できる
  • 時間がたつとスコアが増える
  • 敵に当たるとゲームオーバーになる

【画像挿入①】完成したゲーム画面のスクリーンショットを入れる

例:プレイヤー、道路、敵、スコアが見えている画面


プログラムの中心は PlayScene

今回いちばん注目するのは PlayScene です。

PlayScene は、ゲーム全体の進行を書く台本の役目です。

たとえば、次のように命令を出します。

player_->SetRunState();
stage_->StartScroll();
enemies_->SetSpawning(true);

つまり PlayScene を読むと、

  • いつゲームが始まるのか
  • プレイヤーがどう動くのか
  • スコアがどう増えるのか
  • いつゲームオーバーになるのか

が分かります。


今日の進め方

今日は、次の順番で進めます。

  1. プレイヤーのモデルを表示する
  2. 移動の速さを計算する
  3. 1フレームで進む距離を計算する
  4. 左右キーでレーン移動させる
  5. スコアを増やす
  6. 敵に当たったらゲームオーバーにする

大事
1問終わるごとに、ビルド → 実行 → 動作確認をしてください。


課題01 プレイヤーを表示しよう

学ぶこと

  • ファイルパス
  • 文字列
  • 3Dモデルの読み込み

説明

ゲームの中の3Dモデルは、プログラムの中に直接入っているわけではありません。
プログラムから、ファイルの場所を指定して読み込みます。

たとえば、プレイヤーのモデルは次のファイルです。

Assets/run.mv1

穴埋め問題

次の空欄に、プレイヤーのファイル名を入れてください。

constexpr const char* PLAYER_MODEL_PATH =
    "Assets/______.mv1";

ヒント

run

想定解答

constexpr const char* PLAYER_MODEL_PATH =
    "Assets/run.mv1";

確認

  • ビルドして実行する
  • プレイヤーの3Dモデルが表示されるか確認する

【画像挿入②】プレイヤーが表示されたゲーム画面


課題02 1レーン分の移動速度を求めよう

学ぶこと

  • 距離
  • 時間
  • 速さ
  • 定数
  • 割り算

説明

このゲームでは、プレイヤーは3本のレーンを移動します。

レーンの位置は次のようになっています。

$$ 左 = -12, \quad 中央 = 0, \quad 右 = 12 $$

つまり、中央レーンから右レーンまでの距離は 12 です。

今回は、0.20秒でちょうど1レーン分移動させたいとします。

ここで使う式は次です。

$$ 距離 = 速さ \times 時間 $$

この式を、速さについて変形すると

$$ 速さ = \frac{距離}{時間} $$

になります。

今回の数値を代入すると、

$$ 速さ = \frac{12.0}{0.20} = 60.0 $$

となります。

つまり、1秒間に60進む速さにすれば、0.20秒で1レーン分移動できます。

【画像挿入③】レーンと距離の図を入れる

推奨画像ファイル:lane_speed_diagram.png

穴埋め問題

constexpr float LANE_DISTANCE  = 12.0f;
constexpr float LANE_MOVE_TIME = 0.20f;

// 速さ = 距離 ÷ 時間
constexpr float PLAYER_MOVE_SPEED =
    ______ / ______;

ヒント

  • 距離は LANE_DISTANCE
  • 時間は LANE_MOVE_TIME

想定解答

constexpr float PLAYER_MOVE_SPEED =
    LANE_DISTANCE / LANE_MOVE_TIME;

確認

  • ビルドして実行する
  • まだ左右キーは未完成なので、この時点では見た目の変化がなくてもよい
  • ただし、ビルドが通ることを確認する

課題03 1フレームで進む距離を求めよう

学ぶこと

  • deltaTime
  • 掛け算
  • 毎フレームの移動距離

説明

ゲームは、1回だけ画面を描いて終わりではありません。
1秒間に何十回も、次の処理をくり返しています。

入力を調べる
↓
ゲームの状態を更新する
↓
画面を描画する

この1回分を1フレームと呼びます。

フレームごとに経過した時間を deltaTime といいます。

1フレームで進む距離は、次の式で求めます。

$$ 移動距離 = 速さ \times 時間 $$

プログラムでは、

$$ moveDistance = playerMoveSpeed_ \times deltaTime $$

です。

たとえば、

$$ playerMoveSpeed_ = 60.0, \quad deltaTime = 0.016 $$

なら、

$$ moveDistance = 60.0 \times 0.016 = 0.96 $$

となります。

つまり、そのフレームでは 0.96だけ進む ことになります。

【画像挿入④】1フレームごとの移動距離の図を入れる

推奨画像ファイル:frame_move_diagram.png

穴埋め問題

UpdatePlayerLaneMove(
    ______ * ______
);

ヒント

  • 速さは playerMoveSpeed_
  • 時間は deltaTime

想定解答

UpdatePlayerLaneMove(
    playerMoveSpeed_ * deltaTime
);

補足

この中の詳しい内部処理、たとえば

  • レーンの中央でぴったり止まる処理
  • 2回素早く押すと2レーン移動する処理

は、今回は完成済みです。
今日は「速さ × 時間で距離を出している」という考え方だけ押さえましょう。

確認

  • ビルドして実行する
  • この時点ではまだ左右入力が未完成なので、見た目の変化がなくてもよい
  • ただし、移動距離の計算式が書けたことを確認する

課題04 左右キーでプレイヤーを動かそう

学ぶこと

  • キー入力
  • if
  • 関数呼び出し

説明

今度は、左右キーが押されたときに、レーン移動の命令を出します。

今回使う命令は次の2つです。

MovePlayerLeft();
MovePlayerRight();

穴埋め問題

if (Input::IsKeyDown(KEY_INPUT_LEFT))
{
    ______();
}
else if (Input::IsKeyDown(KEY_INPUT_RIGHT))
{
    ______();
}

ヒント

  • 左へ動かす: MovePlayerLeft
  • 右へ動かす: MovePlayerRight

想定解答

if (Input::IsKeyDown(KEY_INPUT_LEFT))
{
    MovePlayerLeft();
}
else if (Input::IsKeyDown(KEY_INPUT_RIGHT))
{
    MovePlayerRight();
}

確認

  • ビルドして実行する
  • 左右キーでプレイヤーがレーン移動するか確認する
  • 素早く2回押すと、2レーン移動できるか確認する

【画像挿入⑤】左右移動しているゲーム画面


課題05 スコアを増やそう

学ぶこと

  • 変数
  • 足し算
  • 時間経過

説明

ゲーム中は、時間がたつほどスコアが増えます。

今回は、1秒あたり100点ずつ増えるようにしています。

$$ 1秒あたりの得点 = 100 $$

1フレームで増える点数は、

$$ 今回増える得点 = 1秒あたりの得点 \times deltaTime $$

です。

そのため、スコアの更新式は次のようになります。

$$ score_ = score_ + SCORE_PER_SECOND \times deltaTime $$

穴埋め問題

score_ = score_ + ______ * ______;

ヒント

  • 1秒あたりの得点: SCORE_PER_SECOND
  • 今回経過した時間: deltaTime

想定解答

score_ = score_ + SCORE_PER_SECOND * deltaTime;

確認

  • ビルドして実行する
  • プレイ中、スコアが少しずつ増えていくか確認する

【画像挿入⑥】スコアが増えているゲーム画面


課題06 敵に当たったらゲームオーバーにしよう

学ぶこと

  • 条件分岐
  • 真偽値
  • 当たり判定

説明

敵とプレイヤーが当たったかどうかは、IsHit() という関数で調べます。

  • 当たっているとき: true
  • 当たっていないとき: false

つまり、

if (IsHit())

は、**「もし敵に当たっていたら」**という意味です。

穴埋め問題

if (______)
{
    LoadDeadMotion();
    ChangeToGameOver();
    return;
}

ヒント

IsHit()

想定解答

if (IsHit())
{
    LoadDeadMotion();
    ChangeToGameOver();
    return;
}

確認

  • ビルドして実行する
  • 敵に当たるとゲームオーバーになるか確認する

【画像挿入⑦】ゲームオーバー直前、またはゲームオーバー画面


まとめ

今回の体験では、完成済みのゲームに穴埋めをしながら、次のことを学びました。

  • 3Dモデルの読み込み
  • 数式を使った移動速度の計算
  • deltaTime を使った1フレームの移動距離
  • キー入力によるレーン移動
  • スコアの更新
  • 当たり判定によるゲームオーバー

今日の特に大切な式は、次の2つです。

1. 速さを求める式

$$ 速さ = \frac{距離}{時間} $$

2. 1フレームで進む距離を求める式

$$ 移動距離 = 速さ \times 時間 $$

ゲームプログラムでは、こうした数学の式が、そのままキャラクターの動きになります。


最後の自由調整

時間に余裕があれば、次の値を変えて遊んでみましょう。

constexpr float LANE_MOVE_TIME   = 0.20f;
constexpr float SCORE_PER_SECOND = 100.0f;

試してみること

  • LANE_MOVE_TIME を小さくすると、レーン移動はどう変わるか
  • LANE_MOVE_TIME を大きくすると、操作感はどう変わるか
  • SCORE_PER_SECOND を大きくすると、スコアはどう変わるか

教員用メモ

この教材で完成済みにしているもの

  • 背景画像の読み込み
  • カウントダウン
  • ゲーム開始命令
  • 道路と敵の移動
  • 位置補正
  • 2レーン移動の内部処理
  • ゲームオーバー遷移の内部処理

画像ファイル案

  • 画像① 完成ゲーム画面
  • 画像② プレイヤー表示画面
  • 画像③ lane_speed_diagram.png
  • 画像④ frame_move_diagram.png
  • 画像⑤ 左右移動画面
  • 画像⑥ スコア増加画面
  • 画像⑦ ゲームオーバー画面