ゲームエンジニア科体験入学
ゲームエンジニア科体験入学の説明ページ
へびゲーム
体験入学会プログラミング体験教材
はじめに
本日は体験入学会にお越しいただきありがとうございます。
プログラミング初心者向けの体験授業として、C++を使った「スネークゲーム」を完成させます。
コード中の TODO マークに沿って穴埋めし、動くゲームを作りましょう。
HEBIGAMEを動かしてみよう
まずはいったん遊んでみよう
- スペースキーでスタート
- キーボードのアローキー(⬆️⬇️⬅️➡️)で方向転換
- エサを食べると成長するよ!
- 自分の体を食べちゃったり、壁に当たるとゲームオーバー
- スペースキーでリスタート
Step 1: 背景色を設定しよう(TODO #1 ) 62行目付近
ゲーム画面の最初に呼ばれる Stage クラスのコンストラクタ(初期化処理)内で、SetBackgroundColor 関数を使って画面の背景色を設定します。
// TODO #1: ここに背景色を設定するRGBを入れてください
SetBackgroundColor( ____, ____, ____ );- 各値は 0〜255 の範囲で指定します。
- 見やすい色(例:空色など)を選びましょう。
ヒント
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();
}ヒント
SceneManagerクラスのChangeScene()メソッドを呼び出します。- 遷移先のシーンはあらかじめ設定されていることを確認してください。
以上で、全 12 ステップの穴埋め教材が完成です! コードをビルドし、ゲームオーバー時にも正しくシーンが切り替わるか確認してみましょう。 コードをビルドして、動作を確認してみましょう。1 つずつクリアしながら進めてください。
URL
https://github.com/youetsux/HEBIGAME_TAIKEN.git
キャラクターAIを作ってみよう
今日の目標
今日の体験実習では、2Dボンバーマン風ゲームのキャラクターAIのプログラムを作りながら、C++によるゲーム開発の実際の雰囲気を体験してほしいと思います。
- 開発環境
- Windows11
- Visual Studio 2022
- プログラミング言語
- C++
- 使用ライブラリ
- DxLib
C++によるプログラム開発の流れ
- Build → ソースをビルドして実行
- Edit → 指定の行番号の定義を『書き換え』
- Test → 動作を確認し、発見・改善
POINT: 小さな変更を加え、すぐにビルド&実行。これを繰り返すのが開発サイクル!
ゲームAIの種類
- キャラクターAI
- キャラクターAIは文字通り、NPCを動かすために利用される
- 格闘ゲームの敵キャラクターや、RPGで主人公を支援する仲間の動作を決める
- メタAI
- ゲーム進行を補助する
- 一定の条件を満たしたらイベントを開始
- ステージに応じて敵やアイテムの配置を変更したりと、ゲーム画面の裏で色々な処理を行う
- ナビゲーションAI
- キャラクターAIやメタAIに対し、各種情報を提供して動作をサポートする
- 敵キャラクターに主人公の位置や進行方向、障害物に関する情報を与える
- この仕組みがないと、キャラクターAIは正確な動作を行えない
その1.キャラクターを選ぼう
ゲームに登場するキャラクターは、その種類によっていろいろなパラメータを持っています。
プログラミング内でそれらを表現するときは、すべて数値や文字などの値として表現します。
これらのゲーム中で利用する値に名前をつけたものを「変数」と呼びます。
変数の例:
- プレイヤーキャラ(Player)
- 位置 (Position)
- 移動速度 (Speed)
- ジャンプ力 (JumpPower)
- 攻撃力 (AttackPower)
- 所持アイテム ( Items)
- プレイヤーの画像 (PlayerImage)
- ・・・
体験用プログラムでも同様に、敵キャラクターをEnemyという変数で表しています。
Enemyはその中に更に細かい設定用のパラメータ(変数を持っています)。
その中の、表示画像を表す変数を変更してみましょう。
Enemyの表示画像の変更
行番号:74 行目を見て書き換え
42 if (isGraphic) {
43 //#01 敵の画像を選択する!
44 std::string enemyImage = GetEnemyImage(KABOCHA);
45 enemyImage_ = LoadGraph(enemyImage.c_str());
46 }- やること:行74 の
KABOCHAをOTAKU/NEKO/OBAKEに変更 - 学ぶ:変数と配列アクセス(enum→配列インデックス)
その2.初期出現位置を変更してみよう
初期出現位置の設定
初期位置をステージの右下に設定してみよう!
キャラクターは、座標系に設置されます。(座標も変数で表すよ!)
2Dならxy平面、3Dならxyz空間に(10,20)とか(40,50,1)のように設定されます。
座標は、x座標とy座標の値を持っています。
初期位置は、ちょっとした計算によって以下のように設定されています。
- 実際のx座標 = x方向に何マス目? ✕ キャラ幅
- 実際のy座標 = y方向に何マス目? ✕ キャラ高さ
- (一番左は0マス目って数えることに注意!)
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 };- やること:行26 の
5, 5を書き換える- ステージの大きさは、どんな変数で表されているかな?(
STAGE_WIDTH, STAGE_HEIGHT) - 一番外側は壁だから、計算すると。。。
- ステージの大きさは、どんな変数で表されているかな?(
- 学ぶこと:定数、算術演算(掛け算)、座標→ピクセル変換
その3.初期進行方向を変更してみよう
次の目標
次の目標として、ステージの右下から、ステージの左端まで移動させたい
Enemy飲む気はどのように決められているのか見てみよう。(どうせ方向も変数なんでしょ!)
方向を表す変数
初期方向はINIT_DIRという変数に設定されています。
- やること:行30 の
UPを変更して、左向きにする - 学ぶこと:if 文による条件分岐
その4.速度を与えて動かそう
ゲームの移動処理の基本
ゲームのキャラクタは、方向を示すベクトルと、スピード、移動時間(フレーム間時間)で、次のフレームの位置を計算します。
これは、小学生の時に習った、
$\text{移動距離} = \text{速度} \times \text{時間}$
の式に方向がついたものを使います。って言っても、
移動に、方向が加わるだけであとは小学校で習ったのと一緒です。
上の図でいうと、
AからBに1フレームで距離16移動している
スピードは?
下の図のようにx軸方向、y軸方向にはどのぐらい移動してるかな?
みたいな話です。

ゲームのフレーム(画面更新)の仕組み
ゲームは1秒間に何回画面を更新している?
一般的には 60FPS (Frame Per Second) が標準です。FPSとは「1秒あたりのフレーム数」を表し、フレームとは「画面を1回描き直すこと」です。
- フレーム更新の流れ
- キーボードやゲームパッドからの入力を取得
- ゲーム内のキャラクターや敵などの状態を計算(移動や当たり判定)
- 画面に描画する(スプライトや3DモデルをGPUへ指示)
- GPUがフレームバッファに描いた結果をディスプレイに表示
- ΔTime(デルタタイム)
フレーム間の時間差(秒)を取得し、移動距離やアニメーションを滑らかにします。
例:1秒間に60回更新するなら、ΔTime = 1/60 ≒ 0.0167秒。 - なぜ60FPS?
- 人間の目がスムーズに映像と認識するには、30FPS以上あればOKですが、60FPSにするとより滑らかに感じられます。
- ハードウェアやゲームの快適さを考慮したバランスです。
方向とフレーム間時間を考慮した移動処理
1フレームごとにこんな感じに、計算していきます。
速度の変数を変更する
向きが決まったら、その方向にEnemyを進めてみよう。
現在は、(わざとらしく)スピード設定が0になっている。
行番号:16 行目を見て書き換え
14 //#04 敵の移動速度を変更してみよう
15 //ちょうどよい速さはどのぐらいかな?
16 const float SPEED = 0.0f; // #04- やること:行16 の
0.0fを変更する - 学ぶこと:フレームの原理、時間差分(ΔTime)による移動量計算
その5.キャラクターAI(移動処理の追加)
移動パターンを考える
ゲームのキャラクタは、ゲーム内で得た情報を使っていろいろな情報を使って、その場面場面に適した動きを切り替えます(状態遷移)これらを自動的に行うのがAIの仕事になります。
- 状態管理:
enemyState_による生存/死亡フェーズ切替 - 判定ロジック:
isHitWa``ll()やisHitPlayer()で環境を認識 - 行動関数:
TurnRight(),ChasePlayer()など“何をするか”を切り出し - 更新サイクル:
UpdateEnemyAlive()→ 移動 or ターン → アニメーション更新
簡単な移動のパターンを考えてみよう
- 往復運動
- 外周を回る
- ランダムに動く
現在のEnemyは、プレイヤーの情報などを知る手段がないので、自分ができる範囲(自分の位置、ステージの情報)を使ってできる移動はこのような感じになると思います。
往復運動
Enemyは、移動していて外周にあたった、という情報のみを知ることができます。
isHitWall()が
true(真)のとき :壁にぶつかった
false(偽)のとき:ぶつかってない
前に出てきた条件分岐の処理を応用することで、往復運動を実現できます。
- 移動方向
forward_を持ち、その方向に毎フレーム進む - 壁判定:
isHitWall()が true になると「壁に当たった」と認識 - 反転(180度ターン):現在の方向を逆向きに変更
- 継続移動:逆向きのまま動き続け、再び壁に当たるまで直進
つまり、右に進んで壁にぶつかったら左に進み、左の壁にぶつかったらまた右に進む、をくり返すことで往復運動を実現します。
反転の処理
行番号: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;
}- やること:206–217 行目の
NONEに反転方向を書く! - 学ぶ:関数定義、if-else による分岐ロジック
行番号:53 行目を見て有効化
51 //#06 敵の進行方向を変える関数を実装しよう
52 // 外周を回ってみよう!壁に当たったら方向転換。
53 if (isHitWall_) { TurnRight(); } // #06- やること:コメントアウトされている場合は外し、壁判定後に
Turn180()が呼ばれるようにしよう - 学ぶ:ループ内判定、AI の行動サイクル(判定→行動→移動)
応用
同様に、TurnLeftかTurnRightを使って、外周を回り続けるには?
避けゲー
実習課題 避けゲー
目的(これを体験で学ぶ)
- ゲームエンジニアリングの基礎を知る
- C++ と Visual Studio で実際に動くゲームを作る流れを体験する
- ソースコード → ビルド → 実行ファイル という開発の基本パイプラインを理解する
- プログラミングの基本(変数/順次・分岐・反復の3つの構造)を使えるようになる
- エラーが出たときの「原因を調べて直す」デバッグの流れを身につける
- DxLib(ゲーム用ライブラリ)を使って画面に描画し、ゲームの構成要素を組み立てる
ゲーム構成(この体験で作る「避けゲー」の要素)
実行してみてみよう
- タイトル画面(スペースキーでスタート)
- プレイヤー(左右キーで移動)
- 敵(上からランダムに落ちてくる)
- 衝突判定(ぶつかったらゲームオーバー)
- スコア(何秒生き残ったか)
- リザルト画面(結果表示)
実習課題(ゲームをうごかしてみよう)
ここから先は、実習ステップ(やってみよう)
課題01:画像を読み込んで表示させよ
- 説明: プレイヤーと敵の画像を正しく読み込まないと表示されない。
playerImageに "image/cat.png"、enemyImageに "image/snake.png" を読み込む作業を行う。
cat.png | snake.png |
読み込む画像ファイルを指定しよう
36~41行目辺り
if( playerImage == -1 )
playerImage = LoadGraph( "image/XXX.png" );
if( enemyImage == -1 )
enemyImage = LoadGraph( "image/XXXX.png" );注意点
- 画像ファイルがプロジェクト内に存在することを確認する。パスのスペルミスに注意。
やること
Init()内のLoadGraph呼び出しを上記のように書き換える。- ビルド & 実行してプレイヤーと敵の画像が表示されるか確認。
- 表示されなければファイル名やパスをチェック。
確認ポイント
- プレイヤーと敵の画像が画面に表示されているか?
- 画像が表示されない場合、ファイルパスの間違いやファイルの存在を確認したか?
課題02:プレイヤーの移動速度を設定しよう
スピードを表す変数に値を入れよう
23行目辺り
C++(他のどの言語でもそうだけど)プログラミングでは、いろいろなものを変数として表現します。
プレイヤーの位置は、初期位置と、スピード、経過時間などの変数から毎フレーム計算されます。
//課題02 プレイヤーの移動速度を設定
playerSpeed = _____f; // 例: 150.0f- 左右キーを押したときに動く量が変わる。値を変えても動きを試してみよう。
用語と基礎説明
- プログラミングの変数と数学の変数の違い:数学で習う や は「値がわからないもの(代入される記号)」として出てくるけど、プログラムの変数は「値を入れておける箱」で、中の値を変えたり取り出したりできる。
例:playerSpeedという箱に150.0fを入れておいて、使うときにその中身を取り出して移動に使う。 - 座標軸:画面の左上が (0,0) で、右に行くほど X が増え、下に行くほど Y が増える。
(0,0) ┌─────────────> X │ │ v Y - ピクセル:画面を構成する最小の点。位置やサイズはピクセル単位で考える。
例:画像が 64x64 ピクセルなら、横幅が64ピクセル、高さが64ピクセル。 - 画像サイズ:キャラクター画像の幅と高さ。描画位置は中心を基準にしているので、
player.x - CHARACTER_SIZE/2などで左上を計算している。 - 移動速度(playerSpeed):1秒あたり何ピクセル動くかを表す。
player.x += playerSpeed * deltaTime;で実際の移動距離になる。
例:playerSpeed = 150.0f;なら 1秒で 150 ピクセル移動する(0.5秒なら 75 ピクセル)。
課題03:敵の落下速度を設定しよう
プレイヤーは動き始めたけど、今度は敵が落ちてきません。
敵のスピードも、(わざとらしく?)0になってるので、スピードを設定してあげてください。
敵の落下速度を設定しよう
26行目辺り?
//課題03 敵の落下速度を設定
enemySpeed = _____f; // 例: 150.0f- 敵がどれくらい速く落ちてくるかを調整する。難易度に関係する。
- 今の画面のサイズは横✕縦=960x640、640ピクセルを2秒で落ちるにはスピードは?みたいに決める
課題04:敵の生成(出現)タイマーを増やそう
敵がまだ出現しない。。。
敵は、一定時間ごとに設定されたタイマーによって、定期的に現れる仕組みになっている。
現状では、タイマーが動いていないのでタイマーを動かして敵を出現させよう!
ここでは、ゲームの進行管理で重要なフレームについて学ぶよ!
59行目辺り?
//課題04 敵の生成タイマーを加算
enemySpawnTimer = enemySpawnTimer + deltaTime; // ここを書いて敵が定期的に出るようにするenemySpawnIntervalを超えたらCreateEnemy()が呼ばれる。
仕組みの補足(フレームごとの動きとタイマーの関係)
- ゲームは「フレーム」という単位で動いている。1フレームごとに画面を更新する。普通は1秒に60回くらい(60FPS)呼ばれる。
- 毎フレーム、次の順番で処理が行われる:
Update()(状態を変える:敵を落とす、タイマーを増やす、入力を読む)Draw()(画面に今の状態を描く)
deltaTimeは前のフレームからの時間(たとえば 1/60 秒 = 約0.0167)。これをenemySpawnTimerに足して「時間を数える」。enemySpawnTimerが設定された間隔(enemySpawnInterval)を超えたら、その時点で新しい敵が出現する。つまり「何秒たったら出すか」をフレームをまたいで数えている。- 例:
enemySpawnInterval = 0.5fなら、0.5秒ごとに敵が出る。60FPS なら約30フレームごとにCreateEnemy()が呼ばれる。
式 enemySpawnTimer = enemySpawnTimer + deltaTime について(数学とプログラムの違い)
- 数学の式だと左辺と右辺に同じ記号が出てくると変に見えるが、プログラムでは「今の値に新しい分を足して更新する」操作として普通に使う。
これは「箱の中身を取り出して、そこに時間分を足して、また箱に戻す」処理と考えられる。
例:今のタイマーが0.3秒で、deltaTimeが0.0167なら、次のフレームでは 0.3167 秒になる。
課題05:スコアの更新を有効にしよう
フレーム間時間(deltaTime)を使ってスコアの変数を変化させよう
93行目辺り
//課題05 スコアの値を更新
score = score + ?????; // 生き残った時間がスコアになる仕組みの補足(敵の出現タイマーと同じ考え方)
- スコアも
enemySpawnTimerと同じで、毎フレームdeltaTimeを足して時間を数えている。 score += deltaTime;は「生き残った時間」がそのまま点数になる仕組み。60FPSなら1フレームごとに約0.0167ずつ増える。
画面表示の豆知識
- このスコア表示や文字の描画は
DxLibが簡単にしてくれているから少ないコードで書ける。
もしDirectX単体で文字を表示しようとすると、フォントを読み込んでテクスチャを作り、描画用の頂点バッファやシェーダを用意する必要があり、かなりコードが長くなる。DxLibはそういう面倒な下準備を隠してくれていて、DrawFormatStringToHandleなどを呼ぶだけで表示できる。
課題06:当たり判定をつけよう
当たり判定が働いたら敵を消す/終了にする(条件分岐、関数呼び出し)
86行目辺り
当たり判定の処理を呼び出してみよう。当たり判定の処理は、別のところにIsHit()として記述されています。
それを必要なところで呼び出すと、すべての敵と、プレイヤーがぶつかっているかどうかを判定し、その結果を真偽値(true, false)で、知らせてくれます。
//課題06 当たり判定
if( 0 ) //ここで当たり判定の処理を呼び出してみよう
{
ClearEnemies(); // 敵を消してみる(実装済み関数を使う)
// 課題07 リザルト画面へ移行(下につなげる)
}説明:ついに当たり判定です
ゲームにおける当たり判定とは、画面上のもの同士が「ぶつかったかどうか」を調べて、それに応じた反応(ダメージを受ける/ゲームオーバーになる/アイテムを取るなど)を起こす仕組みです。
このプログラムでは、キャラクター(プレイヤー)と敵のぶつかり判定に「円と円の距離」を使っています。
プレイヤーと敵の中心の距離を計算して、それが一定以内(キャラクターのサイズの2乗)ならぶつかったと判断します。これは簡単で速い衝突チェックの方法です。
条件分岐(if文)と真偽値
- 当たり判定の結果は「ぶつかっているか」「ぶつかっていないか」の 真(true)/偽(false) で表される値(真偽値)として返される。
IsHit()はこれを返す関数で、ぶつかっていれば true を、そうでなければ false を返す。 if( IsHit() ) { ... }のように書くと、「ぶつかったときだけ中の処理をする」ことができる。これが 条件分岐 で、プログラミングの3つの基本構造(順次・分岐・反復)のひとつで非常に重要。処理を状況に応じて切り替えるための仕組み。- ここでは「当たっていたら敵を消す/ゲームオーバーにする」といった反応を
ifを使って実現している。
サブルーチン(関数)としての IsHit() の役割
IsHit()という関数が別に作られていて、毎フレームの中で呼ばれています。
ゲームの処理は1フレームごとにUpdate()→Draw()の順に動いていて、そのUpdate()の中で状態チェック(衝突の有無など)を行うためにIsHit()を呼び出します。- このように「特定の処理をまとめて名前をつけ、必要なときに呼び出す仕組み」を サブルーチン(関数、ルーチン)と呼びます。
サブルーチンを使うと、同じ処理を何度も書かずにすみ、プログラムが整理されて読みやすく、直しやすくなります。
課題07:リザルト画面へ移行する処理を追加しよう
89行目辺り
//課題06 当たり判定
if( 0 )
{
ClearEnemies();
//課題07 リザルト画面へ移行
//->ここに移行処理を追加
}- 衝突後にリザルト用のシーンに変えるコードを追加。
- 補足:
GoToResultScene()という関数が用意されている
画面遷移と Scene(状態遷移)の考え方
- 多くのゲームは「Scene(シーン)」という単位で画面ごとの役割を分けて管理している。たとえばタイトル画面、プレイ画面、リザルト画面がそれぞれ別の Scene。
- ある条件が満たされると Scene を切り替える。これを 状態遷移 と呼ぶ。たとえば当たり判定で衝突したら「プレイ中」から「リザルト」へ移る、といった具合。
GoToResultScene()やchangeScene(...)は状態を変える関数で、現在の Scene の状態を終了して新しい Scene に移る(遷移する)処理をまとめている。- 状態遷移は画面遷移だけでなく、キャラクターの状態管理(立っている・走っている・ジャンプ中など)やアニメーションの切り替え、敵のAIの振る舞いの切り替えなど、ゲームのあらゆる部分で使われている。
- 状態遷移を整理しておくと、「何が起きたら次に何を表示するか/どう振る舞うか」が明確になり、ゲーム全体の流れや作りやすさが向上する。
おまけ
このゲームの「動く」仕組みのミニ解説
- プログラミングの3つの基本構造
- 順次:コードは上から下へ動く。初期化 → 毎フレームの処理(Update) → 描画(Draw)と流れる。
- 分岐:
ifで条件によって違うことをする(例:左右キーで動くかどうか)。 - 反復:
forで敵を全部落とす、全部描く。
- 変数:
player.xやscoreは変化する値を保存する箱。 - deltaTime:前のフレームからの時間。これを掛けるとどんなPCでも速さが同じになる。
- 当たり判定:プレイヤーと敵の距離の2乗を比べてぶつかったか判定している。
主な変数の意味(実習で触るもの)
player.x,player.y:プレイヤーの位置。画面上のピクセル座標(左上が0,0)。playerSpeed:プレイヤーが1秒あたり何ピクセル動くか。左右キー入力時に使われる。enemySpeed:敵が1秒あたり何ピクセル落ちるか。落下の速さ。enemySpawnInterval:何秒ごとに新しい敵を出すかの間隔。小さいほど敵がたくさん出る。enemySpawnTimer:出現までの経過時間をためておくタイマー。これがenemySpawnIntervalを超えると敵が出る。score:生き残った時間を蓄積した値。時間経過で増える(=ゲームの得点)。enemies:敵の位置のリスト。forループで全部更新・描画・判定する。CHARACTER_SIZE:プレイヤー・敵のサイズ(直径)。当たり判定や描画位置の計算に使う。
フレームレート依存と「距離 = 速さ × 時間」の重要性
- もし
player.x += playerSpeed;のように deltaTime を掛けずに移動処理を書いた場合、1フレームあたり同じ量だけ動くので、フレームレートが高いと速く、低いと遅くなる。
例:1秒間に 60 フレームだと 60 × 150 = 9000 ピクセル動くが、30 フレームだと 30 × 150 = 4500 ピクセルしか動かない。動きがマシン依存になってしまう。 - これを防ぐには「距離 = 速さ × 時間」の考え方で、
player.x += playerSpeed * deltaTime;のように、前のフレームからの時間(deltaTime)を使って移動量を調整する。
こうすると、どんなフレームレートでも1秒あたり同じ距離だけ動く。 deltaTimeを使うのはゲームでもっとも基本的なテクニックの一つ。
なぜこれを使わないと困るのか(詳しい影響)
- 動きがマシンによって違う:高性能なPCではフレームレートが高くなりすぎてキャラクターが速くなり、古いPCでは遅くなる。プレイ体験がバラバラになり、公平なゲームにならない。
- 入力の反応や操作感が不安定になる:フレームが速いとキーを押した瞬間の移動量が大きく変わるため、プレイヤーが思った通りに操作できない。
- 物理的な計算が壊れる:重力や速度の積み重ねなど、時間に依存するシミュレーションはフレームごとに変わると累積誤差やバウンスの違いが発生し、動きがぎこちなくなる。
- 再現性がなくなる:同じ操作をしてもフレームレートによって結果が変わるので、バグの再現や調整が難しくなる。
- フレーム落ちで急に遅くなる現象(スタッタリング):一時的にフレームレートが下がると移動距離も一気に減る/飛び跳ねたように見えることがある。
追加の対策例
- 固定タイムステップを使って、物理計算だけ一定の時間間隔で何度も更新し、その間の描画は自由に行う方法。時間の積み残しを管理して見た目と計算を分けることで安定した動きにできる。
- 補間を使って描画と物理のズレをなめらかに見せる工夫をする。
発展課題
ゲームの中で気になるところない?
- プレイヤーが画面の外に出るまで移動可能
- どうやって、移動を制限するかな?
- 時間があったら試してみよう
- 敵の見た目の変更
- 敵の出現間隔の変更
- 敵の落下位置をプレイヤーキャラクター付近に変更
- 敵の落下スピードを徐々に上げる