100 行で作るアクションゲーム講習会
後半資料

@kegra

今日の予定(後半)

  • 発展
    • カメラの実装
    • 多数オブジェクトの実装
    • 画面切り替えの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

後半資料のコードについて

面倒くさいのでもう疑似コードしか書きません

科学大生ならどうせ行けるでしょ

今日の予定(後半)

  • 発展
    • カメラの実装
    • 多数オブジェクトの実装
    • 画面切り替えの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

カメラの実装

今の状態ではスクロールの概念が無い

→ 広いマップを表示できない

カメラの実装

考え方は簡単

全ての表示を一定数ずらせばよい

(x, y) → (x - cx, y - cy)

cx, cy : カメラ座標

カメラの実装

主人公が中央になるようにしてみよう (疑似コード)

cx = x - (画面横幅) / 2
cy = y - (画面縦幅) / 2
drawRect(x - cx, y - cy, me_w, me_h)
for(i = 0; i < 8; i++)
    for(j = 0; j < 8; j++)
        if (tiles[i][j] == 1)
            drawRect(j * bs - cx, i * bs - cy, bs, bs)

カメラの実装

主人公が中央になったかな?

余談: カメラ処理はどこに属するか?

今のところメインループの中身は

  • ゲームロジック
  • 物理演算
  • 描画処理

の 3 部分に分かれている。

Q. カメラ座標cx cy の値を決める処理はどこに属するか?


A. カメラもゲーム上のオブジェクトの一種と考えれば
これはゲームロジックが妥当だと思う

カメラの実装

発展問題

  • マップ端まで行くとスクロールが止まるように実装してみよう
  • 画面外に外れたものは描画しない方が軽くなるかもしれない。広いマップを作り、画面外のタイルは描画しないようにしてみよう
  • 主人公が右へ走っているとしたら、主人公は画面の真ん中よりも
    少し左にずれていた方が見やすいかもしれない。
    どのようなカメラアルゴリズムだとプレイヤーは遊びやすいか?

今日の予定(後半)

  • 発展
    • カメラの実装
    • 多数オブジェクトの実装
    • 画面切り替えの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

多数オブジェクトの実装

これまで出てきたゲームの要素は

  • タイルマップ
  • 主人公

だけだったが...

実際のゲームなら

  • 敵キャラ
  • 動く足場
  • 自分や敵の撃った弾

などなど「動く存在」が多数出てくるはず

多数オブジェクトの実装

敵キャラを実装してみよう!

数が変動するので、可変長配列に記録する

各言語の可変長配列

  • C++
    • std::vector
  • Python
    • [] 言語組み込み
  • JavaScript
    • [] 言語組み込み
  • Processing
    • ArrayList

各言語の可変長配列

要素の追加

  • C++
    • arr.push_back(item)
  • Python
    • arr.append(item)
  • JavaScript
    • arr.push(item)
  • Processing
    • arr.add(item)

各言語の可変長配列

要素の条件削除

  • C++
    • arr.erase(remove_if(arr.begin(), arr.end(), cond), arr.end());
  • Python
    • arr = filter(cond, arr)
  • JavaScript
    • arr = arr.filter(cond)
  • Processing

敵キャラの実装

以下の疑似コードは多数のオブジェクトを扱うコードの典型
何となく覚えておこう

enemies = []   // 敵キャラを表す配列
for(i = 0; i < size(enemies); i++) {
    enemies[i].process();   // 更新処理
}
for(i = 0; i < size(enemies); i++) {
    enemies[i].draw();      // 描画処理
}

敵キャラの実装

適当に上下移動する敵を作ってみよう (疑似コード)

// 構造体定義
{
    int x, y;
    int vy;
    int timer;
}

敵キャラの実装

適当に上下移動する敵を作ってみよう (疑似コード)

Enemy::process() {
    if(timer >= 50) {
        vy = -vy;
        timer = 0;
    }
    y += vy;
    timer++;
}

敵キャラの実装

適当に上下移動する敵を作ってみよう (疑似コード)

Enemy::draw() {
    drawRect(x, y, 30, 30)
}

敵キャラの実装

適当に配置してみる (疑似コード)

enemies.add({
    .x = 200, .y = 200,
    .vy = 5, .timer = 0,
})
enemies.add({
    .x = 350, .y = 250,
    .vy = 5, .timer = 0,
})

多数オブジェクトの実装

弾を撃って倒せるようにしてみよう!

弾の実装

(疑似コード)

bullets = []   // 弾を表す配列
for(i = 0; i < size(bullets); i++) {
    bullets[i].process();   // 更新処理
}
for(i = 0; i < size(enemies); i++) {
    bullets[i].draw();      // 描画処理
}

弾の実装

(疑似コード)

// 構造体定義
{
    int x, y;
    int vx;
}

弾の実装

キー入力で弾を撃てるようにしてみよう

面倒なので向き固定で (疑似コード)

if(key_z_down) {
    bullets.add({
        .x = x + me_w / 2,
        .y = y + me_h / 2,
        .vx = 20,
    })
}

弾の実装

(疑似コード)

Bullet::process() {
    x += vx;
}
Bullet::draw() {
    Line(x, y, x + 30, y).draw()
}

