Skip to main content

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

体験実習 3Ⅾゲーム開発を体験してみよう!

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

プログラムのすべてを最初から書くのではなく、用意されているプログラムのTODO部分を順番に完成させます。

1つの課題が終わるたびに、

  1. プログラムを書く
  2. ビルドする
  3. 実行する
  4. ゲーム画面の変化を確認する
1.プログラムを書く→2.ビルドする
image.png
3.実行する→4.変化を確認

     スクリーンショット 2026-07-30 103951.png

という流れで進めます。


今日完成させるゲーム

ゲームを完成させると、次のことができるようになります。

  • プレイヤー、敵、背景が画面に表示される
  • READYのカウントダウン後にゲームが始まる
  • 左右キーでプレイヤーがレーン移動する
  • プレイ時間に応じてスコアが増える
  • 敵に当たるとゲームオーバーになる

今日の作業では、ゲームづくりでよく使う次の考え方にも触れます。

  • 変数
  • 関数呼び出し
  • Init()Update()Draw()
  • フレームとFPS
  • deltaTime
  • 入力処理
  • 条件分岐
  • 距離・時間・速さ
  • 当たり判定

作業を始める前に

Visual StudioでPlayScene.cppを開いてください。

このファイルには、今回使用する穴埋め用のプログラムがあらかじめ用意されています。
参加者が別のファイルからコピーする作業はありません。

各課題には、PlayScene.cppで確認する行番号を「〇〇行目あたり」と書いてあります。

Visual Studioの左側に表示される行番号を見ながら、その行付近へ移動してください。
行番号が少しずれている場合は、近くにあるTODO番号を確認します。

1つのTODOが終わったら、必ずビルドしてから実行してください。
エラーが出た場合は、次のTODOへ進まず、今書いた場所を確認します。

エラーが出たときに確認すること

  • 英字の大文字と小文字が合っているか
  • "を消していないか
  • ()の数が合っているか
  • 行の最後に;が付いているか
  • 全角文字をコードの中へ入力していないか

1.プログラムは変数と関数で動いている

変数は、ゲームの情報を覚える箱

プログラムは、ゲームに必要な情報を変数に保存します。

変数は、数値や状態に名前を付けて覚えておくための箱です。

このゲームでは、たとえば次の情報を変数で管理しています。

変数

覚えている情報

stateTimer_

カウントダウンなどの残り時間

score_

現在のスコア

scrollSpeed_

道路や敵が流れる速さ

state_

READY、プレイ中、ゲームオーバー中などの状態

backgroundHandle_

読み込んだ背景画像を使うための番号

画像や3Dモデルそのものを、変数の小さな箱へ入れているわけではありません。

画像やモデルを読み込むと、それを使うための番号や情報が作られます。プログラムは、その番号を変数に覚えておき、必要なときに画像やモデルを表示します。

変数の値が変化すると、ゲーム画面にも変化が起こります。

  • 位置の値が変わると、キャラクターが移動する
  • stateTimer_が減ると、カウントダウンが進む
  • score_が増えると、画面の得点が増える
  • プレイヤーの状態が変わると、走る・倒れるなどのアニメーションが切り替わる

関数は、処理をまとめた命令

関数は、何らかの処理をまとめて名前を付けたものです。

たとえば、次のような命令を関数として用意できます。

Jump();

このコードは、Jumpという名前の関数を呼び出しています。

関数を呼び出すときは、基本的に次の形で書きます。

関数名();

関数によっては、丸かっこの中へ情報を渡します。

LoadModel("ファイルの場所");

丸かっこの中へ渡す情報を引数と呼びます。

今回のプログラムでも、関数を使って次のような命令を出します。

  • モデルを読み込む
  • 背景画像を読み込む
  • プレイヤーを左へ移動させる
  • プレイヤーを右へ移動させる
  • 敵との当たり判定を調べる

2.ゲームは処理を繰り返して動いている

ゲームの画面は、1枚の画像を表示して終わりではありません。

