ゲームエンジニア科体験入学

ゲームエンジニア科体験入学の説明ページ

へびゲーム


体験入学会プログラミング体験教材

はじめに

本日は体験入学会にお越しいただきありがとうございます。
プログラミング初心者向けの体験授業として、C++を使った「スネークゲーム」を完成させます。
コード中の TODO マークに沿って穴埋めし、動くゲームを作りましょう。


HEBIGAMEを動かしてみよう

まずはいったん遊んでみよう



Step 1: 背景色を設定しよう(TODO #1 )  62行目付近

ゲーム画面の最初に呼ばれる Stage クラスのコンストラクタ(初期化処理)内で、
SetBackgroundColor 関数を使って画面の背景色を設定します。

// TODO #1: ここに背景色を設定するRGBを入れてください
SetBackgroundColor( ____, ____, ____ );

ヒント

SetBackgroundColor(132, 255, 255); は淡いシアン(空色)です。


Step 2: ステージを描画しよう(TODO #2) 148行目付近

Stage::Draw() 内で、先ほど作ったグリッド状ステージを描画する関数を呼び出します。

void Stage::Draw()
{
    // TODO #2: ここにステージ描画関数を呼び出すコードを追加してください
    ____();
    
    if (!snake || !food)
        return;
    // 以下、蛇とフード、スコア描画...
}

ヒント

DrawStageGrid() を呼び出します。


Step 3: スネークを作成しよう(TODO #3) 63行目付近

コンストラクタ内で snake ポインタに新しい Snake オブジェクトを生成します。

// TODO #3: Snakeオブジェクトを生成しよう
snake = ____;

ヒント

new Snake() でインスタンス化します。


Step 3.5: 移動タイマーを設定しよう(TODO #3.5) 19行目付近

スネークの移動間隔を決めるタイマーを設定します。

// TODO #3.5: スネークの移動間隔を設定しよう
SetMoveTimer( ____ );

ヒント

定数 MOVETIME を使いましょう。


Step 4: フードを作成しよう(TODO #4) 64行目付近

コンストラクタ内で food ポインタに新しい Food オブジェクトを生成します。

// TODO #4: Foodオブジェクトを生成しよう
food = ____;

ヒント

new Food() を使います。


Step 5: キーボード入力を処理しよう(TODO #5)120行目付近

Stage::Update() 内で矢印キーを押したときにスネークの進行方向を変更します。

if (Input::IsKeyDown(KEY_INPUT_UP))
    snake->SetDirection( ____ );
if (Input::IsKeyDown(KEY_INPUT_DOWN))
    snake->SetDirection( ____ );
if (Input::IsKeyDown(KEY_INPUT_LEFT))
    snake->SetDirection( ____ );
if (Input::IsKeyDown(KEY_INPUT_RIGHT))
    snake->SetDirection( ____ );

ヒント

Direction::UP, DOWN, LEFT, RIGHT を使います。


Step 6: ヘビを伸ばそう(TODO #6) 131行目付近

スネークがフードを食べたら、体を伸ばすメソッドを呼び出します。

if (snake->GetHeadPos() == food->GetPosition()) {
    // TODO #6: ヘビを伸ばす関数を呼び出そう
    snake->____();
    food->SpawnRandom();
    // スコア加算へ(TODO #8)
}

ヒント

Snake クラスの Grow() メソッドを呼び出します。


Step 7: スコアを描画しよう(TODO #7) 155行目付近

Stage::Draw() 内で画面上にスコアを表示します。

// TODO #7: スコアを描画しよう
DrawScore();

ヒント

すでに用意した DrawScore() を呼び出します。


Step 8: スコアを加算しよう(TODO #8) 136行目付近

フードを食べたときにスコアを増加させます。

if (snake->GetHeadPos() == food->GetPosition()) {
    // ヘビを伸ばす
    // TODO #8: スコアを加算しよう
    Score::AddScore( ____ );
    food->SpawnRandom();
}

ヒント

1 回のフード取得につき 1 点加算しましょう。


Step 9: 壁との当たり判定をしよう(TODO #9) 110行目付近

Stage::Update() 内でスネークがステージ外に出たらゲームオーバーにします。

ivec3 pos = snake->GetBody()[0].GetPosition();
// TODO #9: 壁に当たったかチェックして、当たったら死亡処理を呼ぼう
if ( CheckHitWall( vec3(pos.x, 0, pos.z) ) ) {
    snake->SetDeath();
}

ヒント

CheckHitWall() を使って判定します。


Step 10: フードをスポーンしよう(TODO #4.5) 92行目付近

ステージ読み込み後にフードをランダムな位置に配置します。

if (food) {
    // TODO #4.5: フードをランダムにスポーンさせよう
    food->____();
}

ヒント

SpawnRandom() メソッドを呼び出します。


Step 11: フードを更新しよう(TODO #11) 142行目付近

Stage::Update() 内でフードのアニメーション更新を呼び出します。

// TODO #11: フードの更新処理を呼び出そう
food->____();

ヒント

Update() メソッドを呼び出します。


以上でスネークゲームの基本機能が完成します!
コードをビルドして、動作を確認してみましょう。1 つずつクリアしながら進めてください。

Step 12: ゲームオーバー時のシーン切り替え(TODO #12) 117行目付近

スネークが死亡したときに、別のシーンへ切り替えます。

if (!snake->IsAlive())
{
    // TODO #12: 死亡時にシーンを切り替えるコードを追加しよう
    SceneManager::ChangeScene();
}

ヒント

以上で、全 12 ステップの穴埋め教材が完成です! コードをビルドし、ゲームオーバー時にも正しくシーンが切り替わるか確認してみましょう。 コードをビルドして、動作を確認してみましょう。1 つずつクリアしながら進めてください。



URL

https://github.com/youetsux/HEBIGAME_TAIKEN.git

キャラクターAIを作ってみよう

image.png

今日の目標

今日の体験実習では、2Dボンバーマン風ゲームのキャラクターAIのプログラムを作りながら、C++によるゲーム開発の実際の雰囲気を体験してほしいと思います。

C++によるプログラム開発の流れ

  1. Build → ソースをビルドして実行
  2. Edit → 指定の行番号の定義を『書き換え』
  3. Test → 動作を確認し、発見・改善

POINT: 小さな変更を加え、すぐにビルド&実行。これを繰り返すのが開発サイクル!

ゲームAIの種類

  1. キャラクターAI
    • キャラクターAIは文字通り、NPCを動かすために利用される
    • 格闘ゲームの敵キャラクターや、RPGで主人公を支援する仲間の動作を決める
  2. メタAI
    • ゲーム進行を補助する
    • 一定の条件を満たしたらイベントを開始
    • ステージに応じて敵やアイテムの配置を変更したりと、ゲーム画面の裏で色々な処理を行う
  3. ナビゲーションAI
    • キャラクターAIやメタAIに対し、各種情報を提供して動作をサポートする
    • 敵キャラクターに主人公の位置や進行方向、障害物に関する情報を与える
    • この仕組みがないと、キャラクターAIは正確な動作を行えない

その1.キャラクターを選ぼう

ゲームに登場するキャラクターは、その種類によっていろいろなパラメータを持っています。
プログラミング内でそれらを表現するときは、すべて数値や文字などの値として表現します。
これらのゲーム中で利用する値に名前をつけたものを「変数」と呼びます。

変数の例:

体験用プログラムでも同様に、敵キャラクターをEnemyという変数で表しています。
Enemyはその中に更に細かい設定用のパラメータ(変数を持っています)。
その中の、表示画像を表す変数を変更してみましょう。

Enemyの表示画像の変更

image.png

行番号:74 行目を見て書き換え

42  if (isGraphic) {
 43      //#01 敵の画像を選択する!  
 44      std::string enemyImage = GetEnemyImage(KABOCHA);
 45      enemyImage_ = LoadGraph(enemyImage.c_str());
 46  }

その2.初期出現位置を変更してみよう

初期出現位置の設定

初期位置をステージの右下に設定してみよう!
キャラクターは、座標系に設置されます。(座標も変数で表すよ!)
2Dならxy平面、3Dならxyz空間に(10,20)とか(40,50,1)のように設定されます。

image.png



座標は、x座標とy座標の値を持っています。
初期位置は、ちょっとした計算によって以下のように設定されています。

image.png

x,y方向に何マス目は26行目辺りで設定されているよ!

書き換えてみよう

行番号:26 行目を見て書き換え

24  //#02 敵の初期出現位置を変更してみよう
 25  //ステージの右下に設定するには?
 26  const Pointf INIT_POS{ 5, 5 };  // #02
 
 //26行目を書き換えると、66行目の計算に反映される
 66  pos_ = { INIT_POS.x * CHA_WIDTH, INIT_POS.y * CHA_HEIGHT };

その3.初期進行方向を変更してみよう

次の目標

次の目標として、ステージの右下から、ステージの左端まで移動させたい
Enemy飲む気はどのように決められているのか見てみよう。(どうせ方向も変数なんでしょ!)

方向を表す変数

初期方向はINIT_DIRという変数に設定されています。

image.png


image.png

image.png

その4.速度を与えて動かそう

ゲームの移動処理の基本

ゲームのキャラクタは、方向を示すベクトルと、スピード移動時間(フレーム間時間)で、次のフレームの位置を計算します。
これは、小学生の時に習った、

$\text{移動距離} = \text{速度} \times \text{時間}$

の式に方向がついたものを使います。って言っても、

image.png

移動に、方向が加わるだけであとは小学校で習ったのと一緒です。
上の図でいうと、
AからBに1フレームで距離16移動している
スピードは?
下の図のようにx軸方向、y軸方向にはどのぐらい移動してるかな?
みたいな話です。

ベクトル演算を使ってゲームキャラクターを移動する方法|Mizeee

ゲームのフレーム(画面更新)の仕組み

ゲームは1秒間に何回画面を更新している?
一般的には 60FPS (Frame Per Second) が標準です。FPSとは「1秒あたりのフレーム数」を表し、フレームとは「画面を1回描き直すこと」です。

方向とフレーム間時間を考慮した移動処理

image.png

1フレームごとにこんな感じに、計算していきます。

速度の変数を変更する

向きが決まったら、その方向にEnemyを進めてみよう。
現在は、(わざとらしく)スピード設定が0になっている。

行番号:16 行目を見て書き換え

14  //#04 敵の移動速度を変更してみよう
 15  //ちょうどよい速さはどのぐらいかな?
 16  const float SPEED = 0.0f;  // #04

その5.キャラクターAI(移動処理の追加)

移動パターンを考える

ゲームのキャラクタは、ゲーム内で得た情報を使っていろいろな情報を使って、その場面場面に適した動きを切り替えます(状態遷移)これらを自動的に行うのがAIの仕事になります。

簡単な移動のパターンを考えてみよう

現在のEnemyは、プレイヤーの情報などを知る手段がないので、自分ができる範囲(自分の位置、ステージの情報)を使ってできる移動はこのような感じになると思います。

往復運動

Enemyは、移動していて外周にあたった、という情報のみを知ることができます。
isHitWall()
 true(真)のとき :壁にぶつかった
 false(偽)のとき:ぶつかってない

前に出てきた条件分岐の処理を応用することで、往復運動を実現できます。

  1. 移動方向 forward_ を持ち、その方向に毎フレーム進む
  2. 壁判定isHitWall() が true になると「壁に当たった」と認識
  3. 反転(180度ターン):現在の方向を逆向きに変更
  4. 継続移動:逆向きのまま動き続け、再び壁に当たるまで直進

つまり、右に進んで壁にぶつかったら左に進み、左の壁にぶつかったらまた右に進む、をくり返すことで往復運動を実現します。

反転の処理

行番号:206行目を見て書き換え

// #05 方向転換ロジック
void Enemy::Trurn180()
{
	if (forward_ == UP)
		forward_ = NONE;
	else if (forward_ == LEFT)
		forward_ = NONE;
	else if (forward_ == DOWN)
		forward_ = NONE;
	else if (forward_ == RIGHT)
		forward_ = NONE;

}

行番号:53 行目を見て有効化

51  //#06 敵の進行方向を変える関数を実装しよう
 52  // 外周を回ってみよう!壁に当たったら方向転換。
 53  if (isHitWall_) { TurnRight(); }  // #06

応用

同様に、TurnLeftかTurnRightを使って、外周を回り続けるには?

避けゲー


image.png

実習課題 避けゲー

目的(これを体験で学ぶ)




ゲーム構成(この体験で作る「避けゲー」の要素)

実行してみてみよう

スクリーンショット 2025-08-01 013357.png

スクリーンショット 2025-08-01 013406.png

スクリーンショット 2025-08-01 013411.png

  1. タイトル画面(スペースキーでスタート)
  2. プレイヤー(左右キーで移動)
  3. 敵(上からランダムに落ちてくる)
  4. 衝突判定(ぶつかったらゲームオーバー)
  5. スコア(何秒生き残ったか)
  6. リザルト画面(結果表示)



実習課題(ゲームをうごかしてみよう)

ここから先は、実習ステップ(やってみよう)

Let's Get Coding!
image.png




課題01:画像を読み込んで表示させよ

読み込み画像

cat.png

cat.png

snake.png

snake.png


読み込む画像ファイルを指定しよう

36~41行目辺り
if( playerImage == -1 )
    playerImage = LoadGraph( "image/XXX.png" );
if( enemyImage == -1 )
    enemyImage = LoadGraph( "image/XXXX.png" );

注意点

やること

  1. Init() 内の LoadGraph 呼び出しを上記のように書き換える。
  2. ビルド & 実行してプレイヤーと敵の画像が表示されるか確認。
  3. 表示されなければファイル名やパスをチェック。

確認ポイント




課題02:プレイヤーの移動速度を設定しよう

スピードを表す変数に値を入れよう 

23行目辺り

C++(他のどの言語でもそうだけど)プログラミングでは、いろいろなものを変数として表現します。
プレイヤーの位置は、初期位置と、スピード、経過時間などの変数から毎フレーム計算されます。

//課題02 プレイヤーの移動速度を設定
playerSpeed = _____f; // 例: 150.0f

用語と基礎説明




課題03:敵の落下速度を設定しよう

プレイヤーは動き始めたけど、今度は敵が落ちてきません。
敵のスピードも、(わざとらしく?)0になってるので、スピードを設定してあげてください。

敵の落下速度を設定しよう  

26行目辺り?
//課題03 敵の落下速度を設定
enemySpeed = _____f; // 例: 150.0f



課題04:敵の生成(出現)タイマーを増やそう

敵がまだ出現しない。。。
敵は、一定時間ごとに設定されたタイマーによって、定期的に現れる仕組みになっている。
現状では、タイマーが動いていないのでタイマーを動かして敵を出現させよう!

ここでは、ゲームの進行管理で重要なフレームについて学ぶよ!

59行目辺り?
//課題04 敵の生成タイマーを加算
enemySpawnTimer = enemySpawnTimer + deltaTime; // ここを書いて敵が定期的に出るようにする

仕組みの補足(フレームごとの動きとタイマーの関係)

enemySpawnTimer = enemySpawnTimer + deltaTime について(数学とプログラムの違い)




課題05:スコアの更新を有効にしよう

フレーム間時間(deltaTime)を使ってスコアの変数を変化させよう

93行目辺り
//課題05 スコアの値を更新
score = score + ?????; // 生き残った時間がスコアになる

仕組みの補足(敵の出現タイマーと同じ考え方)

画面表示の豆知識




課題06:当たり判定をつけよう

当たり判定が働いたら敵を消す/終了にする(条件分岐、関数呼び出し)

86行目辺り

当たり判定の処理を呼び出してみよう。当たり判定の処理は、別のところにIsHit()として記述されています。
それを必要なところで呼び出すと、すべての敵と、プレイヤーがぶつかっているかどうかを判定し、その結果を真偽値(true, false)で、知らせてくれます。

//課題06 当たり判定
if( 0 ) //ここで当たり判定の処理を呼び出してみよう
{
    ClearEnemies(); // 敵を消してみる(実装済み関数を使う)
    // 課題07 リザルト画面へ移行(下につなげる)
}

説明:ついに当たり判定です

ゲームにおける当たり判定とは、画面上のもの同士が「ぶつかったかどうか」を調べて、それに応じた反応(ダメージを受ける/ゲームオーバーになる/アイテムを取るなど)を起こす仕組みです。
 このプログラムでは、キャラクター(プレイヤー)と敵のぶつかり判定に「円と円の距離」を使っています。
プレイヤーと敵の中心の距離を計算して、それが一定以内(キャラクターのサイズの2乗)ならぶつかったと判断します。これは簡単で速い衝突チェックの方法です。

image.png

条件分岐(if文)と真偽値

サブルーチン(関数)としての IsHit() の役割




課題07:リザルト画面へ移行する処理を追加しよう

89行目辺り
//課題06 当たり判定
	if( 0 )
	{
		ClearEnemies();
		//課題07 リザルト画面へ移行
		//->ここに移行処理を追加
	}

画面遷移と Scene(状態遷移)の考え方




おまけ

このゲームの「動く」仕組みのミニ解説

主な変数の意味(実習で触るもの)

フレームレート依存と「距離 = 速さ × 時間」の重要性

なぜこれを使わないと困るのか(詳しい影響)

  1. 動きがマシンによって違う:高性能なPCではフレームレートが高くなりすぎてキャラクターが速くなり、古いPCでは遅くなる。プレイ体験がバラバラになり、公平なゲームにならない。
  2. 入力の反応や操作感が不安定になる:フレームが速いとキーを押した瞬間の移動量が大きく変わるため、プレイヤーが思った通りに操作できない。
  3. 物理的な計算が壊れる:重力や速度の積み重ねなど、時間に依存するシミュレーションはフレームごとに変わると累積誤差やバウンスの違いが発生し、動きがぎこちなくなる。
  4. 再現性がなくなる:同じ操作をしてもフレームレートによって結果が変わるので、バグの再現や調整が難しくなる。
  5. フレーム落ちで急に遅くなる現象(スタッタリング):一時的にフレームレートが下がると移動距離も一気に減る/飛び跳ねたように見えることがある。
追加の対策例




発展課題

ゲームの中で気になるところない?