弾の実装

画面外に飛んで行った弾は消えるようにする (疑似コード)

bullets.remove_if(
    bullet => abs(bullet.x) > 1000
)

オブジェクト同士の当たり判定

シンプルに2重ループで判定しよう

for(i = 0; i < size(enemies); i++) {
    for(j = 0; j < size(bullets); j++) {
        if (enemies[i].x < bullets[j].x &&
            bullets[j].x < enemies[i].x + enemy_width &&
            enemies[i].y < bullets[j].y &&
            bullets[j].y < enemies[i].y + enemy_height)
        {
            // 弾が当たったときの処理
        }
    }
}

余談: 当たり判定の計算量

物と物の衝突は基本的にかかる

重いので、さまざまな工夫が生まれる

  • タイルマップで計算量軽減
    • グラフィックだけ連続的な動き、中身はタイル扱いとか
  • 4分木空間分割で計算量削減
    • 3Dであれば8分木

やる気ある人は調べてみよう

オブジェクトの削除

こう書きたくなる

for(i = 0; i < size(enemies); i++)
    for(j = 0; j < size(bullets); j++)
        if (...) {
            // 弾が当たったときの処理
            enemies.erase(i);
            bullets.erase(j);
        }

が、ループの途中で要素を削除するのは危険!

オブジェクトの削除

このような書き方が安全 (疑似コード)

for(i = 0; i < size(enemies); i++)
    for(j = 0; j < size(bullets); j++)
        if (...) {
            enemies[i].deleteFlag = true;
            bullets[i].deleteFlag = true;
        }

enemies.remove_if( enemy -> enemy.deleteFlag );
bullets.remove_if( bullet -> bullets.deleteFlag );

発展課題

  • 敵や弾の種類がいっぱいある場合はどうするか?
    • インタフェース(C++なら仮想クラス)で抽象化
  • 敵と地形の判定の実装
  • 敵と敵同士の判定の実装

今日の予定(後半)

  • 発展
    • カメラの実装
    • 多数オブジェクトの実装
    • 画面切り替えの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

画面切り替えの実装

実際のゲームには、「タイトル画面」や「リザルト画面」などがある。

またステージも複数ある。

これらの切り替えはどのように実装すると良いか?

画面切り替えの実装

最も単純なやり方: 「今どの画面にいるか」を変数で保持して切り替える

mode = TitleScreen;

switch(mode) {
    case TitleScreen:
        TitleScreenUpdate();
        break;
    case MaingameScreen:
        MainScreenUpdate();
        break;
    case ResultScreen:
        ResultScreenUpdate();
        break;
}

画面切り替えの実装

先ほどのようなやり方でも問題ないといえば問題ないが、
画面が増えてくると大変になってくる

画面切り替えの実装

ゲーム中の様々な画面をクラスで表し、インタフェースで抽象化する

interface Screen {
    update();
}
class TitleScreen implements Screen {
    update() { /* ... */ }
}
class MaingameScreen implements Screen {
    update() { /* ... */ }
}

画面切り替えの実装

現在の画面を表すオブジェクトを作り、それのupdateを呼ぶ

// 現在の画面
Screen current_screen = TitleScreen();

while(true) {
    current_screen.update();
}

これでメインループの中が単純になる

画面切り替えの実装

単純に以下のような実装にすると、自分自身を破棄してしまう

class TitleScreen {
    update() {
        if(...) {
            current_screen = MaingameScreen();     // NG!
        }
    }
}

(RAII機構を積まずにGCで何とかする言語なら問題ないのかもしれないけど…)

画面切り替えの実装

処理中に画面オブジェクトの破棄が走ると良くないので、その外でやる

  • まず何かしらの変数に次の画面の識別情報を記録
string next_screen = "";

class TitleScreen {
    update() {
        if(...) {
            next_screen = "Maingame";
        }
    }
}

画面切り替えの実装

処理中に画面オブジェクトの破棄が走ると良くないので、その外でやる

  • 次フレームへ移る際に画面オブジェクトの破棄・生成を行う
string nextScreen;

while(true) {
    current_screen.update();
    if(nextScreen) {
        if(nextScreen == "Title") current_screen = TitleScreen();
        if(nextScreen == "Game") current_screen = GameScreen();
        if(nextScreen == "Result") current_screen = ResultScreen();
    }
}

画面切り替えの実装

画面遷移部分も抽象化すると良いかも?

change_screen(next_screen) {
    current_screen = screen_factories[next_screen].make();
}

while(true) {
    current_screen.update();
    if(next_screen)
        change_screen(next_screen);
}

画面切り替えの実装

管理機能を 1 つのクラスにまとめてしまうと便利

Siv3D や Phaser.js などは元からそういうクラスを用意している

  • 課題: このクラスの実装を考えてみよう
class ScreenManager {
    register_screen(string id, ScreenFactory factory);
    change_screen(string id);
    current_screen_update();
};

循環依存の回避

「画面Aから画面Bに移る遷移がある」
ならば⇒「画面Aクラスは画面Bクラスに依存する」

となってしまった場合、依存関係の循環が発生する これは良くない!