入力を調べ、キャラクターの位置やスコアを更新し、新しい画面を描く処理を何度も繰り返しています。

今回使うPlaySceneには、大きく分けて次の3つの処理があります。

関数

役割

実行される回数

Init()

モデルや画像などを準備する

最初に1回

Update()

入力、計算、当たり判定などを行う

ゲーム中に何度も

Draw()

現在のゲーム画面を描く

ゲーム中に何度も

流れを図にすると、次のようになります。

ゲーム開始
    ↓
  Init()
    ↓
┌──────────────┐
│   Update()   │
│      ↓       │
│    Draw()    │
└──────┬───────┘
       │
       └─ 何度も繰り返す

この繰り返しをゲームループと呼びます。

1フレームとは

Update()でゲームの状態を更新し、Draw()で画面を描く1回分を、1フレームと考えます。

1秒間に何フレーム処理したかを表す値がFPSです。

FPSは、Frames Per Secondの略です。

FPSは、1秒間に処理するフレーム数です。

1秒間に処理したフレーム数を$N$、時間を$t$秒とすると、FPSは次の式で表せます。

$$\mathrm{FPS}=\frac{N}{t}$$

たとえば60FPSなら、1秒間に約60回、更新と描画を行います。

ゲームは1フレームごとに少しずつ状態を変えるため、キャラクターが滑らかに動いて見えます。


3.TODO 01:モデルと背景を読み込もう

この課題で変わること

現在のプログラムでは、モデルと背景のファイル名が仮の名前になっています。

正しいファイル名を指定すると、プレイヤー、敵、背景がゲーム画面に表示されます。

変更する場所

PlayScene.cppの157行目あたりを確認してください。

この場所はInit()の中です。
Init()は、ゲームを始める前に必要なものを準備する場所です。

157行目あたりには、次の3つの関数が並んでいます。

LoadPlayerModel("Assets/XXX.mv1");
LoadEnemyModel("Assets/XXXXXXX.mv1");
LoadBackgroundImage("Assets/XXXXXXXX.png");

それぞれの関数名を日本語で考えてみましょう。

関数

読み込むもの

LoadPlayerModel

プレイヤーの3Dモデル

LoadEnemyModel

敵の3Dモデル

LoadBackgroundImage

背景画像

.mv1は3Dモデルのファイル、.pngは画像ファイルです。

作業

  1. Assetsフォルダの中を確認する
  2. プレイヤーとして使うモデルのファイル名を探す
  3. 敵として使うモデルのファイル名を探す
  4. 背景として使う画像のファイル名を探す
  5. XXXなどの仮の部分を、見つけたファイル名へ書き換える
  6. Assets/や拡張子まで含めて、正確に入力する

書くときの注意

文字列は"で囲みます。

"Assets/ファイル名.拡張子"

次の部分は消さないようにしてください。

  • 最初と最後の"
  • Assets/
  • モデルの.mv1
  • 画像の.png
  • 行末の;

動作確認

ビルドして実行します。

次の3つが表示されるか確認してください。

  • プレイヤー
  • 背景

この時点では、READYの表示が止まったままでも問題ありません。次の課題でカウントダウンを動かします。


4.TODO 02:カウントダウンタイマーを動かそう

この課題で変わること

現在は、READYのカウントダウンが止まっています。

残り時間をフレームごとに少しずつ減らすことで、カウントダウンが進み、ゲームが始まるようになります。


deltaTimeとは

パソコンが1フレームを処理する時間は、いつも完全に同じとは限りません。

前のフレームから今回のフレームまでに経過した時間を、deltaTimeに保存しています。

数式では、1フレームの経過時間を$\Delta t$と表します。

FPSと1フレームの時間には、次の関係があります。

$$\Delta t=\frac{1}{\mathrm{FPS}}$$

60FPS付近なら、1フレームの時間はおよそ次の値です。

$$\Delta t\approx\frac{1}{60}\approx0.0167$$

単位は秒です。

30FPS付近なら、次のようになります。

$$\Delta t\approx\frac{1}{30}\approx0.0333$$

単位は秒です。

FPS

1フレームのおよその時間

60FPS

約0.0167秒

30FPS

約0.0333秒

30FPSでは1回に進む時間が大きく、60FPSでは1回に進む時間が小さくなります。しかし、1秒間に積み重ねると、どちらも約1秒になります。


タイマーを減らす考え方

現在の残り時間を$T_{\mathrm{now}}$、次のフレームの残り時間を$T_{\mathrm{next}}$とします。

毎フレーム、経過した時間だけ残り時間を減らします。

$$T_{\mathrm{next}}=T_{\mathrm{now}}-\Delta t$$

プログラムでは、次の変数が対応しています。

数式

プログラム

$T$

stateTimer_

$\Delta t$

deltaTime

作業

PlayScene.cppの395行目あたりを確認してください。

現在は、stateTimer_へ同じstateTimer_を代入しているため、値が変わりません。

stateTimer_ = stateTimer_;

右辺を、次の意味になる式へ変更してください。

現在の残り時間 - 1フレームの経過時間

使用する変数は次の2つです。

  • 残り時間:stateTimer_
  • 1フレームの経過時間:deltaTime

動作確認

ビルドして実行します。

次の順番で変化するか確認してください。

  1. READYの数字が減る
  2. GOが表示される
  3. プレイヤーが走る
  4. 道路が動き始める
  5. 敵が出現する

5.関数を呼ぶと、複数のものが動き始める

カウントダウンが終了すると、プログラムはStartGameという関数を呼びます。

StartGameの中では、プレイヤー、道路、敵へそれぞれ命令を出しています。

StartGame
├─ プレイヤーを走る状態にする
├─ 道路のスクロールを始める
└─ 敵の出現を始める

1つの関数を呼ぶことで、ゲーム開始に必要な複数の処理をまとめて実行できます。

このように、複雑なゲームでも、処理を小さな関数へ分けることで分かりやすく整理できます。


6.TODO 03:左右キーの入力を完成させよう

この課題で変わること

左矢印キーと右矢印キーを押したとき、プレイヤーへレーン移動の命令を出せるようにします。

入力処理とif

Update()はゲーム中に何度も呼ばれます。

その中でキーの状態を調べると、プレイヤーがキーを押したタイミングに反応できます。

if (条件)
{
    条件が成立したときの処理
}

ifは、「もし条件が成立したら、中の処理を実行する」という命令です。

次のコードは、左矢印キーが押されたかを調べています。

if (Input::IsKeyDown(KEY_INPUT_LEFT))
{
    // 左へ移動する命令
}

右矢印キーは、else ifで調べています。

else if (Input::IsKeyDown(KEY_INPUT_RIGHT))
{
    // 右へ移動する命令
}

作業

PlayScene.cppの105行目あたりを確認してください。

TODO 03-1TODO 03-2の空いている場所へ、プレイヤーを移動させる関数呼び出しを書きます。

使用する関数名は次のとおりです。

方向

使用する関数名

MovePlayerLeft

MovePlayerRight

関数呼び出しの書き方を思い出してください。

関数名();

左側には左移動の関数、右側には右移動の関数を使います。

動作確認

ビルドして実行します。

この時点では、キー入力の処理は完成しますが、プレイヤーの移動速度が0.0fになっています。

そのため、左右キーを押しても、まだプレイヤーが動かないことがあります。

これは失敗ではありません。

キー入力の命令は完成
        +
移動速度はまだ0
        ↓
移動する距離が0

次の課題で移動速度を計算すると、実際にレーン移動するようになります。


7.TODO 04:レーン移動の速さを計算しよう

この課題で変わること

現在のプレイヤー移動速度は0.0fです。

レーン間の距離と、移動にかける時間から必要な速さを計算し、左右キーでプレイヤーが動くようにします。


距離・時間・速さの関係