なので、基本的に遷移先の画面を表すクラスへの依存を持ってはいけない
上の疑似コードで next_screen がクラス型ではなかったのはこのため!

循環依存の回避

よくある実装はこういう形になっている

main関数などが画面マネージャに各画面を表すクラスを登録し、
各画面は他の画面をIDなどで参照する

余談: 「シーン」

今解説したような「ゲーム上で切り替わる各画面」のことを、

Unityおよびそれに影響を受けた文化圏等では「シーン」と呼ぶ


「シーン作って」と言われたらなんか画面増やしたりステージ増やしたりしたいんだなと思おう



余談:
あくまで「Scene」の語は比較的広く共有された単語に過ぎないことに注意。
UnrealEngineでは「Level」だったり、GameMakerStudioでは「Room」だったりする

余談: 「ゲームループ」

タイトル画面~ゲーム本体~リザルト画面~タイトル画面

のような一周できる画面遷移を俗に「ゲームループ」と呼ぶ

「ちゃんとしたゲーム」としての体裁になる、1 つのベースライン

※「メインループ」とは全くの別概念なので混同しないように
※メインループをゲームループと呼ぶ人もいるけど...

余談: インゲーム/アウトゲーム

  • 飛んだり跳ねたり戦ったりするような
    いわゆる「ゲーム本体」の部分をインゲームと呼ぶ

  • タイトル画面・リザルト画面・ロード画面・設定画面など
    副次的な部分全般をアウトゲームと呼ぶ

ゲーム開発者が時々使ってる用語なので覚えとくとビビらなくて済むかも

発展課題

  • ステージ1,2,3のシーンを作り、タイトル画面で選べるようにしてみよう
  • シーン間で情報を持ち越すにはどうすれば良いか?
    • グローバル変数が一番手っ取り早いけど、行儀良くはない
    • コンストラクタに直接変数を与えようとすると、依存が発生
      → 循環依存になる可能性が...

小休憩

今日の予定(後半)

  • 発展
    • カメラの実装
    • 画面切り替えの実装
    • 多数オブジェクトの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

描画フレームと処理フレーム

前半において、FPSの調整方法には2種類あると説明した

  • 時間測ってスリープ
  • 垂直同期

これらは必要に応じて併用されることがある

ちらっと触れた「複合型FPS調整」ではなく処理自体を2種類に分ける

この場合だとFPSが2種類存在することになる

描画フレームと処理フレーム

  • スリープによる一定FPSの処理 →一定間隔であることが重要な処理
  • 垂直同期による可変FPSの処理 →画面と対応付いていて欲しい処理など

という形で処理を分ける設計が広く使われる

著名な例 :

  • Unity の Update() / FixedUpdate()
  • マイクラなど、オンラインゲームの Tick 概念

マルチスレッドでメインループ自体を複数に分ける設計のほか、
1 つのメインループ内の update 実行回数で帳尻を合わせるケースもある

今日の予定(後半)

  • 発展
    • カメラの実装
    • 画面切り替えの実装
    • 多数オブジェクトの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

大量のオブジェクトを扱う設計

今回は単純にforループで1つずつオブジェクトを見ていくことで実装した

しかし...

  • オブジェクトの種類が多い
  • オブジェクトの数もめちゃめちゃに多い

となってくると、パフォーマンス上の問題が出てくる

大量のオブジェクトを扱う設計

基本的にコンピュータは

  • 同じ形のデータが
  • メモリ上に一直線に並んでいる

場合の計算に強いが、種類が多いとそういう訳に行かない

ループの回し方、オブジェクト管理の仕方の工夫が色々出てくる

  • コンポーネント志向
  • ECS(Entity Component System)

やる気ある人は調べてみてください

今日の予定(後半)

  • 発展
    • カメラの実装
    • 画面切り替えの実装
    • 多数オブジェクトの実装
  • 座学
    • 描画と処理の分離
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

ゲームエンジンは結局何をやっているのか?

今日の内容

  • メインループ
  • FPS 調整
  • ゲームオブジェクトの処理

こうした実装は、本物のゲームプログラマーであれば大体できる

つまり本質的にはUnityなど無くてもゲームは作れる


それでも彼らはUnityなどを使う それはなぜか?

ゲームエンジンは結局何をやっているのか?

「最低限動く」ようなものは誰でも作れる

一方で、高度に細部が最適化されたものは作るのが大変

  • オブジェクト更新処理
  • 描画処理
  • 物理演算

大量の複雑なオブジェクトが絡み合う大規模なゲームでは特に問題!

ゲームエンジンは結局何をやっているのか?

どのゲームでも同じような基盤部分の最適化を

新しいゲームを作るたびにやるのはバカバカしい

→Unityのようなゲームエンジンで労力を節約する

これがゲームエンジンを使う(プログラマ視点の)大きな理由


(プログラマ以外の視点では素材管理とかステージ設計みたいなツールが全部入りというのが大きい)

ゲームエンジンは結局何をやっているのか?

が、そうした重量級のお膳立てが必要なければ


別に必ずしも使わなくて良いと思う(個人の感想です)

おわり

本日はお疲れ様でした!