一定の速さで移動するとき、距離、時間、速さには次の関係があります。

$$d=vt$$

記号

意味

$d$

距離

$v$

速さ

$t$

時間

今回は速さ$v$を求めたいので、式を次の形にします。

$$v=\frac{d}{t}$$

つまり、「速さ=距離÷時間」です。


ミニワーク:必要な速さを求めよう

このゲームでは、中央レーンから隣のレーンまでの距離が12.0です。

1レーンの移動を0.20秒で終わらせます。

次の式へ数値を入れて、必要な速さを計算してください。

$$v=\frac{12.0}{0.20}=?$$

計算できたら、次の空欄へ記入してください。

必要な速さ:________

数値ではなく変数を使って式を書く

PlayScene.cppの26行目あたりを確認してください。

プログラムには、距離と時間がすでに変数として用意されています。

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

内容

変数名

1レーン分の距離

LANE_DISTANCE

1レーンの移動時間

LANE_MOVE_TIME

TODO 04では、次の空欄へ変数名を入れます。

constexpr float PLAYER_MOVE_SPEED =
    ________ / ________;

数式の

$$v=\frac{d}{t}$$

と同じ順番になるように考えてください。

計算結果の数字を直接書くのではなく、距離と時間を表す変数を使って式を書きます。


1フレームで進む距離

速さが決まっても、プレイヤーを一度に1レーン分移動させるわけではありません。

実際には、フレームごとに少しずつ位置を変えています。

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

$$\Delta x=v\Delta t$$

記号

意味

$\Delta x$

そのフレームで進む距離

$v$

プレイヤーの速さ

$\Delta t$

1フレームの経過時間

この細かい位置更新や、レーン中央で止める処理は完成済みです。

今回は、ゲームの仕様から速さを計算することに注目します。

動作確認

ビルドして実行します。

左右矢印キーを押し、次のことを確認してください。

  • プレイヤーが左右のレーンへ移動する
  • 1回の入力で隣のレーンへ移動する
  • 0.20秒で移動が終わる
  • レーンの中央付近で停止する

8.TODO 05:時間に応じてスコアを増やそう

この課題で変わること

現在は、score_へ同じscore_を代入しているため、スコアが増えません。

1秒あたりの得点とdeltaTimeを使って、プレイ時間に応じてスコアを増やします。

1秒あたりの得点

プログラムには、次の定数が用意されています。

constexpr float SCORE_PER_SECOND = 100.0f;

これは、「1秒間プレイすると100点増える」という意味です。

ただし、Update()は1秒に1回ではなく、1秒間に何度も呼ばれます。

そこで、1フレームで増やす得点を次の式で求めます。

$$\Delta S=r\Delta t$$

記号

意味

プログラム

$\Delta S$

今回増やす得点

これから計算する値

$r$

1秒あたりの得点

SCORE_PER_SECOND

$\Delta t$

1フレームの経過時間

deltaTime

現在のスコアを$S_{\mathrm{now}}$、次のスコアを$S_{\mathrm{next}}$とすると、次のようになります。

$$S_{\mathrm{next}}=S_{\mathrm{now}}+r\Delta t$$


作業

PlayScene.cppの129行目あたりを確認してください。

現在は次のコードになっています。

score_ = score_;

次の形になるように空欄を考えてください。

score_ =
    score_ + ________ * ________;

使うものは次の2つです。

  • 1秒あたりの得点
  • 1フレームの経過時間

変数名は、すぐ上のコードや表から探してください。

動作確認

ビルドして実行します。

次のことを確認してください。

  • READY中はスコアが増えない
  • ゲーム開始後にスコアが増える
  • 約1秒で、およそ100点増える

なぜ毎フレーム1点ではいけないのか

次のように毎フレーム固定で1点増やすとします。

1フレームごとに1点追加

60FPSなら1秒に約60点、30FPSなら1秒に約30点になります。

これではパソコンの処理速度によって得点の増え方が変わります。

deltaTimeを使うことで、FPSが変化しても、実際に経過した時間に合わせて得点を増やせます。


9.TODO 06:敵との当たり判定を有効にしよう

この課題で変わること

現在は、プレイヤーが敵に触れてもゲームオーバーになりません。

当たり判定を調べる関数を条件に使い、敵に当たったときだけ死亡処理を実行します。


trueとfalse

条件が成立していることをtrue、成立していないことをfalseで表します。

if (false)
{
    // この中は実行されない
}

現在のプログラムは条件が常にfalseなので、敵に当たっても中の処理へ入りません。

戻り値

関数は、処理を行ったあとに結果を返すことがあります。

今回使う当たり判定の関数名はIsHitです。

この関数は、当たり判定の結果を次のどちらかで返します。

戻り値

意味

true

敵に当たっている

false

敵に当たっていない


作業

PlayScene.cppの134行目あたりを確認してください。

if (false)

falseの部分を、当たり判定を調べる関数呼び出しへ変更してください。

使用する関数名は次のとおりです。

IsHit

関数を呼び出すときは、関数名の後ろに丸かっこを付けます。

条件部分には、関数が返したtrueまたはfalseが使われます。

当たったときに実行される処理

条件がtrueになると、次の処理が実行されます。

敵に当たる
    ↓
死亡アニメーションへ切り替える
    ↓
道路の動きを止める
    ↓
敵の出現を止める
    ↓
ゲームオーバー画面へ移る

動作確認

ビルドして実行します。

次のことを確認してください。

  1. 敵へプレイヤーを当てる
  2. プレイヤーのアニメーションが変化する
  3. 道路が停止する
  4. 敵の出現が停止する
  5. ゲームオーバー画面へ移る

10.ゲーム全体の流れを確認しよう

今回完成させた処理を、ゲームループへ当てはめて確認します。

Init()で行ったこと

  • プレイヤーモデルを読み込む
  • 敵モデルを読み込む
  • 背景画像を読み込む
  • ゲームに必要なオブジェクトを準備する

Update()で行ったこと

  • カウントダウンを減らす
  • 左右キーの入力を調べる
  • プレイヤーへ移動の命令を出す
  • 時間に応じてスコアを増やす
  • 敵との当たり判定を調べる
  • ゲームの状態を切り替える

Draw()で行っていること

  • 背景を描く

このプロジェクトでは、道路、草、木、プレイヤー、敵、スコアなどは、PlayScene::Draw()のあとに別の描画処理が自動的に表示します。

そのため、今回はDraw()のコードを変更していません。


11.完成チェック

次の項目をすべて確認してください。

  • プレイヤーが表示される
  • 敵が表示される
  • 背景が表示される
  • READYのカウントダウンが進む
  • カウントダウン後にゲームが始まる
  • 左矢印キーで左へ移動する
  • 右矢印キーで右へ移動する
  • プレイヤーが約0.20秒で隣のレーンへ移動する
  • ゲーム中にスコアが増える
  • 敵に当たるとゲームオーバーになる

12.今回学んだこと

変数

ゲームの位置、時間、得点、状態などを覚えておくために使います。

関数

処理をまとめた命令です。関数を呼ぶことで、モデルの読み込みやプレイヤーの移動などを実行できます。

ゲームループ

最初にInit()を実行し、その後はUpdate()Draw()を繰り返します。

フレームとFPS

ゲームは1フレームずつ少しずつ状態を変えています。FPSは、1秒間に処理するフレーム数です。

deltaTime

前のフレームから今回のフレームまでに経過した時間です。

タイマー、移動、スコアなどを実際の時間に合わせて動かすために使います。

入力処理

Update()の中でキー入力を調べ、押されたキーに応じて関数を呼びます。

距離・時間・速さ

$$v=\frac{d}{t}$$

という関係を使い、ゲームの仕様からプレイヤーの移動速度を求めました。

当たり判定

敵に当たっているかを関数で調べ、その結果がtrueのときだけゲームオーバー処理を実行しました。