Yz個人メモ
個人的なメモ
- アルゴリズムの授業
- C++の授業
- 1回目 導入回+文字列
- 2回目 関数と参照・アインズ・ウール・ゴウン
- 補足資料(1.5回目) C言語の関数
- DirectXTK関連(Commonstateってなんぞ)
- DirectXTK関連(頂点情報)
- FBXとufbxで座標合わせをしたときのメモ(どうせUVのときまた頭おかしくなる用)
- クソIT系
- ドラクエ風「商人クラス」で学ぶ getter / setter / public / private
- FBX読み込み処理
- ゲームの市場規模
- ダイクストラ法・BFS・A* を図で理解する課題(時間補習課題)
- GOMファイルフォーマットを作るよ
- Visual Studioの更新うざいよね?
- DxLibで3Dキャラクターの移動(基本)
- AIの課題(5月12日)
- AI モンスターの知能
- 既存ゲームを改良するよ(何様じゃい!)
- 状態遷移でモンスターを管理
- 3Dの移動計算(回転して移動)[ゲーム数学、プログラミング基礎]
- ゲームにおけるフィードバックの研究
- 状態遷移ふたたび
アルゴリズムの授業
1. 線形探索 (Linear Search)
1.1 概要
配列(またはリスト)の先頭から順番に目標値と比較し、一致したらその位置を返す。見つからなければ最後まで調べ終えて「未発見」を返す。
1.2 計算量
- 最良ケース:O(1) … 先頭にあった場合
- 平均ケース:O(n/2) ≒ O(n)
- 最悪ケース:O(n) … 存在しない場合や末尾にあった場合
- 空間計算量:O(1)
1.3 特徴・利点・欠点
- 利点
- ソート不要。要素の順序を問わない
- 実装が非常にシンプル
- 欠点
- 要素数が大きいと比較回数も増大し、性能悪化
- 頻繁に検索する場合は不向き
1.4 実装例 (C++)
// 配列 arr[n] の中から key を線形探索し、見つかればそのインデックスを、
// 見つからなければ -1 を返す
int linearSearch(const std::vector<int>& arr, int key) {
for (size_t i = 0; i < arr.size(); ++i) {
if (arr[i] == key) {
return static_cast<int>(i);
}
}
return -1;
}1.5 適用場面
- データ数が少ない、あるいは検索回数が少ない場合
- 配列がソートされていない、かつソートコストをかけたくない場合
2. 二分探索 (Binary Search)
2.1 概要
ソート済み配列の中央要素と比較し、目標値が小さければ左半分、大きければ右半分を再帰的(またはループ)に探索。範囲を半分ずつ絞っていく。
2.2 計算量
- 最良・平均・最悪ケース:いずれも O(log n)
- 空間計算量:O(1)(ループ版)/O(log n)(再帰版のスタック分)
2.3 特徴・利点・欠点
- 利点
- 大量データでも高速
- 欠点
- 事前にソートが必要(ソートコスト O(n log n))
- 配列へのランダムアクセスが前提
- 実装ミス(境界条件)でバグを生みやすい
2.4 実装例 (C++)
// ソート済み vector の二分探索
int binarySearch(const std::vector<int>& arr, int key) {
int left = 0;
int right = static_cast<int>(arr.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == key) {
return mid;
} else if (arr[mid] < key) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}2.5 適用場面
- データが事前にソートされている
- 検索回数が多く、毎回高速に探したい場合
3. ハッシュ探索 (Hash Search)
3.1 概要
ハッシュ関数でキーからテーブルの領域(バケット)を計算し、直接アクセス。バケット内はリストや連想配列で衝突解消(チェイニング、オープンアドレスなど)する。
3.2 計算量
- 平均ケース:O(1)
- 最悪ケース:O(n) … ただし適切なリサイズやハッシュ関数でほぼ起こらない
- 空間計算量:O(n)(テーブルサイズに応じたオーバーヘッドあり)
3.3 特徴・利点・欠点
- 利点
- 平均O(1)で非常に高速
- キー → 値 の対応に最適
- 欠点
- メモリ使用量が増える
- ハッシュ関数の設計/衝突解消の実装が必要
- 順序付き探索には向かない
3.4 実装例 (C++ 標準ライブラリ利用)
#include <unordered_map>
// key -> value のマップを作成し、検索
int hashSearch(const std::unordered_map<int, std::string>& mp, int key) {
auto it = mp.find(key);
if (it != mp.end()) {
// 見つかったら値を参照できる
return it->second;
}
// 見つからない
throw std::out_of_range("キーが存在しません");
}3.5 適用場面
- キーとデータをペアで管理したい場合
- 頻繁に挿入・削除・検索を行う動的なデータ構造
4. 比較まとめ
特徴 | 線形探索 | 二分探索 | ハッシュ探索 |
|---|---|---|---|
前提条件 | なし | ソート済み | なし |
平均時間計算量 | O(n) | O(log n) | O(1) |
最悪時間計算量 | O(n) | O(log n) | O(n) |
空間オーバーヘッド | ほぼゼロ | ほぼゼロ | テーブル分あり |
実装の易しさ | ◎(簡単) | ○(やや注意) | △(ハッシュ設計) |
用途 | 小規模or単発探索 | 大規模かつ静的 | 動的なキー/値管理 |
5. 使い分けのポイント
- データが小さく、一度だけ検索する → 線形探索
- データはソート可能/ソート済みで、繰り返し検索する → 二分探索
- キーと値の対応を高速に管理したい/挿入・削除も行う → ハッシュ探索
もしそれぞれの詳しい応用例や、別の言語での実装例、あるいは木構造探索(B木・AVL木など)についてもご興味があれば、ぜひお知らせください。
あなた:
ぎりぎりC言語しか勉強していない人がわかる疑似言語表記で、それぞれのアルゴリズムを説明してみてほしい。
ChatGPT:
了解です!
C言語に慣れている人がすんなり読めるように、「C言語風の疑似コード(変数宣言あり、for・if中心、関数呼び出しも明示)」で、
以下3つの探索アルゴリズムを順に説明します。
🔍 1. 線形探索(Linear Search)
● 概要
先頭から順番に調べていって、見つかったらその位置(インデックス)を返す。
● 疑似コード(C風)
int linear_search(int arr[], int size, int key) {
int i;
for (i = 0; i < size; i++) {
if (arr[i] == key) {
return i; // 見つかったのでその位置を返す
}
}
return -1; // 最後まで見つからなかった
}● 説明ポイント
arr… 探す対象の配列size… 配列の要素数key… 探したい値- 見つかると
iを返す、見つからないと-1
🌓 2. 二分探索(Binary Search)
● 概要
中央を調べて、半分ずつ範囲を狭めていく方法(※配列がソートされている必要あり)
● 疑似コード(C風)
int binary_search(int arr[], int size, int key) {
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == key) {
return mid; // 見つかった
} else if (arr[mid] < key) {
left = mid + 1; // 右半分を探す
} else {
right = mid - 1; // 左半分を探す
}
}
return -1; // 見つからない
}● 説明ポイント
leftとrightで調べる範囲を持っておく- 中央(
mid)とkeyを比較 keyが大きければ右、 小さければ左に範囲を狭める
🔳 3. ハッシュ探索(Hash Search)
● 概要
値(key)を使って、直接「探す場所(インデックス)」を計算する方法
→ 計算式: index = hash_function(key)
※ここでは衝突がない簡易な例として説明します
● 疑似コード(C風:簡易ハッシュテーブル使用)
#define TABLE_SIZE 10
int hash_function(int key) {
return key % TABLE_SIZE;
}
int hash_table[TABLE_SIZE]; // 値を保存するテーブル
void insert(int key) {
int index = hash_function(key);
hash_table[index] = key; // ※衝突処理は省略
}
int hash_search(int key) {
int index = hash_function(key);
if (hash_table[index] == key) {
return index; // 見つかった
} else {
return -1; // 見つからない or 衝突で上書きされた
}
}● 説明ポイント
key % TABLE_SIZEで場所を決めるinsert()で格納、hash_search()で検索- ※衝突(同じ場所になること)を処理するには工夫が必要(例:線形探査など)
✨ 比較まとめ(覚えやすく)
種類 | ソート必要 | 速さ(目安) | 簡単さ | 特徴 |
|---|---|---|---|---|
線形探索 | 不要 | 遅い O(n) | ◎ 簡単 | 一番素直な方法 |
二分探索 | 必要 | 速い O(log n) | ○ 普通 | 中央から半分に絞っていく |
ハッシュ探索 | 不要 | 速い O(1) | △ やや難 | 直接位置を計算して探す |
C++の授業
C++講義計画(C言語からの移行・ゲームオブジェクト構築まで)
🎯 目標
C言語を半年学んだ学生が、C++の基礎文法からオブジェクト指向(継承・ポリモーフィズム)を学び、最終的に GameObject クラスを継承してプレイヤーや敵を実装できるようにする。
🧭 全体構成(全15回想定)
| 回 | テーマ | 技術目標 | 演習例 |
|---|---|---|---|
1 | C++導入・Cとの違い |
| Cのprintf版→C++出力へ書き換え |
2 | 関数と参照・オーバーロード | 参照渡し・デフォルト引数・関数多重定義 | 距離計算関数のオーバーロード |
3 | クラス・コンストラクタ入門 |
| Vector2構造体→Vector2クラス化 |
4 | クラスの設計とヘッダ分離 |
| Vector2, Rect クラス実装 |
5 | 動的メモリとコンテナ基礎 |
|
|
6 | コンストラクタ/デストラクタ/コピー | RAIIの考え方 | ファイル読み込み管理クラス |
7 | 継承入門 |
|
|
8 | 仮想関数とポリモーフィズム |
|
|
9 | 純粋仮想関数・抽象クラス |
|
|
10 | スマートポインタ・メモリ安全化 |
|
|
11 | 継承+多態性の活用 |
|
|
12 | クラス間関係・静的メンバ |
|
|
13 | 名前空間・クラス分割整理 |
|
|
14 | 総合演習:ミニゲーム制作 | ゲームループ構築 | コンソール or DxLibで動作確認 |
15 | 発表・振り返り | 設計比較・自己評価 | ソース+UML提出・口頭説明 |
🧩 ステップ詳細
STEP 1〜4:C++基礎をCとの違いで理解
- Cの書き方との比較で、スムーズに導入。
- printf→
std::cout、struct→class、malloc→new など差分理解。
STEP 5〜6:クラスとメモリ管理
- C構造体+関数をクラスに統合。
vectorと RAII の基本を体験。
STEP 7〜9:継承とポリモーフィズム
GameObjectにvirtual void Update()を持たせて多態の体験。Player,Enemy,Itemの派生クラスを一括更新。
for (auto obj : objects) obj->Update();
STEP 10〜11:メモリ安全化
unique_ptr導入でC言語的delete漏れを防止。ObjectManagerによる一元管理へ。
STEP 12〜15:まとめとゲーム制作
- 名前空間整理とクラス分離で設計の見通しをよくする。
- 最終的にプレイヤー・敵・アイテムなどを
GameObject継承で統合。
🎮 最終課題例
タイプ | 内容 |
コンソールRPG |
|
2Dアクション (DxLib) |
|
スネーク風ゲーム |
|
📘 授業設計のポイント
- 毎回「CからC++へどう変わるか」を比較して示す。
- すぐ動かせるコード例を提示。
- 最終回で「OOPでコードがどう整理されたか」を可視化する。
💡 まとめ
15回で「C構文→クラス→継承→仮想関数→安全なゲーム構造」まで自然に到達できる構成。 最終的には、ポリモーフィズムを使って
GameObjectを扱う“C++らしいゲーム設計”を完成させる。
1回目 導入回+文字列
第1回授業:C++導入とCとの違い(入出力・文字列・書式制御)
🎯 目標
C言語の知識をもとに、C++の基本文法と文字列処理を理解する。
iostreamを使った標準入出力を理解する。std::stringとchar[]の違いを学ぶ。coutによる書式制御を体験する。
🧩 授業構成(90分想定)
時間 | 内容 | ポイント |
|---|---|---|
0〜10分 | CとC++の関係 | 「C++はCの拡張」だが、文字列処理や入出力が高機能であることを紹介。 |
10〜30分 | 入出力の基本 |
、
/
の使い方。
の意味。 |
30〜55分 | 文字列の扱い |
と
の違いを説明。代入・連結・比較などを実演。 |
55〜80分 | 書式制御 |
、
、
、
など
の使用。表形式出力の練習。 |
80〜90分 | まとめ・課題説明 | 文字列と入出力のまとめ。次回予告:「関数と参照」へ。 |
🧠 授業のねらい
- C言語で扱いづらかった「文字列」や「入出力フォーマット」がC++で簡潔に扱えることを体感させる。
- 今後の授業で使う
std::stringを習得し、OOPでのクラス利用にもスムーズに入れるようにする。
📘 板書・スライド例
Cの例(文字列と出力)
#include <stdio.h>
int main() {
char name[32];
printf("Input your name: ");
scanf("%s", name);
printf("Hello, %s!\n", name);
return 0;
}C++の例(std::string版)
#include <iostream>
#include <string>
using namespace std;
int main() {
string name;
cout << "Input your name: ";
cin >> name;
cout << "Hello, " << name << "!" << endl;
return 0;
}💬 ポイント
stringは動的にサイズが変わる安全な文字列クラス。- 代入・連結・比較が簡単。
string a = "Hello";
string b = "World";
string c = a + ", " + b;
if (a == "Hello") cout << c << endl;💡 比較:
操作 | C (char[]) | C++ (std::string) |
|---|---|---|
代入 |
|
|
結合 |
|
|
比較 |
|
|
長さ |
|
|
✏️ 書式制御の例
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double pi = 3.14159265;
cout << fixed << setprecision(3);
cout << "pi = " << pi << endl;
cout << setw(10) << setfill('-') << 42 << endl;
return 0;
}💬 ポイント
<iomanip>によりCのprintfに近い整形出力が可能。setw():桁幅指定、setfill():埋め文字、setprecision():小数点桁数指定。
🧩 演習課題
- 名前と年齢を入力し、整形して出力するプログラムを作成。
Name: _____ Age : __ years old std::stringを使って2つの単語を入力し、結合して表示する。<iomanip>を使って3つの数値を整列表示する。
💬 授業後課題(宿題)
std::stringとchar[]の違いを、3つの観点(代入・結合・安全性)で説明せよ。- 書式制御を使って表のようなスコア表示を作るプログラムを書け。
🔎 次回予告
第2回:関数と参照・オーバーロード
C++独自の関数設計(参照渡し・デフォルト引数)を学び、関数の柔軟性を理解する。
補足資料
第1回授業補足編:書式制御とfmtライブラリ入門
🎯 目的
C++における出力整形の幅を広げ、より実践的なフォーマット出力を理解する。
<iomanip>による標準的な書式指定を整理。std::format(C++20)やfmtライブラリの書式構文を紹介。printfとの対応関係を示し、Cからの移行をスムーズにする。
🧩 <iomanip>による標準フォーマットまとめ
操作 | 操作関数 | 効果 | 使用例 |
|---|---|---|---|
桁幅指定 |
| 指定幅に右寄せ |
→
|
左寄せ |
| 出力を左寄せ |
→
|
埋め文字 |
| 空白を指定文字で埋める |
→
|
進数指定 |
/
/
| 10進・16進・8進表示 |
→
|
小数点桁数 |
| 小数点以下の桁数指定 |
→
|
浮動小数点表記 |
/
| 固定小数点/指数表記 |
→
|
💡 ポイント
setw()は 次の出力にのみ有効(毎回指定が必要)。setfill()は 継続して有効。fixedを指定しない場合、整数部分や小数点以下が自動的に最適化される。
🧠 応用例:表形式出力
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
cout << left << setw(10) << "Name" << right << setw(8) << "Score" << endl;
cout << setfill('-') << setw(18) << "" << endl;
cout << setfill(' ');
cout << left << setw(10) << "Alice" << right << setw(8) << 95 << endl;
cout << left << setw(10) << "Bob" << right << setw(8) << 87 << endl;
cout << left << setw(10) << "Chris" << right << setw(8) << 100 << endl;
}出力例:
Name Score
------------------
Alice 95
Bob 87
Chris 100🧩 std::format (C++20)とfmtライブラリ
C++20以降では、Python風の書式指定が可能なstd::formatが導入されています。旧環境ではfmtライブラリ(https://github.com/fmtlib/fmt)を使用できます。
使い方例
#include <format> // C++20以降
#include <iostream>
using namespace std;
int main() {
string name = "Alice";
int score = 95;
double rate = 0.8765;
cout << format("Name: {:<10} | Score: {:>4d} | Rate: {:.2f}\n", name, score, rate);
}出力:
Name: Alice | Score: 95 | Rate: 0.88書式指定子 | 意味 | 例 |
|---|---|---|
| 左寄せ・幅10 |
→ 左寄せ |
| 右寄せ・幅10 |
→ 右寄せ |
| 小数点以下2桁 |
→
|
| 6桁ゼロ埋め |
→
|
| 自動推論 | デフォルト出力 |
💬 fmtライブラリでの使用例(C++17以前)
#include <fmt/core.h>
#include <string>
int main() {
std::string name = "Bob";
int age = 20;
fmt::print("Hello, {}! You are {} years old.\n", name, age);
}出力:
Hello, Bob! You are 20 years old.🔍 比較:Cのprintf関数との違い
操作 | printf | iostream | fmt / format |
|---|---|---|---|
書式構文 |
| 関数チェーン形式 |
ベースのテンプレート式 |
型安全性 | ❌ 弱い | ✅ 強い | ✅ 強い |
可読性 | △ | ○ | ◎ |
柔軟性 | ○ | ○ | ◎(動的組み立て可) |
💡 まとめ
<iomanip>は既存Cライクコードに馴染む標準方式。std::format/fmtは現代的で読みやすく、ゲームデバッグ出力などに最適。- どちらも「Cのprintfより安全で拡張性が高い」という点を体験させるのが狙い。
🔎 次にやるとよい演習
- 数値を右寄せ・左寄せしてスコア表を整形。
fmt::printを使ってデバッグメッセージを出すツール関数を作る。std::formatでログ出力を作り、cout版と比較する。
2回目 関数と参照・アインズ・ウール・ゴウン
🎯 第2回 関数と参照・オーバーロード
🏁 授業目標
- ✳️ C++の関数定義・宣言・呼び出しを理解する
- 🔒 const参照(const &)の使い方を理解する
- 🔁 関数のオーバーロードを使って柔軟な関数設計ができるようにする
🧠 関数の基本
C++の関数は、処理をまとめて呼び出せる仕組みです。
関数には「宣言」と「定義」があり、呼び出すときは名前と引数を使います。
#include <iostream>
using namespace std;
int add(int a, int b); // 宣言
int main() {
int result = add(2, 3);
cout << result << endl; // 5
}
int add(int a, int b) {
return a + b;
}💬 ポイント
- 関数宣言(プロトタイプ)は main より前に書く
- main のあとに本体(定義)を書くのが一般的
🧩 const参照の基本
大きなデータをコピーせずに扱いたいときに便利!
void show(const string& name) {
cout << "Hello, " << name << endl;
}🔒 const参照のメリット
- コピーが発生しない → 高速
- 値を変更できない → 安全
- 一時オブジェクトも受け取れる
種類 | 内容 | 特徴 |
|---|---|---|
値渡し | コピーして渡す | 安全だが遅い場合もある |
参照渡し | 実体を直接操作 | 変更される可能性あり |
const参照 | 読み取り専用参照 | 安全で効率的 |
⚙️ 関数のオーバーロード
同じ名前で引数が違う関数を複数定義できる!
int area(int w, int h) { return w * h; }
double area(double r) { return 3.14 * r * r; }🔎 注意
- 戻り値の型だけ違ってもNG
- 引数の数・型・順序が異なる必要あり
💡 const参照+オーバーロードの組み合わせ
void print(string& s) { cout << "L-Value: " << s << endl; }
void print(const string& s) { cout << "Const: " << s << endl; }👉 実体(変数)ならL-Value版、一時文字列ならconst参照版!
🧪 演習問題
1️⃣ 2つの整数を足す関数 add
2️⃣ 名前を受け取って挨拶する greet(const string&)
3️⃣ 面積を求める area を整数/実数でオーバーロード
🏆 まとめ
- 関数は「処理をまとめる箱」
- const参照で安全・高速な引数渡し
- オーバーロードで同じ名前の関数を使い分け
補足資料(1.5回目) C言語の関数
🧩 補足資料:C言語の関数の基本
🧠 関数とは?
C言語の関数は、処理をひとまとめにして何度も呼び出せる仕組みです。
プログラムの再利用性・可読性を高めるために使います。
#include <stdio.h>
int main() {
hello(); // 関数呼び出し
return 0;
}
// 関数定義
void hello() {
printf("Hello, world!\\n");
}🔹 宣言と定義の違い
名称 | 役割 | 書く場所 | 例 |
|---|---|---|---|
関数宣言(プロトタイプ宣言) | 関数があることをコンパイラに知らせる | mainより上 |
|
関数定義 | 実際の処理内容を書く | mainより下でもOK |
|
💡 ポイント
- 宣言と定義を分けることで、関数を別ファイルに分割できる。
- 宣言だけをヘッダファイル(
.h)に書くのが一般的。
🔸 引数と仮引数の違い
種類 | 意味 | 例 |
|---|---|---|
仮引数(parameter) | 関数の受け取り側の変数 |
の a,b |
実引数(argument) | 関数を呼び出すときに渡す値 |
の 3,5 |
🧭 イメージ図
int add(int a, int b) ← 仮引数
↑ ↑
| |
add(3, 5); ← 実引数🧷 ポインタ渡し
関数に変数の**アドレス(場所)**を渡すことで、呼び出し元の値を直接変更できます。
#include <stdio.h>
void addOne(int* x) {
*x = *x + 1;
}
int main() {
int num = 10;
addOne(&num); // 変数のアドレスを渡す
printf("%d\\n", num); // → 11
}💬 ポイント
int* xは「int型のポインタ」*xは「ポインタの指す実際の値」- 呼び出し元の変数を直接書き換えることができる
⚙️ 戻り値・void・手続きと関数
C言語の関数には、値を返す関数と**値を返さない手続き(void関数)**の2種類があります。
// 値を返す関数
int add(int a, int b) {
return a + b;
}
// 値を返さない(void関数)
void printHello() {
printf("Hello!\\n");
}💡 return文
- 関数の実行を終了し、呼び出し元に値を返す。
- void関数では return を省略可能。
🧾 C言語の関数まとめ
分類 | 説明 | 例 |
|---|---|---|
宣言(プロトタイプ) | 関数の存在を知らせる |
|
定義 | 実際の処理を書く |
|
実引数/仮引数 | 呼び出す値と受け取る変数 |
/
|
値渡し | 値のコピーを渡す | 元の変数は変わらない |
ポインタ渡し | アドレスを渡す | 元の変数が変わる |
戻り値 | 処理結果を返す |
を使う |
void関数 | 戻り値なしの手続き | 画面出力など |
🧠 まとめ
- 宣言=関数の存在を伝える
- 定義=関数の中身を書く
- 引数はコピーか参照かで挙動が変わる
- ポインタ渡しはC言語の“参照渡し”のようなもの
- 戻り値の有無で「手続き」と「関数」を区別できる
練習問題!
🧮 第1問:平均点と評価を返す関数
3科目の点数を入力し、平均点を返す関数と、
その平均に応じて評価(A〜D)を返す関数を作れ。
float getAverage(int a, int b, int c);
char getRank(float avg);📘 仕様
getAverage()… 平均値を計算して返すgetRank()… 80点以上→A、60点以上→B、40点以上→C、それ以下→D
💡 例
入力: 80 70 90
出力:
平均点 = 80.0
評価 = A🔄 第2問:配列の最大値と最小値を求める関数
配列とその要素数を受け取り、最大値と最小値をポインタ経由で返す関数を作れ。
void getMinMax(int arr[], int n, int* min, int* max);📘 仕様
- ループで配列を走査し、
*min,*maxに代入 - mainで表示する
💡 例
入力: {3, 9, 2, 5}
出力: 最大値=9, 最小値=2
💡 ヒント:ポインタで関数内の変数を書き換える例
#include <iostream>
using namespace std;
void AddOne(int* x) {
*x = *x + 1; // *xを通して実際の変数の中身を変更する
}
int main() {
int num = 10;
AddOne(&num); // numのアドレスを渡す
cout << "num = " << num << endl; // 出力:num = 11
}👉 *x はアドレス先の実体、&num は「numの住所」です。
🔁 第3問:配列の平均値より大きい値を表示
整数配列と要素数を引数に受け取り、
平均値を求めて、その平均より大きい要素だけを出力する関数を作れ。
void printAboveAverage(int arr[], int n);📘 仕様
- 関数内で平均値を計算
- 平均より大きい要素をすべて表示
💡 例
入力: {60, 80, 70, 90}
出力:
平均値: 75.0
平均より大きい値: 80 90🧷 第4問:文字列中の特定文字を数える関数
文字列と文字を受け取り、その文字がいくつ含まれているか数える関数を作れ。
int countChar(char str[], char target);または
int CountChar(const string& str, char target);📘 仕様
for文でstr[i] != '\\0'まで走査- 一致したらカウントを増やす
💡 例
入力: "banana", 文字 'a'
出力: 文字 'a' は 3 個含まれています
💡 ヒント:std::stringを1文字ずつ調べるには?
#include <iostream>
#include <string>
using namespace std;
int main() {
string word = "banana";
for (int i = 0; i < word.size(); i++) {
cout << word[i] << " "; // 1文字ずつ出力
}
cout << endl;
}👉 string は [] で1文字ずつアクセスできる。
word.size() で文字数(終端の'\0'を除く)を取得できる。
🧩 第5問:関数を組み合わせて統計出力
以下の関数を組み合わせて、5人の点数から平均・最高・最低を出力するプログラムを作れ。
float getAverage(int arr[], int n);
int getMax(int arr[], int n);
int getMin(int arr[], int n);📘 仕様
- mainで配列を作り、関数を順に呼び出す
- 結果を整形して表示
💡 例
入力: {70, 85, 60, 90, 75}
出力:
平均点: 76.0
最高点: 90
最低点: 60
DirectXTK関連(Commonstateってなんぞ)
✅ 主な機能
CommonStates は以下のような「よくある組み合わせの状態オブジェクト」を提供します。 GitHub+1
- ブレンド状態(BlendState)
Opaque(): ブレンドなし(不透明)AlphaBlend(): アルファ付き(通常の透過)Additive(): 加算ブレンド(光・パーティクル用)NonPremultiplied(): プレマルチプライドでないアルファ用
- 深度/ステンシル状態(DepthStencilState)
DepthDefault(): 通常の深度テスト&書き込みDepthNone(): 深度テスト・書き込みとも無効DepthRead(): 深度確認はするが深度バッファ更新しない/透過用などDepthReverseZ()/DepthReadReverseZ(): 反転Zバッファ使用時用
- ラスタライザ状態(RasterizerState)
CullNone(): カリングなし(両面描画)CullClockwise(),CullCounterClockwise(): カリング方向指定Wireframe(): ワイヤーフレームモード
- サンプラ状態(SamplerState)
PointWrap(),PointClamp(),LinearWrap(),LinearClamp(),AnisotropicWrap(),AnisotropicClamp(): テクスチャサンプラの典型
🧮 使い方(例)
std::unique_ptr<DirectX::CommonStates> states =
std::make_unique<DirectX::CommonStates>(device);
deviceContext->OMSetBlendState(states->AlphaBlend(), nullptr, 0xFFFFFFFF);
deviceContext->OMSetDepthStencilState(states->DepthDefault(), 0);
deviceContext->RSSetState(states->CullCounterClockwise());
auto sampler = states->LinearWrap();
deviceContext->PSSetSamplers(0, 1, &sampler);このように、状態オブジェクトを毎回自作せず「典型パターン」を states から取得して使えます。 GitHub
🔍 なぜ使うといいか
- 状態オブジェクト(BlendState 等)の設定を毎回細かく書くと煩雑。CommonStates によって手軽に済む。
- 状態オブジェクトを重複して生成するのを防げる。状態生成は比較的コストがあるため。
- 読みやすく、間違えにくい。例えば「透過用に深度書き込みを止める」など典型パターンが明確。
- プロジェクトが大きくなっても「この状態セットを使えばOK」という共通の仕様にできる。
⚠️ 注意すべきこと
- CommonStates が提供する「典型の状態」がすべてのケースに最適とは限らない。カスタムなラスタ/ブレンド/深度設定が必要な場合は自分で構築する必要あり。
- 状態オブジェクトを頻繁に切り替えるとパフォーマンスに影響出る可能性あり。CommonStates とは別に描画順/バッチング/状態切り替えを整理するべき。
- サンプラ状態や深度モードを変更することで、既存の描画が崩れる可能性あり(例:カリングなし+両面描画=意図しない裏面が見える、深度無効=描画順が重要になる)。
- ライブラリをリンク/設定ミスすると、「CommonStates の関数が未定義(Linker error)」になることが実際に質問として出ています
よく使う設定集
states は std::make_unique<CommonStates>(device) 済み前提です。
1) 不透明3D(デフォルト)
- 用途:普通の3Dモデル(不透明)
ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthDefault(), 0);
ctx->RSSetState(states->CullCounterClockwise()); // 右手系ならCCW, 左手系なら適宜- メモ:最初はこれ。描画順の考慮が少なくて済む。
2) 透明(アルファブレンド)
- 用途:ガラス/フェンス/半透明UI 等
ctx->OMSetBlendState(states->AlphaBlend(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthRead(), 0); // 深度テストON/書き込みOFF
ctx->RSSetState(states->CullNone()); // 両面必要なら- メモ:後ろから前の順に描く。深度書き込みOFFがコツ。
3) 加算(パーティクル/発光)
- 用途:火花・魔法・グローっぽいもの
ctx->OMSetBlendState(states->Additive(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthRead(), 0);
ctx->RSSetState(states->CullNone());- メモ:重なるほど明るくなる。順番は透明同様、奥→手前。
4) 2D/UI/スプライト
- 用途:テクスチャをそのまま貼るUI/スプライト
ctx->OMSetBlendState(states->AlphaBlend(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthNone(), 0);
ctx->RSSetState(states->CullNone());
auto smp = states->LinearClamp(); // UIはClampが無難
ctx->PSSetSamplers(0, 1, &smp);- メモ:DepthNone にしておくのが楽。拡大縮小が多いなら Linear。
5) スカイボックス
- 用途:立方体の内側に空を描く
ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthRead(), 0); // 背景なので書かない
ctx->RSSetState(states->CullNone()); // 内側を描くため- メモ:ビュー行列の平行移動成分を消すのも忘れずに。
6) デバッグ(ワイヤーフレーム)
- 用途:ジオメトリ確認
ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthDefault(), 0);
ctx->RSSetState(states->Wireframe());7) シャドウ用(深度のみパスの基本)
- 用途:単純な深度プリパスや影マップ(※深度バイアスは別途)
ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthDefault(), 0);
ctx->RSSetState(states->CullCounterClockwise());
// ピクセルシェーダを外す or 深度書き込み専用PSに- メモ:CommonStates には深度バイアスは含まれないので、必要なら自作 RasterizerState を用意。
8) ポストプロセス(フルスクリーン)
- 用途:トーンマップ/ブラーなど
ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthNone(), 0);
ctx->RSSetState(states->CullNone());
auto smp = states->LinearClamp(); // スクリーンスペースはClampが基本
ctx->PSSetSamplers(0, 1, &smp);9) サンプラの目安(用途別)
- ドット絵/ピクセルパーフェクト:
PointClamp()/PointWrap() - 一般/テクスチャ:
LinearWrap()(タイル) orLinearClamp()(UI/ポスト) - 高品質リニア:
AnisotropicWrap()/AnisotropicClamp()(斜めに見る床など)
10) Reverse-Z を使う場合(上級)
- 用途:遠距離のZ精度改善(プロジェクションと深度比較を反転)
ctx->OMSetDepthStencilState(states->DepthReverseZ(), 0); // 不透明
// or 透過時
ctx->OMSetDepthStencilState(states->DepthReadReverseZ(), 0);- メモ:射影行列のZも反転が必要(アプリ側対応)。慣れてからでOK。
使い分けのコツ(超要約)
- 不透明は DepthDefault / Opaque、透過は AlphaBlend / DepthRead
- UI/ポストは DepthNone / CullNone
- 粒子は Additive or AlphaBlend(内容で使い分け)
- スカイボックスは DepthRead / CullNone
- サンプラは UI=Clamp、タイル=Wrap、品質=Anisotropic
DirectXTK関連(頂点情報)
🎯 DirectXTK の主要な頂点構造体一覧(VertexTypes.h)
構造体名 | 含まれる要素 | セマンティクス | 主な用途 | 備考 |
|---|---|---|---|---|
VertexPosition | 位置 |
| 最小構成(点・線など) | シェーダで
だけ使う場合 |
VertexPositionColor | 位置+頂点色 |
,
| 頂点ごとに色を持つラインやデバッグ描画 | 2D線やワイヤーフレーム表示向き |
VertexPositionTexture | 位置+UV |
,
| テクスチャ貼り付け(Quad, Billboard, Sprite3Dなど) | ✅ 最も汎用的で、現在あなたが使っている構成 |
VertexPositionNormal | 位置+法線 |
,
| 照明(ライティング)を行う3Dメッシュ | 影・陰影処理に使う基本形 |
VertexPositionNormalColor | 位置+法線+頂点色 |
,
,
| 頂点色と照明を併用する場合 | ゲームモデルなどでよく使われる |
VertexPositionNormalTexture | 位置+法線+UV |
,
,
| ライティング+テクスチャ | ✅ モデル描画で最も一般的 |
VertexPositionColorTexture | 位置+色+UV |
,
,
| 色つきUI・デカールなど | 色とテクスチャを混ぜたいとき |
VertexPositionNormalTangentTexture | 位置+法線+接線+UV |
,
,
,
| 法線マップ(ノーマルマップ)を使う高品質シェーディング | ⚙️ 高級ライティング用 |
VertexPositionDualTexture | 位置+UV×2 |
,
,
| マルチテクスチャ合成 | 例:ライトマップ+ディフューズ |
🧩 それぞれの使い分け方(実例つき)
1️⃣ VertexPositionTexture
VertexPositionTexture v[] = {
{ { -1, 1, 0 }, { 0, 0 } },
{ { 1, 1, 0 }, { 1, 0 } },
{ { 1, -1, 0 }, { 1, 1 } },
{ { -1, -1, 0 }, { 0, 1 } },
};→ Quadや2Dスプライト、UI表示などに最適。BasicEffect と組み合わせると、SetTextureEnabled(true) で完結。
2️⃣ VertexPositionNormalTexture
VertexPositionNormalTexture v[] = {
{ {0,1,0}, {0,1,0}, {0,0} },
{ {1,0,0}, {0,1,0}, {1,1} },
...
};→ ライトを当てたい3Dモデルや FBX などで使う。BasicEffect::SetLightingEnabled(true) を使えば、方向光・点光源も一瞬で追加できる。
3️⃣ VertexPositionNormalTangentTexture
VertexPositionNormalTangentTexture v[] = {
{ pos, normal, tangent, uv },
};→ ノーマルマップ対応モデル用。
DirectXTK の SkinnedEffect や NormalMapEffect と組み合わせると最短で使える。
4️⃣ VertexPositionColor
VertexPositionColor v[] = {
{ {0,0,0}, Colors::Red },
{ {1,0,0}, Colors::Green },
};→ デバッグ線・グリッド表示・ワイヤーフレーム描画など。BasicEffect の SetVertexColorEnabled(true) をオンにすれば、色付き描画可能。
⚙️ 裏側の仕組み(共通点)
DirectXTK の各頂点構造体は以下のような定義を持っています:
struct VertexPositionTexture
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT2 textureCoordinate;
static const D3D11_INPUT_ELEMENT_DESC InputElements[2];
static const UINT InputElementCount = 2;
};InputElements の定義はライブラリ内にあり、BasicEffect や CreateInputLayout() にそのまま渡せます:
m_device->CreateInputLayout(
VertexPositionTexture::InputElements,
VertexPositionTexture::InputElementCount,
bytecode, bytecodeLength,
m_inputLayout.ReleaseAndGetAddressOf());👉 自分で D3D11_INPUT_ELEMENT_DESC を書かなくてよくなる!
💡 他のEffectとの対応
Effectクラス | 対応する頂点構造体 |
|---|---|
BasicEffect | VertexPosition, VertexPositionColor, VertexPositionTexture, VertexPositionNormalTexture |
SkinnedEffect | VertexPositionNormalTexture(+ボーン情報) |
NormalMapEffect | VertexPositionNormalTangentTexture |
EnvironmentMapEffect | VertexPositionNormalTexture |
DualTextureEffect | VertexPositionDualTexture |
AlphaTestEffect | VertexPositionTexture(アルファテスト付きPS) |
各 Effect は自分が想定している頂点構造に合わせて内部シェーダを持っています。
つまり、構造体を正しく選ぶだけでInputLayoutとシェーダの整合性が取れる。
🧭 まとめ
目的 | 頂点構造体 | 使用する Effect |
|---|---|---|
シンプルなテクスチャ付き板ポリ | VertexPositionTexture | BasicEffect |
光を当てたい3Dモデル | VertexPositionNormalTexture | BasicEffect |
ノーマルマップ付き高品質モデル | VertexPositionNormalTangentTexture | NormalMapEffect |
色付きライン・ワイヤー | VertexPositionColor | BasicEffect |
2枚テクスチャ合成 | VertexPositionDualTexture | DualTextureEffect |
FBXとufbxで座標合わせをしたときのメモ(どうせUVのときまた頭おかしくなる用)
結論(このリポの現状)
同じ見え方にするためのチェックリスト
ありがちな“ズレ”と対処
まとめ
クソIT系
💻 「IT系」と言われる仕事の実態
日本の専門学校や一般企業が「IT系」と呼ぶ職種は、大きく次のように分かれます:CS=Computer Science
| 分類 | 仕事内容 | コンピュータサイエンスとの関係 |
|---|---|---|
| ① システムエンジニア(SIer系) | 顧客の業務システムを設計・構築・納入する。要件定義、設計書作成、発注、ベンダー管理など。 | 理論よりも業務知識・調整力中心。CSとは距離がある。 |
| ② プログラマ(下請け開発) | 上流設計に基づきコードを書く。 | 一部アルゴリズム理解が必要だが、工学というより技能職に近い。 |
| ③ インフラ・ネットワーク職 | サーバやネットワークの構築・保守。 | OS・通信・セキュリティの実務知識中心。理論的なネットワーク科学とは別物。 |
| ④ ITコンサル/導入支援 | 業務効率化やクラウド導入の提案、運用支援。 | テクノロジー理解より業務コンサル色が強い。 |
| ⑤ 企業の情報システム部門(情シス) | 社内システムの選定・調達・保守。 | CSとは無縁。購買・ヘルプデスク業務も多い。 |
🧠 一方の「コンピュータサイエンス」とは
CSは「情報処理の原理」そのものを研究・応用する分野で、主に以下のような内容を含みます。
-
アルゴリズムと計算量理論
-
データ構造・オートマトン理論
-
オペレーティングシステム・コンパイラ
-
グラフィックス・AI・機械学習
-
分散システム・ネットワーク理論
これらは**「技術を創る側」**の知識体系であり、実務で言う「ITを使う/導入する側」とは明確に異なります。
🏢 なぜ専門学校の「IT系」と乖離しているのか
いくつか理由があります:
-
就職先がCSを必要としない業界が多い(SIer・受託開発・情シスなど)
-
教育課程が企業の即戦力(Word/Excel/資格)寄りに設計されている
-
「理論よりスキル(ツール操作)」の需要が高い
-
日本の「IT産業」が技術開発よりも“システム納入業”中心に発展した歴史
つまり、日本では「IT=情報システム導入業界」という構造的な事情があり、
「CS=情報科学的探求」とは異なる職能文化が形成されています。
🎯 まとめ
-
日本で「IT系」と呼ばれる仕事の多くは、CS(理論・開発)ではなく、運用・導入・調整業務。
-
コンピュータサイエンス的な職種は、研究開発・プロダクト開発・ゲーム開発・AI分野など、少数派。
-
専門学校の「IT教育」は、どちらかといえば**職業訓練(実務対応)**寄りで、CS的素養を育てるものではない。
ドラクエ風「商人クラス」で学ぶ getter / setter / public / private
🎮 授業進行カンペ+教材一体版
ドラクエ風「商人クラス」で学ぶ getter / setter / public / private
🧭 授業概要
-
対象:専門学校1年(C++基礎中盤)
-
時間:45分 × 1コマ
-
前提:クラス宣言・メンバ関数は既習
-
目的:getter/setter・public/private の基本構造と目的を理解する
-
進行:
-
バグる商人(問題提起)
-
安全な商人(getter/setter導入)
-
演習問題(小課題+全体レビュー)
-
🧾 提示する資料(学生閲覧用 DokuWiki ページ)
BookStackからリンクまたはQRで提示。学生側にはDokuWiki版をコピー配布。
ページタイトル:
🧭 ドラクエ風「商人クラス」で学ぶ getter / setter / public / private
構成:
-
バグる商人コード(直接代入で壊れる例)
-
安全な商人コード(getter/setter+private)
-
解説表:public/private/getter/setter
-
練習問題5問
-
チェックポイント
提示タイミング:
-
導入5分で「このページを開いてください」
-
教員は1つずつ実行例を見せながら口頭解説
🧩 授業進行
| 時間 | 内容 | 教員アクション |
|---|---|---|
| 0〜5分 | 導入:商人バグ実行 | 「やくそう−100G」など笑いを取りながら興味づけ |
| 5〜15分 | getter/setterの考え方 | 「値の出入口を関数で管理」と板書 |
| 15〜25分 | 安全な商人コード実行 | private→public→setterの流れを追う |
| 25〜35分 | 演習問題 | 1〜4を個人、5はグループ討論 |
| 35〜40分 | 全体レビュー | setterの制限・public/privateを再確認 |
| 40〜45分 | まとめ+次回予告 | 「次は勇者(Player)と取引します」 |
💬 教員トーク例
「この商人、在庫2個で始めたのに、勇者が直接
stock=9999って書いたらどうなる?」
「マイナス価格にしたら勇者が儲かる! これはバグ。
だから金庫(private)に鍵をかけて、店員(getter/setter)を通すんです。」
💻 教示コード(投影または配布用)
① バグる商人(問題提起)
#include <iostream>
#include <string>
using namespace std;
class Merchant {
public:
string itemName;
int price;
int stock;
void Sell() {
if (stock > 0) {
stock--;
cout << itemName << "を" << price << "Gで売った!" << endl;
} else {
cout << "在庫がない!" << endl;
}
}
};
int main() {
Merchant m;
m.itemName = "やくそう";
m.price = 8;
m.stock = 2;
m.Sell();
m.Sell();
m.Sell();
m.stock = 9999; // ← 不正操作
m.price = -100; // ← マイナス価格!
m.Sell();
}
② 安全な商人(getter/setter + private)
#include <iostream>
#include <string>
using namespace std;
class Merchant {
private:
string itemName;
int price;
int stock;
public:
void SetItemName(string name) { itemName = name; }
void SetPrice(int value) {
if (value < 0) value = 0;
if (value > 9999) value = 9999;
price = value;
}
void SetStock(int value) {
if (value < 0) value = 0;
stock = value;
}
string GetItemName() { return itemName; }
int GetPrice() { return price; }
int GetStock() { return stock; }
void Sell() {
if (stock > 0) {
stock--;
cout << itemName << "を" << price << "Gで売った!" << endl;
} else {
cout << "在庫がない!" << endl;
}
}
};
int main() {
Merchant m;
m.SetItemName("やくそう");
m.SetPrice(8);
m.SetStock(2);
m.Sell();
m.Sell();
m.Sell(); // 在庫がない!
// m.stock = 9999; ← エラー
// m.price = -100; ← エラー
}
🧩 授業内演習問題(提示用)
Q1
商人クラスに「商品名」「価格」「在庫」を private で定義し、
それぞれに Set/Get 関数を作成せよ。
Q2
SetPrice() で価格が 0未満なら0、9999より大きければ9999 に制限せよ。
Q3
SetStock() で在庫が マイナスのとき0に補正 されるようにせよ。
Q4
main()関数で商人を作成し、やくそうを2個販売するコードを書け。
(3回目の販売時には「在庫がない!」が出力されること)
Q5(発展)
新しいメンバ関数 ShowStatus() を追加し、
「商品名・価格・在庫」を1行で表示せよ。
💡 教員用解説・ポイント
| 問題 | ねらい | 指導ポイント | 模範コード抜粋 |
|---|---|---|---|
| Q1 | getter/setter の型と戻り値の対応 | void SetXxx(T) と T GetXxx() の対称性を意識 |
void SetItemName(string n){itemName=n;} |
| Q2 | setter での値検証 | if文で範囲制限。固定値ではなく変数を渡す形を見せる | if(value<0)value=0; |
| Q3 | 状態の整合性保持 | 在庫は負数で意味を持たない → 0で丸める | if(value<0)value=0; |
| Q4 | 実行結果の確認 | 出力順に注目。「在庫がない!」が出るまでSell() | m.Sell();m.Sell();m.Sell(); |
| Q5 | 状態表示 | デバッグ・確認に便利。getterの活用を実感させる | cout<<"商品:"<<itemName<<" 在庫:"<<stock<<endl; |
✅ チェックリスト
-
setterで値をチェックできているか
-
private変数を直接触っていないか
-
出力が想定通りか
-
コードに不正アクセス部分(コメントで残してある)が理解できているか
🧩 発展予告(次回)
-
Playerクラスを導入し、
SellTo(Player&)で取引処理 -
has-a関係・参照渡しの導入
-
複数商品対応(vector)へ展開
🎬 まとめコメント例(授業締め)
「今日の目的は“商人の金庫に鍵をかける”でした。
直接代入では壊れる。関数を通せば守れる。
getter/setter は、プログラム世界のルールブックです。」
FBX読み込み処理
全体構造(ロード〜描画の流れ)
ざっくりした流れはこうなっています。
[FBXファイル]
│ (ufbxでロード済み)
▼
[ufbx_scene]
│
├─ ExpandAllNodes(scene) … CPU側メッシュ展開・スキン情報抽出・AABB計算
│
├─ CreateGpuBuffers() … VB / IB 作成
│
└─ CreateEffectsAndTextures(fbx_path, scene)
└─ テクスチャや BasicEffect の初期化
(ここまでがロード時)
描画時:
└─ Draw(world, view, proj)
├─ CPU スキニング (必要なら)
├─ VB を UpdateSubresource で更新
└─ IA 設定 → BasicEffect → DrawIndexedUfbxStaticModel のメッシュ関係で主役になるフィールドは(名前だけ):
mesh_.vertices_ : VertexPNT2 の配列(展開済み頂点)
mesh_.indices_ : uint32_t の配列(三角形インデックス)
mesh_.parts_ : MeshPart の配列(マテリアルごとのサブメッシュ)
mesh_.influences_ : VertexInfluence の配列(頂点ごとのボーン情報)
mesh_.bind_vertices_ : バインドポーズの頂点(=元の姿)
mesh_.skinned_vertices_: スキニング後の頂点(CPUスキニング結果)
draw_.vb_ / draw_.ib_ : D3D11のVB/IB
draw_.fx_ : BasicEffect
draw_.states_ : CommonStates
draw_.layout_ : 入力レイアウトデータ構造イメージ
頂点とインデックス
VertexPNT2:
pos : float3 (頂点位置)
nrm : float3 (法線)
uv : float2 (テクスチャ座標)
頂点バッファ (mesh_.vertices_)
index: 0 1 2 3 4 5 ...
+---+---+---+---+---+---+
| V | V | V | V | V | V |
+---+---+---+---+---+---+
インデックスバッファ (mesh_.indices_)
0,1,2 / 3,4,5 / ...
→ TRIANGLELIST で描画頂点ごとのボーン影響 VertexInfluence
VertexInfluence:
uint16_t bone[4];
float weight[4];
1頂点 = 最大4本のボーンが影響するmesh_.influences_[v] ← 頂点 v に対するボーン情報MeshPart(サブメッシュ)
MeshPart:
const ufbx_material* mat; // 元FBXのマテリアル
uint32_t start_index; // mesh_.indices_ のどこから
uint32_t index_count; // 何個インデックスを描くか
ComPtr<ID3D11ShaderResourceView> srv; // diffuse テクスチャ概念図:
mesh_.indices_:
[0] [1] [2] [3] [4] [5] [6] [7] ...
MeshPart 0:
start_index = 0
index_count = 300
MeshPart 1:
start_index = 300
index_count = 150ExpandAllNodes(scene): メッシュ展開
1. 初期化
mesh_.vertices_.clear();
mesh_.indices_.clear();
mesh_.parts_.clear();
mesh_.influences_.clear();
mesh_.bind_vertices_.clear();
mesh_.skinned_vertices_.clear();AABB 用の min/max も初期化。
bb_min = ( +∞, +∞, +∞ )
bb_max = ( -∞, -∞, -∞ )2. シーン中の全ノードを走査
for each node in scene->nodes:
mesh = node->mesh
if !mesh: continue
…イメージ:
ufbx_scene
├─ node0 (meshなし)
├─ node1 (mesh A)
├─ node2 (mesh B)
└─ ...mesh を持っているノードだけメッシュ展開の対象にします。
3. スキンウェイト抽出 (infl_per_vtx)
メッシュがスキンを持っている場合:
infl_per_vtx.resize(mesh->num_vertices);
skin = mesh->skin_deformers.data[0];
clusters = skin->clusters;
vtx_list = skin->vertices;
w_list = skin->weights;ここでやっていること:
FBXのデータ:
- skin->vertices : 各頂点に対して "どの weight が付いているか" の範囲
- skin->weights : (cluster_index, weight) の配列
- clusters : cluster_index → どのボーン(node)かアルゴリズム(概念):
for each vertex v:
acc[v] = 空のリスト
for each "skinned vertex" sv in vtx_list:
v = sv.vertex(実際の頂点番号)
for k in その頂点に対応する全weight:
w = weights[weight_begin + k]
ci = w.cluster_index
cl = clusters[ci]
bone_node = cl->bone_node
// スケルトン側に登録済みならボーン番号を取得
bone_index = skeleton_.Data().bone_index_of_[bone_node]
acc[v].push_back( (bone_index, weight) )図にすると:
+---------------------------+
| ufbx_skin_deformer (skin) |
+---------------------------+
| clusters[] | vertices[] / weights[]
v v
[ cluster0 ] → bone_node0 vertex0 → (ci, weight)...
[ cluster1 ] → bone_node1 vertex1 → ...
|
└─ bone_node をキーに
skeleton_.Data().bone_index_of_ を引くその後、各頂点について
acc[v]を「weight の大きい順にソート」- 上位 4 本に絞る
- 重みを正規化(合計 ≒ 1.0)
VertexInfluenceに詰めてinfl_per_vtx[v]に保存
4. UV セットの決定
const ufbx_vertex_vec2* base_uv = nullptr;
if (mesh->vertex_uv.exists) base_uv = &mesh->vertex_uv;
else if (mesh->uv_sets.count > 0 && mesh->uv_sets.data[0].vertex_uv.exists)
base_uv = &mesh->uv_sets.data[0].vertex_uv;あとでテクスチャとUVセットの名前を見て、ResolveUVByName() で差し替えもします。
5. フェイスをマテリアルごとにグループ化
std::unordered_map<uint32_t, std::vector<uint32_t>> faces_by_mat;
for each face fi in mesh->faces:
mi = (mesh->face_material.count > 0) ? mesh->face_material.data[fi] : 0;
faces_by_mat[mi].push_back(fi);概念:
faces_by_mat:
mat_index 0 → [faceID 1, 5, 8, ...]
mat_index 1 → [faceID 0, 2, 3, ...]これを後で MeshPart ごとに展開します。
6. マテリアルごとに MeshPart 作成&頂点展開
for each (mat_index, face_list) in faces_by_mat:
MeshPart part;
part.start_index = mesh_.indices_.size();
// node / mesh から ufbx_material* を引く
if (node->materials.count > mat_index) part.mat = node->materials.data[mat_index];
else if (mesh->materials.count > mat_index) part.mat = mesh->materials.data[mat_index];UV セットの最終決定:
tex_for_uv = GetDiffuseTexture(part.mat);
uvv = base_uv;
if (tex_for_uv && tex_for_uv->uv_set.length > 0)
uvv = ResolveUVByName(mesh, tex_for_uv->uv_set);その後、face_list の各フェイスを「扇形分割」で三角形にする:
for each face f in face_list:
indices: f.index_begin ... f.index_begin + f.num_indices - 1
for k = 0 .. (f.num_indices - 3):
corners[0] = f.index_begin + 0;
corners[1] = f.index_begin + (k+1);
corners[2] = f.index_begin + (k+2);
→ この3つで1三角形各 corner について
- 頂点番号
vtx = mesh->vertex_indices[corner] - 位置
P:mesh->vertex_positionから取得 - 法線
N:mesh->vertex_normalから取得+正規化 - UV
T:決定済みuvvから取得し、T.y = 1 - v
そして
VertexPNT2 vtx_out = { P, N, T };
VertexInfluence vi;
if (!infl_per_vtx.empty())
vi = infl_per_vtx[vtx];
mesh_.influences_.push_back(vi);
mesh_.indices_.push_back( mesh_.vertices_.size() );
mesh_.vertices_.push_back( vtx_out );図示すると:
FBX mesh
├─ faces[]
│ ├─ face0 (n角形)
│ └─ face1 ...
├─ vertex_position
├─ vertex_normal
└─ vertex_uv / uv_sets
▼ 展開
mesh_.vertices_ (VertexPNT2)
mesh_.indices_ (三角形リスト)
mesh_.influences_(VertexInfluence)
mesh_.parts_ (マテリアル単位)最後に part.index_count を計算し、>0 なら mesh_.parts_.push_back(part)。
7. シーン半径(scene_radius_)の計算
展開中に AABB を更新しているので、それを使って「おおよその半径」を求めます。
ext = (bb_max - bb_min) * 0.5f;
r = max(|ext.x|, |ext.y|, |ext.z|);
if (r < 1e-3f) r = 1.0f;
skeleton_.Data().scene_radius_ = r;これはボーンのデバッグ描画時の軸の長さ決定に使っています。
8. バインド頂点とスキニング頂点の準備
mesh_.bind_vertices_ = mesh_.vertices_; // バインドポーズ
mesh_.skinned_vertices_ = mesh_.vertices_; // 初期値:同じ描画時の CPU スキニングでは bind_vertices_ を入力として skinned_vertices_ を更新します。
CreateGpuBuffers(): VB/IB の作成
単純に mesh_.vertices_, mesh_.indices_ から ID3D11Buffer を作成しています。
[CPU] mesh_.vertices_ ----CreateBuffer----> draw_.vb_
[CPU] mesh_.indices_ ----CreateBuffer----> draw_.ib_特にスキニング対応のための特殊なことはしておらず、Usage = DEFAULT の普通のVB/IBです。
CreateEffectsAndTextures(): エフェクト&テクスチャ
ここでやっていること:
CommonStatesとBasicEffectを new- BasicEffect にライティング設定を行う
- BasicEffect から VS バイトコードを取り出し、
VertexPNT2に合わせた InputLayout を作成 - FBX ファイルのディレクトリを求める
mesh_.parts_を走査して diffuse テクスチャを読む
テクスチャの読み方は二通り:
1) tex->content に埋め込みバイナリがある
→ CreateWICTextureFromMemory()
2) tex->filename にパスがある
→ FBXファイルのディレクトリと結合して CreateWICTextureFromFile()成功すれば MeshPart::srv に SRV を保持します。
Draw(world, view, proj): CPUスキニング+描画
1. 前提チェック
VB/IB, BasicEffect, layout, indices が揃っているかをチェック。
2. CPU スキニング
if (!mesh_.influences_.empty() && !mesh_.bind_vertices_.empty()) {
skeleton_.SkinMatrices().resize(skeleton_.Bones().size());
for i in 0 .. Bones()-1:
W = CurrWorld()[i] // 現在のボーン姿勢 (ワールド)
G2B = geom_bind_world // ジオメトリ→ボーン
skin_mats[i] = G2B * W // スキン行列スキン行列ののち、ApplySkinCPU() を呼び出し:
入力:
skin_mats : ボーン数
mesh_.influences_: 頂点数
mesh_.bind_vertices_: 頂点数
出力:
mesh_.skinned_vertices_: 頂点数 (更新される)中身は
for each vertex v:
P = 0, N = 0
for k=0..3:
if weight[k] > 0:
B = skin_mats[bone[k]]
P += (B * bind_pos) * weight
N += (B * bind_nrm) * weight
if any:
正規化した N と P を skinned_vertices_[v] に書き戻し
else:
bind_vertices_[v] をそのままコピー最後に
ctx->UpdateSubresource(draw_.vb_.Get(), 0, nullptr,
&mesh_.skinned_vertices_[0], 0, 0);で GPU のVBを更新。
3. IA & ラスタ設定 → MeshPartごとに描画
ctx->IASetInputLayout(draw_.layout_.Get());
ctx->IASetVertexBuffers(..., draw_.vb_.Get(), ...);
ctx->IASetIndexBuffer(draw_.ib_.Get(), DXGI_FORMAT_R32_UINT, 0);
ctx->IASetPrimitiveTopology(TRIANGLELIST);
ctx->OMSetDepthStencilState(DepthDefault);
ctx->RSSetState(CullCounterClockwise);
ctx->PSSetSamplers(0, 1, LinearClamp);行列設定:
fx->SetWorld(world);
fx->SetView(view);
fx->SetProjection(proj);ブレンドステートは
テクスチャあり → NonPremultiplied
テクスチャなし → Opaqueを使い分けています。
最後に
for each MeshPart part in mesh_.parts_:
has_tex = (part.srv != nullptr)
fx->SetTextureEnabled(has_tex);
if (has_tex) fx->SetTexture(part.srv.Get());
fx->Apply(ctx);
ctx->DrawIndexed(part.index_count, part.start_index, 0);まとめ(要点)
ExpandAllNodes()で「FBX → 自前メッシュ構造」へ変換- 頂点 (
VertexPNT2)、インデックス、MeshPart、ボーンウェイト (VertexInfluence)、AABB を作成
- 頂点 (
CreateGpuBuffers()で VB/IB を確保(この段階ではバインドポーズ形状)CreateEffectsAndTextures()で BasicEffect と テクスチャ(SRV)を用意Draw()でFbxSkeletonからボーン姿勢をもらい、スキン行列を計算- CPUスキニングで
bind_vertices_ → skinned_vertices_に変換 - VB を UpdateSubresource → MeshPartごとに BasicEffect で描画
この構造が、今後 FbxMesh に切り出すときの「設計単位」になります。ExpandAllNodes / CreateGpuBuffers / CreateEffectsAndTextures / Draw あたりがそのまま FbxMesh に移るイメージです。
ゲームの市場規模
🎮 日本のゲーム業界:最新市場規模と開発費まとめ(2024–2025)
1. 日本のゲーム市場規模
■ 国内ゲームコンテンツ市場規模(2024年)
- 2兆3,961億円(前年比 +3.4%)
[gamebiz.jp]
要因として、物価上昇によるゲーム機・周辺機器・課金・サブスクの値上げが市場規模の拡大に直結している。
■ 国内ゲーム人口
- 5,475万人(2024年、前年比わずかに減)
[gamebiz.jp]
PC+コンシューマーを併用するプレイヤーが増加し、複数デバイスを活用する層が2478万人に。
プレイスタイルがマルチプラットフォーム化していることが市場の維持に寄与。
2. モバイルゲーム市場(2024年)
日本のゲーム市場の主軸であるモバイルゲームの最新傾向:
■ ダウンロード数
- 3億2,000万DL(前年比 +2.5%)
[sensortower.com], [presswalker.jp]
■ モバイルゲームの収益(IAP)
- 53億ドル(前年比 -17%)※円安の影響
[sensortower.com], [presswalker.jp]
■ 収益ランキングトップタイトル
- 『モンスターストライク』:2.9億ドル(1月〜7月)
[sensortower.com], [presswalker.jp]
■ 収益が大きいIP
- ドラゴンクエスト:1.7億ドル(2024年1〜7月)
[sensortower.com], [presswalker.jp]
3. 世界市場の中での日本の位置づけ
- 世界のゲームコンテンツ市場:31兆円(2024年)
[gamebiz.jp] - モバイルが約60%を占め、依然として世界の中心。
日本は特に**モバイル3Dアニメゲームの世界最大市場(世界シェア49%)**であることが特徴。
[sensortower.com], [presswalker.jp]
4. ゲーム1本あたりの開発費の実態)
■ スマートフォンゲーム開発費(オンラインゲーム会社への調査)
- 平均開発費:4億9,200万円(2024年)
[famitsu.com] - 10年前(2014年)と比較して 4.7倍 に増加
[famitsu.com]
■ ソーシャルゲームの開発費 相場(規模別)
(開発会社ヒアリングベース)
- カジュアル(パズル・2D):数百万円〜1,000万円
- ミッドコア(ストラテジー等):数千万円〜数億円
- ハードコア(RPG / FPS):数億円〜数十億円
- VR/大型オンライン:数十億円〜数百億円
[imitsu.jp]
■ コンシューマー大作タイトル例
- AAAタイトルは数百億円級
例:Cyberpunk2077の開発費 ≒ 314億円
(推定に基づき投じられた総額分析)
[amamiya-ga...nablog.com]
■ オンラインゲーム運営(1タイトルあたり)
JOGAレポートより:
- 開発費に加えて、運営サービス関連コスト(サーバー、CS、アップデート費)が年間で数億円規模
[japanonlinegame.org]
5. その他、産業の雇用・構造
- 日本のゲーム産業総就業人口:約20万人
[cesa.or.jp] - 平均年収:708万円
[cesa.or.jp]
→ クリエイティブ産業としては突出して高い生産性があることが明らか。
6. 学科として強調できるポイント
✔ 日本のゲーム市場は「2.4兆円」の巨大産業で安定成長中
✔ 世界最大級のモバイルゲーム消費国(特に3Dアニメ系は世界シェア50%)
✔ 1タイトルあたりの開発費が急増(スマホ平均5億円 → AAAは300億円級)
✔ 高度な専門職が必要で平均年収も高く、人材育成の価値が高い
✔ 国内外パブリッシャーが継続投資を増やす構造で「人材不足」が深刻
(専門学校・大学のゲーム学科の存在意義が強い)
ダイクストラ法・BFS・A* を図で理解する課題(時間補習課題)
【学習目標】
この課題では次のことができるようになります。
- BFS(幅優先探索)の基本と動き方を説明できる
- ダイクストラ法の仕組みを理解できる
- A* が「何を追加したアルゴリズム」か説明できる
- 図を見て 3つの探索アルゴリズムを比較できる
- ゲーム開発での使い分けが説明できる
🔶【課題図(グラフ構造)】
(1) (1) (1)
[S] ------ [A] ------ [B] ------ [G]
\
\ (5)
\
[C]
\
\ (1)
\
[B](同じB)- S:スタート
- G:ゴール
- 数字:その道を通るコスト
◆ ① BFS(幅優先探索)とは?(穴埋め)
BFS は、
スタートから ( ) の順に探索していくアルゴリズムで、
道のコストが ( ) ときに最短経路を見つけることができる。
選択肢:
- 近いノード
- 深さ
- 同じ
- バラバラ
◆ ② ダイクストラ法とは?(穴埋め)
ダイクストラ法は、
スタートからの ( ) を更新しながら、
( ) が最も小さいノードを順に確定する方法である。
選択肢:
- 最短距離
- コスト
- 深さ
- 未確定ノード
◆ ③ A* アルゴリズムとは?(穴埋め)
A* は、ダイクストラ法に ( ) を追加した方法で、
より ( ) ゴールに到達することを目的としている。
選択肢:
- ヒューリスティック
- ランダム
- 早く
- 正確に
◆ ④ 図を見て考える:BFS / ダイクストラ / A* の動き
Q1. BFS はどのルートを最初に見つける?
BFS が最初に見つけるルートは ( )
理由:__________________________________________________
Q2. ダイクストラ法が求める “最短経路” は?
- 経路:( )
- 合計コスト: ( )
ヒント:
- S→A→B→G = 1+1+1 = 3
- S→C→B→G = 5+1+1 = 7
Q3. A* の式を完成させよう
A* の評価値 f(n) は次の式で求める:
f(n) = g(n) + h(n)
- g(n):Sからそのノードまでの実コスト
- h(n):ゴールまでの ( )
選択肢:
- 予想距離(予想値)
- 完全一致する距離
- 乱数
◆ ⑤ BFSの理解を深める
追加Q1:BFS はどんな順番で探索を進める?
A と B どちらかを選んで、理由を書くこと。
- A:スタート地点から「近いノード」から順番に調べる
- B:コストが小さい道を優先して調べる
回答:A / B(理由:___________________________________)
追加Q2:図を使って BFS の探索順を書こう
例:S → A → B → C → G のように記述。
探索順:_____________________________________
追加Q3:なぜ BFS は「コスト同じのときだけ」最短経路を求められるのか?
穴埋め:
BFS は、道のコストがすべて ( ) とき、
スタートからの ( ) の順に探索するため、
最短経路が求まる。
選択肢:
- 同じ
- バラバラ
- 深さ(段階)
- 重さ
追加Q4:コストがバラバラのときに BFS が失敗する例
BFS はどちらを先に探索する?
- ルート1:S → A → B → G(合計3)
- ルート2:S → C → B → G(合計7)
回答:________________________________________
理由:______________________________________________________
追加Q5:ゲーム開発で BFS が役立つ場面は?
番号を選び、理由を書く。
- 敵から一定距離以内に逃げたいとき
- コストがバラバラのマップで探索したい
- 同じコストのタイルで広いマップを探索するとき
選んだ番号:___
理由:______________________________________
追加Q6:BFS の弱点
図のように“遠回りだが軽い道”があるとき、BFS はどんな失敗をするか?
回答:_______________________________________________
追加Q7:BFS を1行でまとめよ
「BFS は ________ を優先し、コストが ________ ときに最短経路を求められる」
追加Q8:BFS とダイクストラの共通点・相違点(穴埋め)
- 共通点:どちらも( )に沿って探索し、最終的に( )を求める
- 違い:BFS は( )が同じときに最短になるが、
ダイクストラ法は( )が0以上であれば最短を求められる
◆ ⑥ 3つのアルゴリズムの違いを1行ずつまとめよう
- BFS: 道のコストが ( ) とき、 _______ の順に探索する
- ダイクストラ法: コストが0以上なら _______ を必ず見つけられる
- A*: ヒューリスティックを使って _______ 探索する
◆ ⑦ ゲーム開発での使い分け
- 草原でコストが全部同じ
→ ( )
理由:_________________________________________ - コストがバラバラの道(泥・坂・砂)
→ ( )
理由:_________________________________________ - ゴール(敵の位置)が分かっている
→ ( )
理由:_________________________________________
◆ ⑧ 最終まとめ(作文)
ダイクストラ法・BFS・A* の違いを、初心者にもわかるよう詳細に説明せよ。
GOMファイルフォーマットを作るよ
Game Oriented Model Format(GOMフォーマット)を作っていくよ。
シンプルで、アニメーションだけ含んだゲーム制作初心者向けフォーマットだよ。
GOM(Game Oriented Model Format)草案
GOMフォーマット仕様書
Game Oriented Model Format (.gom) Version 1.0 (Complete Specification)
1. 目的
GOMは、FBX等のDCCデータをゲーム実行向けに最適化し、 オフライン変換して保存するためのフォーマットである。
設計目標
- スキニングはモデル(geometry)空間で完結
- インスタンスごとに別Transform・別アニメ時間を安全に扱える
- FBX差(ノード階層・geometry transform)を吸収
- ワールド直スキニング事故を防止
- 実行時にFBXSDKやufbxなどのライブラリに依存しない
- 同一FBXから常に同一GOMが生成される(決定性保証)
2. 空間設計ポリシー(不変条件)
Invariant A: 頂点空間
- 頂点は常に geometry(=モデルローカル)空間
- スキニング結果も geometry 空間
- instanceWorld は描画直前に1回のみ適用
禁止事項
- スキニング途中で world に焼く
- geometry_to_world を途中適用する
3. 行列規約(最終固定)
GOM v1では以下を固定する。
- 数値型:float32
- メモリ並び:row-major
- 演算規約:row-vector 右掛け(p' = p * M)
- 行列は常に 4x4
- 座標系:Left-handed / Y-up / meters
4. ファイル形式
GOMはテキスト形式(GOMT0001)とバイナリ形式(GOMB0001)を持つ。
マジックナンバー(先頭8バイト)
- GOMT0001
- GOMB0001
5. テキスト形式(GOMT0001)
UTF-8(BOMなし)、構造リテラル形式。
5.1 Skeleton
Skeleton = {
boneCount = <int>,
bones = [
{
parent = <int>,
name = "<string>",
path = "<string>",
bindLocal = [[...],[...],[...],[...]]
}
]
}
- bones配列順がboneIndex
- pathは同名衝突対策
5.2 MeshPart
Meshes = [
{
name = "<string>",
meshBindGeoToModel = [[...],[...],[...],[...]],
vertices = [ { p,n,t,uv,bi,bw } ],
indices = [ ... ],
submeshes = [ { materialIndex, indexStart, indexCount } ],
inverseBind = [ [[...]] ]
}
]
ウェイト規約(必須)
- 最大4影響
- 合計1.0正規化
- weight降順、同値はboneIndex昇順タイブレーク
5.3 Animation
Animations = [
{
name = "<string>",
duration = <float>,
rootBoneIndex = <int>,
flags = ["HasRootMotion"],
tracks = [ { bone, keys[] } ]
}
]
補間:T/S=linear、R=slerp
6. MeshPart規約
Rule M1
- FBXメッシュノード単位でMeshPart生成
- マテリアル分割はSUB0で表現
Rule M2
以下の場合はMeshPartを分割する:
- 別スケルトン参照
- inverseBindが異なる
- 頂点レイアウトが異なる
7. バイナリ形式(GOMB0001)
7.1 固定ヘッダ(512 bytes)
Little Endian固定。
7.1.1 固定値
magicは常にGOMB0001(8 bytes ASCII)headerSizeは常に512formatVersionは常に1
7.1.2 列挙値(enum)定義
本仕様の enum はすべて uint32 とする。
未知の enum 値を受け取った場合、読み込みはエラーとする(v1)。
endianness(将来用)
v1ではファイルはLittle Endian固定だが、将来拡張のためヘッダに enum を持つ。
ENDIAN_LE = 1(v1は常にこれ)ENDIAN_BE = 2(予約)
coordinateSystem
CS_LH_YUP_METERS = 1
matrixConvention
行列の「格納順+演算規約」を表す。
MC_ROW_MAJOR_ROW_VECTOR_RIGHT_MUL = 1
定義:
- 行列は row-major で格納される(m00,m01,m02,m03,m10,...)
- 位置は row-vector として扱い
p' = p * M(p=[x y z 1])
7.1.3 validationFlags(bitfield)
validationFlags は uint32。
- bit0(1<<0): bindPosePass
- bit1(1<<1): weightsPass
- bit2(1<<2): submeshMaterialsPass
予約:bit3以降は将来用。v1では0。
7.1.4 ChunkTableの指定
chunkTableOffset == 0の場合:ChunkTableなしchunkTableOffset != 0の場合:ファイル先頭からのバイトオフセットで ChunkTable 先頭を指すchunkTableCountはエントリ数
char magic[8] // GOMB0001
uint32 headerSize // 512
uint32 formatVersion // 1
uint32 coordinateSystem // enum
uint32 matrixConvention // enum
uint8 sourceHash[32] // SHA-256 raw
uint32 converterVersionStr // STR0 index
uint32 validationFlags // bitfield
uint32 chunkTableOffset
uint32 chunkTableCount
uint8 reserved[...] // 0埋め
8. バイナリ全体の堅牢ルール(バイナリ専用)
8.1 Endianness
- Little Endian固定
8.2 アライメント
- チャンクは16バイト境界にアライン
8.3 チャンク基本構造
uint32 fourcc
uint32 size
uint32 version
uint32 flags
byte payload[size]
pad to 16
8.4 チャンクサイズ上限チェック(必須)
- sizeがファイル末尾を超える場合は即エラー
- sizeが実装上限を超える場合はエラー
8.5 未知チャンク
- 未対応fourccはsize分スキップ可能
8.6 ChunkTable(任意)
- chunkTableOffset!=0 の場合、ChunkEntry配列を保持
8.7 MSH0.flags(任意)
- bit0 = indices16
- bit1 = hasGBTM
- 予約ビットは0
9. バイナリチャンク厳密レイアウト
STR0
uint32 stringCount
uint32 offsets[stringCount]
uint32 dataSize
uint8 data[dataSize]
SKEL
uint32 boneCount
int32 parent[boneCount]
uint32 nameId[boneCount]
uint32 pathId[boneCount]
float32 bindLocal[boneCount][16]
MSH0
uint32 meshNameId
uint32 vertexFormat
uint32 vertexCount
uint32 indexCount
uint32 boneCount
uint32 flags
uint32 reserved[2]
VB0
uint32 vertexCount
uint32 stride
uint32 vertexFormat
uint32 reserved
uint8 data[vertexCount*stride]
IB0
uint32 indexCount
uint32 indexSizeBytes
uint32 reserved[2]
uint8 data[indexCount*indexSizeBytes]
SUB0
uint32 submeshCount
struct { int32 materialIndex; uint32 indexStart; uint32 indexCount; }
IBND
uint32 boneCount
float32 inverseBind[boneCount][16]
BND0
float32 aabbMin[3]
float32 aabbMax[3]
float32 sphereCenter[3]
float32 sphereRadius
GBTM(任意)
float32 meshBindGeoToModel[16]
ANIM
uint32 clipCount
CLP0
uint32 clipNameId
float32 duration
float32 sampleRate
int32 rootBoneIndex
uint32 flags
uint32 trackCount
Track構造:
int32 boneIndex
uint32 keyCount
for each key:
float32 time
float32 tr[3]
float32 rot[4]
float32 sc[3]
10. 変換ツール検証(必須)
- Bind整合
- ウェイト合計
- Submesh完全被覆
- boneIndex範囲チェック
- index範囲チェック
11. 実装安全規約(v1推奨)
11.1 読み込み上限値(推奨)
実装は以下の上限を持つことを推奨する(超過時はエラー)。
- maxBoneCount = 4096
- maxVertexCount = 10,000,000
- maxIndexCount = 30,000,000
- maxClipCount = 1024
- maxTrackCount = 8192
- maxKeyCount = 1,000,000(クリップ単位)
※ 実装側で変更可能だが、無制限読み込みは禁止。
11.2 必須チャンク条件(バイナリ)
各 MSH0 は以下の子チャンクを必須とする:
- VB0
- IB0
- SUB0
- IBND
- BND0
GBTM は任意。
IB0.indexSizeBytes は 2 または 4 のみ許可。 それ以外は読み込みエラー。
11.3 アニメキー時刻規約
- keys[].time は昇順必須
- 0 <= time <= duration
- ループ再生時は time = fmod(time, duration) を推奨
11.4 inverseBind の定義
inverseBind[i] は、bind姿勢における boneWorld[i] の逆行列に相当する。
スキニングパレットは以下で構成される:
palette[i] = boneWorld_current[i] * inverseBind[i]
これにより、geometry 空間内でスキニングが完結する。
End of GOM Specification v1.0
GOM Format quick Reference
GOM + MTA フォーマット 簡易仕様書
Game Oriented Model Format + Motion TimeLine Animation
バージョン: GOM v1.0 / MTA v1.0
最終更新: 2026-03-18
📖 目次
概要
GOM(Game Oriented Model)
目的: FBX等のDCCデータをゲーム実行向けに最適化し、オフライン変換して保存する
設計目標:
- スキニングはモデル(geometry)空間で完結
- インスタンスごとに別Transform・別アニメ時間を安全に扱える
- FBX差(ノード階層・geometry transform)を吸収
- ワールド直スキニング事故を防止
- 実行時にFBXSDKに依存しない
- 同一FBXから常に同一GOMが生成される(決定性保証)
MTA(Motion TimeLine Animation / GOMTA)
目的: GOMのアニメーション部分のみを独立させた兄弟フォーマット
特徴:
- メッシュ/マテリアルを含まない
- スケルトン実体を含まない(skeletonSig のみ)
- 複数のアニメーションファイルを用意して動的に切り替え可能
- 異なる GOM ファイルでも同じスケルトンなら適用可能
GOM フォーマット
ファイル形式
| 形式 | マジックナンバー | 説明 |
|---|---|---|
| テキスト | GOMT0001 |
UTF-8、構造リテラル形式 |
| バイナリ | GOMB0001 |
Little Endian、16-byte アライン |
含まれる要素
- Skeleton(SKEL): ボーン階層、bind pose
- Mesh(MSH0): 頂点、インデックス、サブメッシュ
- Material(MATL): Phong最小セット、テクスチャ参照
- Animation(ANIM/CLP0): ボーンアニメーション
- String Table(STR0): 文字列テーブル(名前、パス)
ファイルサイズ
- ヘッダ: 512 bytes
- チャンク: 可変長(16-byte アライン)
MTA フォーマット
ファイル形式
| 形式 | マジックナンバー | 説明 |
|---|---|---|
| バイナリ | MTAB0001 |
Little Endian、16-byte アライン |
注意: v1 ではテキスト形式(MTAT0001)は非サポート。詳細は テキストフォーマット セクション参照。
含まれる要素
- String Table(STR0): 文字列テーブル(必須)
- Animation(ANIM/CLP0): アニメーションクリップ
- skeletonSig: スケルトン互換性チェック(SHA-256, 32 bytes)
除外される要素
- ❌ メッシュ(MSH0, VB0, IB0, SUB0, IBND)
- ❌ マテリアル(MATL)
- ❌ スケルトン実体(SKEL)
- ❌ Geometry Transform(GBTM)
スケルトン互換性チェック
実行時判定:
if (memcmp(gom.skeletonSig, mta.skeletonSig, 32) != 0) {
error("Skeleton signature mismatch!");
}
skeletonSig 生成:
SHA-256(
boneCount (uint32, little-endian) +
foreach boneIndex in bonePath 辞書順:
bonePath (UTF-8) + '\0' + parentIndex (int32, little-endian)
)
基本規約
空間設計ポリシー(Invariant A)
GOM の最重要設計原則。
Invariant A: 頂点空間
- 頂点は常に geometry(=モデルローカル)空間
- スキニング結果も geometry 空間
instanceWorldは 描画直前に1回のみ適用
禁止事項:
- ❌ スキニング途中で world に焼く
- ❌ geometry_to_world を途中適用する
描画時の変換式:
// 正しい変換順序
vec4 skinnedPos = vertex * skinning; // geometry 空間
vec4 worldPos = skinnedPos * GBTM * instanceWorld; // world 空間
vec4 clipPos = worldPos * viewProj; // clip 空間
この設計の利点:
- インスタンシング安全: 同一メッシュを異なる位置/回転で配置可能
- アニメーション独立: インスタンスごとに異なるアニメ時間を適用可能
- FBX差の吸収: ノード階層の違いを完全に隠蔽
- 事故防止: ワールド直スキニングによる破綻を防止
行列規約
| 項目 | 値 |
|---|---|
| 数値型 | float32 |
| メモリ並び | row-major |
| 演算規約 | row-vector 右掛け(p' = p * M) |
| 行列サイズ | 4x4(常に) |
行列演算の具体例:
// ベクトルは行ベクトルとして扱う
vec4 p = {x, y, z, 1.0f};
vec4 p_transformed = p * M; // 右から掛ける
// 複数の変換
vec4 result = p * M1 * M2 * M3; // 左から順に適用
メモリレイアウト(row-major):
float m[16] = {
m00, m01, m02, m03, // 第1行
m10, m11, m12, m13, // 第2行
m20, m21, m22, m23, // 第3行
m30, m31, m32, m33 // 第4行
};
// アクセス方法
float value = m[row * 4 + col];
変換順序の例:
// スキニング → GBTM → インスタンス変換 → ビュー → プロジェクション
vec4 worldPos = vertex * skinning * GBTM * instanceWorld;
vec4 viewPos = worldPos * view;
vec4 clipPos = viewPos * projection;
// または一度に
vec4 clipPos = vertex * skinning * GBTM * instanceWorld * view * projection;
座標系
| 項目 | 値 |
|---|---|
| ハンドネス | Left-handed |
| 上方向 | Y-up |
| 単位 | meters |
バイナリ規約
| 項目 | 値 |
|---|---|
| エンディアン | Little Endian |
| アラインメント | 16-byte |
| パディング値 | 0x00 |
ファイル構造
GOM/MTA バイナリファイル構造
GOM ファイル構造
+-------------------------+ offset 0
| Header (512 bytes) |
+-------------------------+ offset 512
| STR0 Chunk |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| MATL Chunk |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| SKEL Chunk (任意) |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| MSH0 Chunk |
| header (16) |
| VB0, IB0, SUB0, |
| IBND, BND0, GBTM |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| ANIM Chunk (任意) |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| CLP0 Chunk(s) (任意) |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+
MTA ファイル構造
+-------------------------+ offset 0
| Header (512 bytes) |
| skeletonSig (32 bytes)|
| at offset 76 |
+-------------------------+ offset 512
| STR0 Chunk (必須) |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| ANIM Chunk |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| CLP0 Chunk #1 |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| CLP0 Chunk #2 |
| header (16) |
| payload (...) |
| padding (0x00) |
+-------------------------+ ALIGN16(...)
| ... (複数クリップ可) |
+-------------------------+
主な違い:
- GOM: MATL, SKEL, MSH0 を含む(フルモデルデータ)
- MTA: STR0, ANIM, CLP0 のみ(アニメーションのみ)
- MTA ヘッダ: offset 76 に skeletonSig (32 bytes) を含む
ヘッダ構造(512 bytes)
GOM ヘッダ
| オフセット | フィールド | 型 | サイズ | 説明 |
|---|---|---|---|---|
| 0 | magic | char[8] | 8 | "GOMB0001" |
| 8 | headerSize | uint32 | 4 | = 512 |
| 12 | formatVersion | uint32 | 4 | = 1 |
| 16 | coordinateSystem | uint32 | 4 | = 1 (LH/Y-up/meters) |
| 20 | matrixConvention | uint32 | 4 | = 1 (row-major) |
| 24 | sourceHash | uint8[32] | 32 | SHA-256 of source |
| 56 | converterVersionStrId | uint32 | 4 | STR0参照ID |
| 60 | validationFlags | uint32 | 4 | ビットフィールド |
| 64 | chunkTableOffset | uint64 | 8 | 0 = なし |
| 72 | chunkTableCount | uint32 | 4 | ChunkEntry数 |
| 76 | reserved | uint8[436] | 436 | 0埋め |
MTA ヘッダ
| オフセット | フィールド | 型 | サイズ | 説明 |
|---|---|---|---|---|
| 0 | magic | char[8] | 8 | "MTAB0001" |
| 8 | headerSize | uint32 | 4 | = 512 |
| 12 | formatVersion | uint32 | 4 | = 1 |
| 16 | coordinateSystem | uint32 | 4 | = 1 (LH/Y-up/meters) |
| 20 | matrixConvention | uint32 | 4 | = 1 (row-major) |
| 24 | sourceHash | uint8[32] | 32 | SHA-256 of source |
| 56 | converterVersionStrId | uint32 | 4 | STR0参照ID |
| 60 | validationFlags | uint32 | 4 | v1 では 0 固定 |
| 64 | chunkTableOffset | uint64 | 8 | 0 = なし |
| 72 | chunkTableCount | uint32 | 4 | ChunkEntry数 |
| 76 | skeletonSig | uint8[32] | 32 | SHA-256 of skeleton |
| 108 | reserved | uint8[404] | 404 | 0埋め |
重要な違い:
- GOM: offset 76 から reserved[436]
- MTA: offset 76 から skeletonSig[32]、offset 108 から reserved[404] GOM ヘッダテーブルと MTA ヘッダテーブルの後に以下を追加:
validationFlags ビット定義
validationFlags は変換ツールが実行した検証をビットフラグで記録する。
| ビット | マスク | 名称 | 意味 |
|---|---|---|---|
| bit0 | 1<<0 | VALIDATED_BIND_MATRICES | inverseBind * bindLocal = Identity を検証済み |
| bit1 | 1<<1 | VALIDATED_WEIGHTS | 全頂点のウェイト合計が 1.0 であることを検証済み |
| bit2 | 1<<2 | VALIDATED_SUBMESHES | サブメッシュが完全被覆(隙間・重複なし)を検証済み |
| bit3 | 1<<3 | VALIDATED_INDICES | 全インデックスが範囲内を検証済み |
| bit4 | 1<<4 | VALIDATED_BONE_INDICES | 全boneIndexが範囲内を検証済み |
| bit5 | 1<<5 | VALIDATED_MATERIAL_INDICES | 全materialIndexが範囲内を検証済み |
その他のビットは予約(v1では0)。
変換ツール側(書き込み):
uint32_t flags = 0;
flags |= (1 << 0); // VALIDATED_BIND_MATRICES
flags |= (1 << 1); // VALIDATED_WEIGHTS
flags |= (1 << 2); // VALIDATED_SUBMESHES
flags |= (1 << 3); // VALIDATED_INDICES
flags |= (1 << 4); // VALIDATED_BONE_INDICES
flags |= (1 << 5); // VALIDATED_MATERIAL_INDICES
header.validationFlags = flags;
ローダー側(読み込み):
// 参考情報として使用してもよいが、
// ローダーは独自の検証を省略してはならない
bool bindValidated = (header.validationFlags & (1 << 0)) != 0;
bool weightsValidated = (header.validationFlags & (1 << 1)) != 0;
// 未知のビットは無視(警告を出してもよい)
uint32_t unknownBits = header.validationFlags & ~0x3F; // bit0-5以外
if (unknownBits != 0) {
// 警告: 未知の検証フラグが立っている
}
重要な規約:
- 変換ツールは実行した検証に対応するビットを立てる
- ローダーは
validationFlagsを参考情報として扱う - ローダーは
validationFlagsに関係なく独自の検証を実行しなければならない - 未知のビットは無視してよい(警告推奨)
チャンク共通構造(16 bytes)
struct ChunkHeader {
uint32_t fourcc; // チャンク識別子(4文字)
uint32_t size; // ペイロードサイズ(バイト)
uint32_t version; // v1 では 1 固定
uint32_t flags; // v1 では 0 固定
};
fourcc 規約
フォーマット:
- 正確に 4 文字 の ASCII 文字
- 大文字のみ
- 数字を許可
- 文字セット:
[A-Z0-9]
3文字 fourcc の扱い:
3文字の識別子(例: VB0, IB0)は、null 終端で 4 バイトに格納する:
'VB0\0' // null終端で4バイト
'IB0\0' // null終端で4バイト
有効な例:
STR0,MATL,SKEL,MSH0VB0,IB0,SUB0,IBNDBND0,GBTM,ANIM,CLP0
無効な例:
str0(小文字は不可)Mesh(小文字を含む)MAT(長さが 4 でない)MATLL(長さが 4 を超える)
実装例:
// fourcc を uint32 として扱う(little-endian)
uint32_t fourcc_vb0 = 0x30304256; // 'VB0\0' を little-endian で格納
// V=0x56, B=0x42, 0=0x30, \0=0x00
// バイト配列として比較
if (memcmp(&chunk_fourcc, "VB0\0", 4) == 0) {
// VB0 チャンク処理
}
注意事項:
- fourcc は 必ず 4 バイト として扱う
- 3文字の場合は null 終端(
\0) で埋める - スペース(
)埋めは使用しない
主要チャンク
STR0(文字列テーブル)- 必須
構造:
stringCount: uint32
offsets: uint32[stringCount]
stringData: char[] // UTF-8, null終端
規約:
- 重複排除
- 辞書順ソート(UTF-8 byte-wise)
- ID: 0xFFFFFFFF = なし
MATL(Material Slots)- GOM 必須
構造:
materialSlotCount: uint32
slots: MaterialSlot[materialSlotCount]
MaterialSlot:
- nameId: uint32
- flags: uint32
- kd, ks, ke: float32[3]
- shininess, opacity: float32
- diffuseMapId, normalMapId, specularMapId, emissiveMapId: uint32
デフォルト白マテリアル:
- kd = (1.0, 1.0, 1.0)
- ks = (0.04, 0.04, 0.04)
- ke = (0.0, 0.0, 0.0)
- shininess = 32.0
- opacity = 1.0
SKEL(Skeleton)- GOM 任意
構造(SoA形式):
boneCount: uint32
parent: int32[boneCount] // -1 = 親なし
nameId: uint32[boneCount] // STR0参照
pathId: uint32[boneCount] // STR0参照
bindLocal: float32[boneCount][16] // row-major 4x4
規約:
- parent[i] < i(循環参照防止)
- bonePath 辞書順でソート
MSH0(MeshPart Container)- GOM 必須
内部チャンク:
- VB0: 頂点バッファ
- IB0: インデックスバッファ(uint32)
- SUB0: サブメッシュ定義
- IBND: InverseBind行列
- BND0: BindLocal行列(任意)
- GBTM: MeshBindGeoToModel(任意)
VF01 頂点フォーマット(72 bytes):
position: float32 x3 (12 bytes)
normal: float32 x3 (12 bytes)
tangent: float32 x4 (16 bytes) // w=handedness
uv0: float32 x2 (8 bytes)
boneIndex: uint16 x4 (8 bytes)
boneWeight: float32 x4 (16 bytes)
ANIM / CLP0(Animation)
ANIM 構造:
clipCount: uint32
clips: CLP0[clipCount]
CLP0 構造:
nameId: uint32
duration: float32 // seconds
rootBoneIndex: int32
interpMode: uint32 // IM_LINEAR_SLERP = 1
flags: uint32 // HasRootMotion, Loop(予約)
trackCount: uint32
tracks: Track[trackCount]
Track 構造:
boneIndex: uint32
trKeyCount, rotKeyCount, scKeyCount: uint32
trKeys: TrKey[]
rotKeys: RotKey[]
scKeys: ScKey[]
補間規約:
- Translation/Scale: 線形補間
- Rotation: slerp(dot<0 なら flip)
ウェイト正規化
規約
GOM v1 では、すべての頂点のボーンウェイトは以下の規約に従う:
-
最大4影響: 5つ以上の影響は上位4つのみ保持
-
合計1.0: ウェイト合計を 1.0 に正規化
-
ソート規約:
boneWeight降順- 同値の場合は
boneIndex昇順
正規化アルゴリズム
入力: n 個のボーン影響 (boneIndex[i], boneWeight[i])
出力: 最大4個の正規化された影響
手順:
// 1. weight 降順、同値なら boneIndex 昇順でソート
std::sort(influences.begin(), influences.end(), [](auto& a, auto& b) {
if (a.weight != b.weight) return a.weight > b.weight; // 降順
return a.boneIndex < b.boneIndex; // 昇順
});
// 2. 上位4つのみ保持
if (influences.size() > 4) {
influences.resize(4);
}
// 3. 合計を計算
float sum = 0.0f;
for (auto& inf : influences) {
sum += inf.weight;
}
// 4. 正規化
for (auto& inf : influences) {
inf.weight /= sum;
}
// 5. 4個未満の場合は0埋め
while (influences.size() < 4) {
influences.push_back({0, 0.0f});
}
例
例 1: 3影響
入力: bi=[3,1,2], bw=[0.5,0.3,0.2]
出力: bi=[3,1,2,0], bw=[0.5,0.3,0.2,0.0] // 既に降順、0埋め
例 2: ソートが必要
入力: bi=[1,3,2], bw=[0.3,0.5,0.2]
ソート後: bi=[3,1,2], bw=[0.5,0.3,0.2]
出力: bi=[3,1,2,0], bw=[0.5,0.3,0.2,0.0]
例 3: 5影響(トリミング必要)
入力: bi=[0,1,2,3,4], bw=[0.4,0.3,0.2,0.08,0.02]
上位4つ: bi=[0,1,2,3], bw=[0.4,0.3,0.2,0.08]
合計: 0.98
正規化: bi=[0,1,2,3], bw=[0.408,0.306,0.204,0.082]
例 4: 同値の場合
入力: bi=[3,1,2], bw=[0.5,0.25,0.25]
ソート: bi=[3,1,2], bw=[0.5,0.25,0.25] // 0.25 同値は boneIndex 昇順
出力: bi=[3,1,2,0], bw=[0.5,0.25,0.25,0.0]
検証
変換ツールは以下を保証しなければならない:
- [x] すべての頂点が 4 影響以下
- [x] ウェイト合計が 1.0 ± 1e-4
- [x] boneWeight 降順、同値なら boneIndex 昇順
- [x] boneIndex が範囲内(
0 <= boneIndex < boneCount)
決定性保証
ソート規約
すべての順序は UTF-8 byte-wise 辞書順 で確定:
- bonePath: SKEL の boneIndex 順序
- meshNodePath: MSH0 の MeshPart 順序
- materialKey: MATL の materialIndex 順序
- clipName: MTA の CLP0 順序
比較方法
// UTF-8 byte-wise 比較(memcmp / strcmp)
// ロケール依存禁止
// 大文字小文字折りたたみ禁止
浮動小数点
- 出力直前に
float32へキャスト - VertexKey は bitwise 一致比較
- 検証は許容誤差(1e-5 推奨)
互換性
GOM v1.0
サポート範囲:
- スキンメッシュ / スキンなしメッシュ
- Phong 最小マテリアル
- ボーンアニメーション(T/R/S)
- ルートモーション抽出(X/Z移動、Y軸回転)
制限:
- 頂点フォーマット: VF01 のみ
- UV: 1セットのみ
- インデックス: uint32 のみ
- 頂点カラー: 非対応
MTA v1.0
サポート範囲:
- 複数アニメーションクリップ
- skeletonSig 互換性チェック
- 決定性保証
制限:
- GOM の SKEL と互換性がある場合のみ適用可能
- boneIndex は GOM の boneCount 未満でなければならない
実行時互換性チェック
GOM 側:
// SKEL から skeletonSig を計算
uint8_t gom_sig[32];
compute_skeleton_sig(skel, gom_sig);
MTA 側:
// ヘッダから skeletonSig を読み込み
uint8_t mta_sig[32];
memcpy(mta_sig, mta_header.skeletonSig, 32);
判定:
if (memcmp(gom_sig, mta_sig, 32) != 0) {
error("Skeleton mismatch!");
}
実装ガイド
推奨上限値
| 項目 | 推奨値 |
|---|---|
| maxBoneCount | 4096 |
| maxVertexCount | 10,000,000 |
| maxIndexCount | 30,000,000 |
| maxMaterialSlotCount | 65,535 |
| maxSubmeshCount | 65,535 |
| maxClipCount | 1024 |
| maxTrackCount | 8192 |
| maxKeyCountPerClip | 1,000,000 |
検証規約
必須チェック:
- Bind Pose Consistency:
inverseBind * bindLocal = Identity - Weight Normalization:
sum(boneWeight) = 1.0 ± 1e-4 - Bone Index Range:
0 <= boneIndex < boneCount - Index Range:
0 <= index < vertexCount - Submesh Coverage: 重複なし、隙間なし、3の倍数
- Material Index Range:
0 <= materialIndex < materialSlotCount
ローダー実装例
void ParseGOM(const uint8_t* data, size_t fileSize) {
// 1. ヘッダを読み込む(512 bytes)
const GOMBHeader* header = reinterpret_cast<const GOMBHeader*>(data);
ValidateHeader(header);
// 2. チャンクを順次読み込む
size_t offset = 512;
while (offset < fileSize) {
const ChunkHeader* chunk = reinterpret_cast<const ChunkHeader*>(data + offset);
// サイズ検証
if (offset + 16 + chunk->size > fileSize) {
Error("Invalid chunk size");
return;
}
// payload 処理
const uint8_t* payload = data + offset + 16;
switch (chunk->fourcc) {
case FOURCC('STR0'):
ParseSTR0(payload, chunk->size);
break;
case FOURCC('SKEL'):
ParseSKEL(payload, chunk->size);
break;
// ... 他のチャンク
default:
// 未知チャンクはスキップ
break;
}
// 次のチャンクに移動(16-byte アライメント)
offset = ALIGN16(offset + 16 + chunk->size);
}
}
テキストフォーマット
GOMT0001(GOM テキスト形式)
概要:
- UTF-8(BOMなし)
- 構造リテラル形式
- 人間が読み書き可能
- デバッグやプロトタイピングに便利
基本構造
Skeleton = { ... }
MaterialSlots = [ ... ]
Meshes = [ ... ]
Animations = [ ... ]
Skeleton 例
Skeleton = {
boneCount = 3,
bones = [
{ parent = -1, name = "Root", path = "Root", bindLocal = [...] },
{ parent = 0, name = "Spine", path = "Root/Spine", bindLocal = [...] },
{ parent = 1, name = "Arm_L", path = "Root/Spine/Arm_L", bindLocal = [...] }
]
}
MaterialSlots 例
MaterialSlots = [
{
name = "DefaultMaterial",
phong = {
kd = [1.0, 1.0, 1.0],
ks = [0.04, 0.04, 0.04],
ke = [0.0, 0.0, 0.0],
shininess = 32.0,
opacity = 1.0
},
maps = {
diffuse = "textures/diffuse.png",
normal = "",
specular = "",
emissive = ""
}
}
]
Meshes 例
Meshes = [
{
name = "CubeMesh",
meshBindGeoToModel = [...], // 4x4 行列
vertices = [
{ p = [x, y, z], n = [nx, ny, nz], t = [tx, ty, tz, tw],
uv = [u, v], bi = [0, 0, 0, 0], bw = [1.0, 0, 0, 0] },
// ... 他の頂点
],
indices = [0, 1, 2, 2, 3, 0, ...],
submeshes = [
{ materialIndex = 0, indexStart = 0, indexCount = 36 }
],
inverseBind = [ [...], [...], [...] ] // boneCount 個の 4x4 行列
}
]
Animations 例
Animations = [
{
name = "WalkCycle",
duration = 1.0,
rootBoneIndex = 0,
flags = 0,
tracks = [
{
boneIndex = 0,
trKeys = [ { time = 0.0, value = [0, 0, 0] }, ... ],
rotKeys = [ { time = 0.0, value = [0, 0, 0, 1] }, ... ],
scKeys = [ { time = 0.0, value = [1, 1, 1] }, ... ]
},
// ... 他のボーンのトラック
]
}
]
MTA テキスト形式(v1 では非サポート)
v1 では MTA のテキスト形式(MTAT0001)は存在しません。
理由:
- 決定性保証の困難(改行コード、空白、エンコーディング等)
- パーサの複雑性増大
- v1 の設計方針(deterministic binary, AI safe)に集中
v1 でサポートされる形式:
- ✅ MTAB0001(バイナリ形式のみ)
- ❌ MTAT0001(テキスト形式は非サポート)
v2 以降での検討事項:
- テキスト形式が本当に必要かどうか(デバッグ用途)
- 必要な場合、canonical form をどう定義するか
- バイナリ形式で十分な場合、追加しない可能性もある
v1 実装者への注意:
- "MTAT0001" 以外のマジックナンバーは読み込みエラーとする
- エラーメッセージ例: "Unsupported MTA format. Only MTAB0001 is supported in v1."
テキスト形式とバイナリ形式の比較
| 項目 | テキスト形式 | バイナリ形式 |
|---|---|---|
| 可読性 | ◎ 人間が読める | ✕ バイナリ |
| ファイルサイズ | ✕ 大きい | ◎ 小さい |
| パース速度 | △ 遅い | ◎ 速い |
| デバッグ | ◎ 簡単 | △ ツール必要 |
| プロトタイピング | ◎ 手書き可能 | ✕ ツール必須 |
| プロダクション | △ 非推奨 | ◎ 推奨 |
推奨用途:
- テキスト形式: 開発中のデバッグ、プロトタイピング、テストデータ作成
- バイナリ形式: 最終ビルド、プロダクション環境、大規模データ
変換ツール実装例
// 1. FBX を読み込む(FBXSDK)
// 2. bonePath 辞書順でボーンをソート
// 3. meshNodePath 辞書順でメッシュをソート
// 4. materialKey 辞書順でマテリアルをソート
// 5. 頂点を geometry 空間に変換
// 6. ウェイトを正規化(最大4影響、合計1.0)
// 7. GOM/MTA を出力(16-byte アライン)
よくある質問(FAQ)
Q1: GOM と FBX の違いは?
A: GOM は FBX のゲーム実行用最適化版です。
| 項目 | FBX | GOM |
|---|---|---|
| 実行時依存 | FBXSDK 必要 | 不要 |
| geometry 空間 | ノードごとに異なる | 統一 |
| 決定性 | 保証なし | 保証あり |
| ファイルサイズ | 大きい | 小さい |
| パース速度 | 遅い | 速い |
Q2: MTA は必須ですか?
A: いいえ、任意です。GOM 単独でも動作します。
使い分け:
-
GOM のみ: ANIM チャンクにアニメーションを含める
- 小規模プロジェクト、アニメーション数が少ない場合
-
GOM + MTA: アニメーションを独立管理
- 大規模プロジェクト、動的なアニメーション切り替えが必要な場合
Q3: テキスト形式とバイナリ形式、どちらを使うべき?
A: 用途によります。
| 用途 | 推奨形式 | 理由 |
|---|---|---|
| デバッグ | GOMT(テキスト) | 人間が読める、Git diff が見やすい |
| プロトタイピング | GOMT(テキスト) | 手書き可能、素早い修正 |
| プロダクション | GOMB(バイナリ) | 高速、ファイルサイズ小 |
| MTA | MTAB(バイナリのみ) | v1 ではテキスト非サポート |
Q4: skeletonSig が一致しないエラーが出ます
A: 以下を確認してください:
チェックリスト:
-
bonePath が一致しているか
- GOM と MTA で同じボーン階層か?
- ボーン名が完全に一致しているか?
-
parentIndex が一致しているか
- 親子関係が同じか?
-
boneCount が一致しているか
- ボーン数が同じか?
重要: bindLocal が異なっていても、階層構造(bonePath + parentIndex)が同じなら適用可能です。
デバッグ方法:
// GOM の skeletonSig を出力
uint8_t gom_sig[32];
compute_skeleton_sig(gom.SKEL, gom_sig);
printf("GOM skeletonSig: ");
for (int i = 0; i < 32; ++i) printf("%02x", gom_sig[i]);
printf("\n");
// MTA の skeletonSig を出力
printf("MTA skeletonSig: ");
for (int i = 0; i < 32; ++i) printf("%02x", mta.header.skeletonSig[i]);
printf("\n");
Q5: 複数の MTA ファイルを同時に使えますか?
A: はい、可能です。
条件:
- すべて同じ
skeletonSig clipNameが重複しない
ファイル例:
character.gom
character.idle.mta
character.walk.mta
character.run.mta
character.attack.mta
使い方:
// すべての MTA を読み込む
LoadMTA("character.idle.mta");
LoadMTA("character.walk.mta");
LoadMTA("character.run.mta");
// clipName で再生
PlayClip("idle");
PlayClip("walk");
PlayClip("run");
注意: GOM と MTA で同じ clipName があるとエラーになります。
Q6: 頂点フォーマットは変更できますか?
A: v1 では VF01 のみサポートです。
VF01 の内容:
- position: float32 x3
- normal: float32 x3
- tangent: float32 x4
- uv0: float32 x2
- boneIndex: uint16 x4
- boneWeight: float32 x4
v2 以降の拡張予定:
- 複数 UV セット
- 頂点カラー
- 追加の tangent
- カスタム頂点属性
Q7: 決定性保証とは何ですか?
A: 同一の入力から常に同一の出力が生成されることを保証します。
具体例:
<a id="sec-71"></a>
# 同じ FBX を変換すると、常に同じ GOM が生成される
./converter input.fbx -o output1.gom
./converter input.fbx -o output2.gom
diff output1.gom output2.gom # 差分なし
保証される項目:
- ボーンの順序(bonePath 辞書順)
- メッシュの順序(meshNodePath 辞書順)
- マテリアルの順序(materialKey 辞書順)
- アニメーションクリップの順序(clipName 辞書順)
- 浮動小数点値(float32 精度で一致)
なぜ重要か:
- バージョン管理(Git)で差分が明確
- 自動テストが安定
- ビルドの再現性が保証される
Q8: ウェイトが正規化されないとどうなりますか?
A: スキニング結果が破綻します。
正常な場合:
boneWeight = [0.5, 0.3, 0.2, 0.0] // 合計 1.0
異常な場合:
boneWeight = [0.5, 0.3, 0.2, 0.1] // 合計 1.1(スケールが変わる)
boneWeight = [0.5, 0.3, 0.1, 0.0] // 合計 0.9(スケールが変わる)
対策:
- 変換ツールは必ずウェイトを正規化
- ローダーは
validationFlagsを確認(参考情報) - ローダーは独自に検証(
sum(boneWeight) == 1.0 ± 1e-4)
まとめ
GOM の利点
- FBX差を完全に吸収
- geometry 空間でスキニング完結
- 決定性保証(同一FBX→同一GOM)
- 実行時にFBXSDK不要
MTA の利点
- アニメーションの独立管理
- ファイルサイズ削減(SKEL省略)
- 異なるGOMでも適用可能(skeletonSig一致)
- 複数アニメーションの動的切り替え
詳細仕様
詳細は以下を参照:
- GOM 詳細仕様:
Docs/GOM_Docs/spec_Sources/Original/INDEX.md - MTA 詳細仕様:
Docs/GOM_Docs/spec_Sources/Original/spec_binary/20_mta_format.md - 変更履歴:
Docs/GOM_Docs/spec_Sources/Original/changes/CHANGELOG.md
作成日: 2025-01-XX
バージョン: GOM v1.0 / MTA v1.0
ライセンス: プロジェクト固有
End of Quick Reference
Visual Studioの更新うざいよね?
設定色々変えたけど、更新のお知らせが来るよ
表題のとおり、残念ながら何回もいろんな設定試したんだけど、更新のお知らせが来ます。
もうどうしようもないです。
仕方がないので、個人設定を変更してこの更新の通知を止めていきます。
以下に、やり方を書くので各自やってみてください。
(学校のPCだけね。おうちのPCはちゃんとアップデートすればいいと思うよ)
1.メニューから、ツール→オプションを選ぶ
2.左の設定項目から、環境を選択
3.環境の設定項目がずらずら右ペインに出るので、一番下に行く
4.出てきたダイアログ(子ウィンドウ)の検索フォームに注目
5.検索フォームに「更新」と入力
6.「拡張機能マネージャー」を選択→更新プログラムを自動的にダウンロードする、のチェックを外す

7.同じく「製品の更新プログラム」を選択→更新プログラムを自動的に。。。のチェックを外す
8.「OK」を押して設定完了。これで更新の通知が出ないはず。
動画で設定の様子を見るならこちら
youtubeボタン押して、でかい画面で見るがいいさ。
DxLibで3Dキャラクターの移動(基本)
カメラの違いによるキャラクター移動操作
このサンプルでは、同じプレイヤー操作を使いながら、カメラの種類によって見え方や操作感がどのように変わるかを確認します。
使用するカメラは次の3種類です。
- 固定カメラ
- TPSカメラ
- OTSカメラ
カメラの種類は CAMERA_MODE で管理しており、CAMERA_FIXED、CAMERA_TPS、CAMERA_OTS の3種類が定義されています。
実行中は Cキー を押すことで、固定カメラ → TPSカメラ → OTSカメラ の順に切り替わります。
サンプルプログラムをクローンして、実行してみてください。
https://github.com/youetsux/radioControlMan
共通のプレイヤー操作
プレイヤーの基本操作は、どのカメラでも共通です。
キー | 動作 |
|---|---|
W | 前進 |
S | 後退 |
A | 左回転 |
D | 右回転 |
C | カメラ切り替え |
プレイヤーは Aキー と Dキー でY軸方向に回転します。
その回転角度から、現在の進行方向ベクトル dir_ を計算しています。
float rad = DirectX::XMConvertToRadians(rotY_);dir_ = VGet(sinf(rad), 0.0f, cosf(rad));Wキー を押すと、進行方向 dir_ に向かって前進します。Sキー を押すと、進行方向と逆向きに後退します。
pos_ = VAdd(pos_, VScale(dir_, speed_ * dt));つまり、このサンプルの移動は「カメラの向き」ではなく、「キャラクター自身の向き」を基準にしています。
固定カメラでのラジコン移動操作
固定カメラとは
固定カメラは、カメラの位置と注視点が常に決まっているカメラです。
このサンプルでは、カメラ位置を (0, 10, -10)、注視点を (0, 0, 0) に固定しています。
VECTOR FIXED_POS = VGet(0.0f, 10.0f, -10.0f);VECTOR FIXED_TARGET = VGet(0.0f, 0.0f, 0.0f);固定カメラモードでは、プレイヤーが移動してもカメラは動きません。
常に同じ位置からステージを見下ろすような見え方になります。
SetCameraPositionAndTarget_UpVecY(FIXED_POS, FIXED_TARGET);固定カメラのサンプル動画
ラジコン操作の特徴
固定カメラでは、画面の上方向とキャラクターの前方向が常に一致するとは限りません。
そのため、操作感としてはラジコンに近くなります。
Aキー:キャラクターが左に向きを変えるDキー:キャラクターが右に向きを変えるWキー:キャラクターが向いている方向へ進むSキー:キャラクターが向いている方向の逆へ下がる
例えば、キャラクターが画面手前を向いている状態で Wキー を押すと、画面上では手前方向に移動します。
これは、Wキー が「画面の上へ進む」ではなく、「キャラクターの前へ進む」操作だからです。
固定カメラのメリット
固定カメラは、ステージ全体の位置関係を把握しやすいのが特徴です。
プレイヤーがどこにいてもカメラが動かないため、背景や地形の見え方が安定します。
パズルゲーム、見下ろし型アクション、昔の3Dアドベンチャーゲームなどで使いやすい方式です。
一方で、キャラクターの向きと画面方向がずれるため、慣れるまでは操作が難しく感じることがあります。
TPSカメラによるキャラクター移動
TPSカメラとは
TPSカメラは、キャラクターの後ろから追いかけるカメラです。
TPSは Third Person Shooter、または Third Person View のように、三人称視点のゲームでよく使われます。
このサンプルでは、プレイヤーの進行方向 playerDir の逆方向にカメラを配置しています。
つまり、キャラクターの背後にカメラが来るようになっています。
VECTOR back = VScale(playerDir, -TPS_DISTANCE);VECTOR camPos = VAdd(playerPos, back);camPos.y += TPS_HEIGHT;カメラの高さは TPS_HEIGHT、キャラクターからの距離は TPS_DISTANCE で調整しています。
float TPS_DISTANCE = 8.0f;float TPS_HEIGHT = 4.0f;カメラは、プレイヤーの少し上あたりを見るように設定されています。
VECTOR target = VAdd(playerPos, VGet(0.0f, TPS_HEIGHT * 0.5f, 0.0f));SetCameraPositionAndTarget_UpVecY(camPos, target);TPSカメラサンプル動画
TPSカメラでの操作感
TPSカメラでは、カメラがキャラクターの後ろに回り込むため、キャラクターの前方向が画面奥方向になりやすくなります。
そのため、Wキー を押したときに「画面の奥へ進む」ように見えます。
固定カメラよりも、プレイヤーの進行方向が直感的に分かりやすくなります。
ただし、このサンプルではカメラがマウス操作で自由に回る方式ではありません。
キャラクターの向きに合わせてカメラが後ろへ回り込む形です。
TPSカメラのメリット
TPSカメラは、キャラクターの進行方向と画面の見え方が一致しやすいため、アクションゲームで使いやすいカメラです。
- キャラクターの前方が見やすい
- 移動方向を把握しやすい
- プレイヤーと周囲の位置関係を見やすい
- 3Dアクションや探索ゲームに向いている
固定カメラと比べると、キャラクターを中心にした操作感になります。
OTSカメラによるキャラクター移動
OTSカメラとは
OTSカメラは、Over The Shoulder の略で、キャラクターの肩越しに見るカメラです。
TPSカメラと似ていますが、カメラがキャラクターの真後ろではなく、少し右側や左側にずれた位置に置かれます。
このサンプルでは、プレイヤーの後ろに下がりつつ、右方向にオフセットした位置へカメラを配置しています。
VECTOR up = VGet(0.0f, 1.0f, 0.0f);VECTOR right = VNorm(VCross(playerDir, up));VECTOR camPos = playerPos;camPos = VAdd(camPos, VScale(playerDir, -OTS_DISTANCE));camPos = VAdd(camPos, VScale(right, OTS_RIGHT_OFFSET));camPos.y += OTS_HEIGHT;OTSカメラ用の距離、高さ、横方向のずれは、次の値で設定しています。
float OTS_DISTANCE = 5.0f;float OTS_HEIGHT = 2.5f;float OTS_RIGHT_OFFSET = 2.0f;OTSカメラのサンプル
肩越し視点の作り方
OTSカメラでは、まずプレイヤーの進行方向 playerDir から、右方向ベクトル right を作っています。
VECTOR right = VNorm(VCross(playerDir, up));その後、カメラ位置を次のように調整しています。
- プレイヤー位置を基準にする
- プレイヤーの後ろに下げる
- 右方向にずらす
- 高さを加える
これにより、キャラクターの背後やや右側から見る「肩越し視点」になります。
さらに、注視点も少し左側にずらしています。
これにより、キャラクターが画面の中央に完全に重なるのではなく、少し横に寄った構図になります。
VECTOR target = playerPos;target = VAdd(target, VScale(right, -OTS_RIGHT_OFFSET * 0.5f));target.y += OTS_HEIGHT * 0.5f;OTSカメラでの操作感
OTSカメラでは、キャラクターの近くから肩越しに前方を見るため、プレイヤーの視点に近い感覚になります。
TPSカメラよりもキャラクターに近いため、前方の対象物や敵を狙うようなゲームに向いています。
- シューティング
- アクション
- ホラーゲーム
- 近距離でキャラクターを見せたいゲーム
などでよく使われます。
一方で、カメラがキャラクターに近いため、周囲全体の状況はTPSカメラより少し見づらくなります。
3種類のカメラの比較
カメラ | カメラの位置 | 操作感 | 向いているゲーム |
|---|---|---|---|
固定カメラ | 常に固定位置 | ラジコン操作に近い | パズル、見下ろし型、固定画面アクション |
TPSカメラ | キャラクターの後ろ | 進行方向が分かりやすい | 3Dアクション、探索ゲーム |
OTSカメラ | キャラクターの肩越し | 狙う・見る感覚が強い | TPS、ホラー、アクションシューティング |
プログラム全体の流れ
このサンプルでは、プレイヤーの Update() の最後で、プレイヤーの現在位置 pos_ と進行方向 dir_ をカメラに渡しています。
Camera::Update(pos_, dir_);カメラ側では、その位置と向きを使って、現在のカメラモードに応じたカメラ位置を計算しています。
つまり、処理の流れは次のようになります。
- キー入力を確認する
- プレイヤーの回転角度を更新する
- 回転角度から進行方向
dir_を作る W/Sキーでプレイヤー位置を移動する- プレイヤーの位置と向きをモデルに反映する
- プレイヤーの位置と進行方向をカメラに渡す
- カメラモードに応じてカメラ位置を更新する
まとめ
固定カメラ、TPSカメラ、OTSカメラは、同じキャラクター移動を使っていても、プレイヤーに与える操作感が大きく変わります。固定カメラでは、カメラが動かないため、キャラクター自身の向きに合わせて操作するラジコン操作になります。TPSカメラでは、カメラがキャラクターの後ろに回り込むため、進行方向が分かりやすく、3Dアクション向きの操作感になります。OTSカメラでは、キャラクターの肩越しから見ることで、狙う・注視する感覚が強くなり、シューティングやホラーゲームのような演出に向いています。
おまけ
固定カメラ+固定方向(上下左右)移動+なめらかな角度変化
ここまでの固定カメラでは、キャラクターの向きを基準にして前進・後退する「ラジコン操作」を説明しました。
このおまけでは、固定カメラ用の別操作として、画面方向に対して固定された移動を行います。
サンプルプログラムのURLは
https://github.com/youetsux/radioControllman2
ラジコン操作では、
A/Dでキャラクターの向きを変えるWでキャラクターの向いている方向へ進むSでキャラクターの後ろへ下がる
という操作でした。
それに対して、固定方向移動では、
W:奥へ移動S:手前へ移動A:左へ移動D:右へ移動
のように、キーと移動方向を固定します。
固定カメラの場合、カメラの位置が変わらないため、画面上の上下左右とワールド座標の前後左右を対応させやすくなります。
固定カメラのときだけ移動処理を切り替える
このサンプルでは、カメラモードが固定カメラのときだけ UpdateFixedCameraMove() を使い、それ以外のカメラでは UpdateFreeCameraMove() を使っています。
if (Camera::GetMode() == CAMERA_FIXED){ UpdateFixedCameraMove(dt);}else{ UpdateFreeCameraMove(dt);}これにより、固定カメラでは画面方向に合わせた移動、TPSカメラやOTSカメラではキャラクターの向きを基準にした移動、というように操作方法を分けています。
ワールド固定の前後左右ベクトルを用意する
固定方向移動では、まずワールド空間での前方向と右方向を決めています。
const VECTOR forward = VGet(0.0f, 0.0f, 1.0f);const VECTOR right = VGet(1.0f, 0.0f, 0.0f);ここでは、
forwardがZ軸プラス方向rightがX軸プラス方向
を表しています。
つまり、キャラクターが今どちらを向いているかに関係なく、W を押したらZ軸プラス方向、D を押したらX軸プラス方向へ移動するようになります。
キー入力から移動方向を作る
次に、押されているキーに応じて移動方向 moveVec を作ります。
VECTOR moveVec = VGet(0.0f, 0.0f, 0.0f);if (Input::IsKeepKeyDown(KEY_INPUT_W)) moveVec = VAdd(moveVec, forward);if (Input::IsKeepKeyDown(KEY_INPUT_S)) moveVec = VAdd(moveVec, VScale(forward, -1.0f));if (Input::IsKeepKeyDown(KEY_INPUT_D)) moveVec = VAdd(moveVec, right);if (Input::IsKeepKeyDown(KEY_INPUT_A)) moveVec = VAdd(moveVec, VScale(right, -1.0f));この処理では、押されたキーに対応する方向ベクトルを足し合わせています。
例えば、W と D を同時に押した場合は、
forward + rightとなるため、右奥方向への移動になります。
斜め移動が速くならないように正規化する
W だけを押した場合、移動方向の長さは 1 です。
しかし、W と D を同時に押した場合、移動方向は斜めになります。
このまま移動すると、斜め移動だけ距離が長くなり、移動速度が速くなってしまいます。
そこで、移動方向の長さを求めています。
float moveLen = sqrtf(moveVec.x * moveVec.x + moveVec.z * moveVec.z);if (moveLen < 0.0001f) return;VECTOR moveDir = VScale(moveVec, 1.0f / moveLen);moveLen がほぼ0の場合は、何もキーが押されていない状態なので移動処理を終了します。
キー入力がある場合は、moveVec を moveLen で割って、長さ1のベクトルにしています。
これを正規化といいます。
正規化することで、上下左右に移動しても、斜めに移動しても、同じ速度で移動できます。
移動方向へキャラクターを向ける
固定方向移動では、キャラクターの向きで移動方向を決めるのではなく、入力された移動方向へキャラクターを向けます。
そのため、まず移動方向 moveDir から目標のY軸回転角 targetRotY を計算します。
float targetRotY = DirectX::XMConvertToDegrees(atan2f(moveDir.x, moveDir.z));atan2f() を使うことで、X方向とZ方向の成分から、キャラクターが向くべき角度を求めています。
例えば、
入力 | 移動方向 | キャラクターの向き |
|---|---|---|
W | 奥 | 奥を向く |
S | 手前 | 手前を向く |
A | 左 | 左を向く |
D | 右 | 右を向く |
W + D | 右奥 | 右奥を向く |
という動きになります。
急に向きを変えず、なめらかに回転させる
目標角度をそのまま rotY_ に代入すると、キャラクターの向きが一瞬で切り替わってしまいます。
そこで、このサンプルでは現在の角度 rotY_ と目標角度 targetRotY の差を求め、少しずつ回転するようにしています。
float diff = targetRotY - rotY_;while (diff > 180.0f) diff -= 360.0f;while (diff < -180.0f) diff += 360.0f;float rotStep = rotYSpeed_ * dt;rotY_ += (fabsf(diff) < rotStep) ? diff : (diff > 0.0f ? rotStep : -rotStep);角度の差が 180度 を超える場合は、逆回りした方が近いことがあります。
そのため、while 文で角度差を -180度 ~ 180度 の範囲に収めています。
その後、1フレームで回転できる量 rotStep を使って、少しずつ目標方向へ回転します。
これにより、キーを押した方向へキャラクターが自然に向き直るようになります。
実際に移動する
最後に、正規化した移動方向 moveDir に速度とデルタタイムを掛けて、プレイヤーの位置に加算します。
pos_ = VAdd(pos_, VScale(moveDir, speed_ * dt));dir_ = moveDir;pos_ がプレイヤーの位置です。moveDir の方向へ speed_ * dt 分だけ移動します。
また、dir_ にも moveDir を入れています。
これは、カメラ更新時にプレイヤーの進行方向として使うためです。
プレイヤーの Update() の最後では、現在位置 pos_ と進行方向 dir_ をカメラに渡しています。
Camera::Update(pos_, dir_);ラジコン操作と固定方向移動の違い
操作方式 | 移動方向の基準 | 特徴 |
|---|---|---|
ラジコン操作 | キャラクターの向き | 向きを変えてから前進する |
固定方向移動 | ワールド座標・画面方向 | 押したキーの方向へ直接移動する |
ラジコン操作は、キャラクターを「操作している」感覚が強い方式です。
一方、固定方向移動は、キャラクターを画面上で直接動かしている感覚に近い方式です。
固定カメラのゲームでは、カメラの向きが変わらないため、固定方向移動の方が直感的に操作しやすい場合があります。
ただし、カメラが回転するゲームで固定方向移動をそのまま使うと、画面方向と移動方向がずれて分かりにくくなることがあります。
そのため、このサンプルでは固定カメラのときだけ固定方向移動を使い、TPSカメラやOTSカメラでは通常のキャラクター向き基準の移動に戻しています。
AIの課題(5月12日)
見張りモンスターAIを設計せよ
ねらい
敵は何を見ているのか?
↓
それをどんなフラグにするのか?
↓
フラグによってどの行動を選ぶのか?
↓
それをif文にするとどうなるのか?課題タイトル
「お宝を守るモンスターを設計しよう」
シチュエーション
あなたはダンジョンゲームの敵AIを作ることになった。
モンスターは部屋の中にある宝箱を守っている。
プレイヤーは部屋に侵入して、宝箱を開けようとする。
モンスターは状況に応じて行動を変える。
モンスターの基本行動
使ってよい行動は以下。
Idle その場で待つ
Patrol 決められた範囲を歩く
Chase プレイヤーを追いかける
Attack プレイヤーを攻撃する
Return 持ち場に戻る
Guard 宝箱の前に立つ
Search プレイヤーを探す使ってよいフラグ
最初はこれだけ与えるとよいです。
bool isFindPlayer; // プレイヤーを見つけた
bool isNearPlayer; // プレイヤーが近い
bool isAttackRange; // 攻撃範囲にいる
bool isPlayerNearTreasure; // プレイヤーが宝箱に近い
bool isFarFromHome; // 持ち場から離れすぎた
bool isLostPlayer; // プレイヤーを見失った考えること!
1. どの行動を優先するか
例えば、
攻撃できるなら攻撃
宝箱に近づいたら追い払う
見つけたら追跡
見失ったら探す
遠くまで行きすぎたら戻る
普段は巡回この優先順位を考える。
2. if文に直す
最終的にこういう形にする。
if (isAttackRange)
{
Attack();
}
else if (isPlayerNearTreasure)
{
Chase();
}
else if (isFindPlayer)
{
Chase();
}
else if (isLostPlayer)
{
Search();
}
else if (isFarFromHome)
{
Return();
}
else
{
Patrol();
}自習の進め方
0〜10分
課題説明を読む
配布資料に以下を載せる。
あなたはゲームAI担当です。
宝箱を守るモンスターの行動ルールを設計してください。考えるゴールは、
フラグ
↓
条件分岐
↓
行動です。
10〜25分
ゲーム内状況を洗い出す
問い
モンスターが判断に使いそうな情報は何か?例:
プレイヤーとの距離
プレイヤーの方向
宝箱との距離
自分の持ち場からの距離
HP
プレイヤーを見失った時間25〜40分
フラグを作る
洗い出した情報を bool に変換する。
例:
プレイヤーとの距離が近い
↓
isNearPlayer
攻撃できる距離にいる
↓
isAttackRange
宝箱に近づいている
↓
isPlayerNearTreasure40〜55分
行動の優先順位を決める
ここが面白いところです。
行動パターンでモンスター(AI)の性格が変わる。
例1:短気なモンスター
攻撃
追跡
探索
巡回例2:宝箱優先モンスター
宝箱防衛
攻撃
追跡
持ち場に戻る
巡回例3:臆病なモンスター
宝箱防衛
攻撃
追跡
持ち場に戻る
巡回55〜70分
if文にする
疑似コードで書いてみる
if (__________)
{
__________();
}
else if (__________)
{
__________();
}
else
{
__________();
}
AI モンスターの知能
モンスターに視覚を与える
isFindPlayer は誰が決めているのか?
1. 前回までの確認
前回、モンスターの行動を if文 で切り替える考え方を学びました。
例えば、次のような処理です。
if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}この処理では、モンスターは次のように行動します。
条件 | 行動 |
|---|---|
攻撃範囲にいる | 攻撃する |
プレイヤーを見つけている | 追いかける |
どちらでもない | 待機する |
このように、条件によって行動を変えることで、モンスターが考えて動いているように見えます。
2. ここで問い
次の変数に注目してください。
isFindPlayerこれは、
プレイヤーを見つけているか?を表すフラグです。
しかし、ここで考えるべきことがあります。
isFindPlayer は誰が決めているのか?
プログラムの中で、
if (isFindPlayer)と書くことはできます。
しかし、isFindPlayer が true になる理由を作らなければ、モンスターはプレイヤーを見つけることができません。
つまり、
isFindPlayer = true;になる条件を、こちらで作る必要があります。
3. ゲームAIには「知覚」が必要
モンスターが行動を決めるには、まずゲームの状況を知る必要があります。
例えば、
- プレイヤーが近くにいる
- プレイヤーが前方にいる
- プレイヤーとの間に壁がない
- プレイヤーが音を立てた
- プレイヤーが攻撃してきた
などです。
このように、AIがゲームの状況を知る仕組みを 知覚 と呼びます。
4. 人間の「見る」とAIの「見る」は違う
人間は目で映像を見ています。
しかし、ゲームAIは本当に画面を見ているわけではありません。
ゲームAIは、次のような数値や条件を使って「見えているか」を判定します。
距離は近いか?
前方にいるか?
壁に隠れていないか?つまり、ゲームAIの視覚とは、
画像を見ることではなく、
条件を満たしたら「見えている」とする処理です。
5. まず一番簡単な視覚を作る
最初は、距離だけで考えます。
例えば、
プレイヤーが10m以内にいたら発見というルールにします。
この場合、モンスターはプレイヤーとの距離を調べます。
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
if (distance < VIEW_DISTANCE)
{
isFindPlayer = true;
}
else
{
isFindPlayer = false;
}ここで、VIEW_DISTANCE は視界距離です。
const float VIEW_DISTANCE = 10.0f;6. 距離だけの視覚の問題点
距離だけで判定すると、かなり単純なAIになります。
例えば、次のような問題があります。
問題1:後ろにいても見つかる
距離だけで判定すると、プレイヤーがモンスターの後ろにいても見つかってしまいます。
プレイヤー
↓
モンスター → 前を向いている本当なら、モンスターの後ろにいるプレイヤーは見えないはずです。
でも距離だけで判定していると、近ければ見つかります。
問題2:壁越しでも見つかる
距離だけで判定すると、壁の向こうにいるプレイヤーも見つかってしまいます。
モンスター |壁| プレイヤー本当なら、壁があるので見えないはずです。
でも距離だけで判定していると、近ければ見つかります。
7. 視覚に必要な条件
より自然な視覚を作るには、次の3つを考えます。
1. 距離
2. 向き
3. 障害物8. 条件1:距離
まずは距離です。
プレイヤーが遠すぎるなら見えないこれは分かりやすい条件です。
if (distance > VIEW_DISTANCE)
{
isFindPlayer = false;
}9. 条件2:向き
次に、プレイヤーがモンスターの前方にいるかを調べます。
モンスターには向きがあります。
モンスター → 前方プレイヤーが前方にいれば見える。
モンスター → プレイヤーしかし、プレイヤーが後ろにいるなら見えません。
プレイヤー ← モンスター10. 前方にいるかをどう調べるか
ここで使うのが 内積 です。
必要なベクトルは2つです。
モンスターの前方向
モンスターからプレイヤーへの方向11. モンスターからプレイヤーへの方向
まず、モンスターからプレイヤーへのベクトルを求めます。
VECTOR toPlayer = VSub(playerPos, enemyPos);次に、長さを1にします。
float distance = VSize(toPlayer);
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);この dirToPlayer が、モンスターから見たプレイヤーの方向です。
12. モンスターの前方向
次に、モンスターの前方向を用意します。
例として、Y軸回転から前方向を作るなら、次のようになります。
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};ただし、モデルの正面方向や座標系によって、sinf と cosf の使い方は変わることがあります。
13. 内積で角度を調べる
2つのベクトルの内積を取ります。
float dot = VDot(forward, dirToPlayer);dot の値は、だいたい次のように考えられます。
dotの値 | 意味 |
|---|---|
1.0 に近い | 正面にいる |
0.0 に近い | 横にいる |
-1.0 に近い | 後ろにいる |
14. 視野角の判定
例えば、次のようにします。
const float VIEW_DOT = 0.5f;
if (dot > VIEW_DOT)
{
isFindPlayer = true;
}
else
{
isFindPlayer = false;
}dot > 0.5f なら、前方のある範囲内にいると判断できます。
15. VIEW_DOT の目安
条件 | 見える範囲の目安 |
|---|---|
| 前方約120度 |
| 前方約90度 |
| 前方約60度 |
数値を大きくすると、視野が狭くなります。
数値を小さくすると、視野が広くなります。
16. 距離と向きを組み合わせる
距離と向きを組み合わせると、次のような判定になります。
bool CheckCanSeePlayer()
{
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
if (distance > VIEW_DISTANCE)
{
return false;
}
if (distance < 0.001f)
{
return true;
}
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};
float dot = VDot(forward, dirToPlayer);
if (dot < VIEW_DOT)
{
return false;
}
return true;
}この関数は、
プレイヤーが見えているなら true
見えていないなら falseを返します。
17. 条件3:障害物
まだ問題があります。
距離が近くて、前方にいても、壁の向こうにいるなら本当は見えません。
モンスター ---- 壁 ---- プレイヤーこの場合は、見えないようにしたいです。
18. 壁で見えない処理
考え方は単純です。
モンスターからプレイヤーまで線を引く
↓
途中に壁があるか調べる
↓
壁があれば見えない
↓
壁がなければ見えるこのような判定を、レイ判定やライン判定と呼びます。
19. 疑似コード
bool isHitWall = CheckLineHitWall(enemyPos, playerPos);
if (isHitWall)
{
return false;
}CheckLineHitWall は、敵とプレイヤーの間に壁があるか調べる処理だと考えてください。
20. 最終的な視覚判定
距離、向き、壁を組み合わせると、次のような流れになります。
bool CheckCanSeePlayer()
{
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
// 遠すぎるなら見えない
if (distance > VIEW_DISTANCE)
{
return false;
}
// ほぼ同じ位置なら見えている扱い
if (distance < 0.001f)
{
return true;
}
// 方向を求める
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);
// モンスターの前方向
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};
// 前方にいるか調べる
float dot = VDot(forward, dirToPlayer);
if (dot < VIEW_DOT)
{
return false;
}
// 壁があれば見えない
if (CheckLineHitWall(enemyPos, playerPos))
{
return false;
}
return true;
}21. 前回のAIに接続する
視覚判定ができたら、前回のAIに接続できます。
isFindPlayer = CheckCanSeePlayer();
if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}これで、
見えたら追いかける
見えなければ待機するというモンスターになります。
22. 何がAIらしくなるのか
距離だけで判定していたときは、
近ければ必ず見つかるという動きでした。
しかし、視野角や壁判定を入れると、次のようになります。
後ろに回ると気づかれない
壁に隠れると見つからない
正面に出ると追いかけてくるこのようなルールがあると、プレイヤーは、
敵に見つかった
敵から隠れた
敵の背後を取ったと感じやすくなります。
つまり、AIが少し賢く見えます。
23. 重要な考え方
ゲームAIは、本当に人間のように世界を理解しているわけではありません。
しかし、次のような情報を使うことで、考えているように見せることができます。
距離
方向
角度
障害物
時間
状態AIは、世界をそのまま理解しているのではなく、ゲームに必要な情報だけを取り出して判断しています。
まとめ
今回のポイントは次の通りです。
isFindPlayer は勝手に決まらないAIには知覚が必要ゲームAIの視覚は画像を見ることではない距離・向き・壁で「見えている」を作る見えているかどうかをフラグにして、行動分岐に使う最後の確認問題
問1
次のフラグは何を表しているか説明しなさい。
bool isFindPlayer;問2
距離だけでプレイヤーを発見するAIには、どんな問題がありますか。
2つ書きなさい。
問3
モンスターの前方にプレイヤーがいるかを調べるために使う計算は何ですか。
問4
壁の向こうにいるプレイヤーを見えないようにするには、どのような判定が必要ですか。
問5
次の処理の意味を説明しなさい。
isFindPlayer = CheckCanSeePlayer();発展課題
次のようなモンスターを考えなさい。
普段は巡回している。
プレイヤーが視界に入ったら追いかける。
壁の向こうに逃げられたら見失う。
見失ったら、その場で3秒間探す。
それでも見つからなければ巡回に戻る。このモンスターに必要なフラグを考えなさい。
例:
bool isFindPlayer;
bool isLostPlayer;
bool isSearching;
bool isPatrol;さらに、どのような順番で if文 を書けばよいか考えなさい。
既存ゲームを改良するよ(何様じゃい!)
既存ゲームの「足りない部分」を見つけて進化案を考える手順書
この課題でやること
この課題では、前回分析したゲームをもとにして、
「このゲームはどこが弱いのか」
「どこを足せばもっと面白くなるのか」
を考えます。
ただし、完全に別のゲームを作るのではありません。
元のゲームの面白さを残したまま、
足りない部分を補って、少し進化させることが目的です。
対象ゲーム
前回分析した次のゲームの中から、自分が選んだものを使います。
- パックマン
- クルクルランド
- デビルワールド
- ドンキーコングJR
手順1 分析したゲーム名を書く
まず、自分が分析したゲーム名を書きます。
記入欄
ゲーム名:__________________
手順2 このゲームの中心的な面白さを一文で書く
足りない部分を考える前に、まず
「このゲームの一番大事な面白さ」
を確認します。
ここを考えないまま新要素を足すと、元のゲームと関係ないアイデアになってしまいます。
考え方
次の文に当てはめて書いてください。
このゲームの中心的な面白さは、
「__________________」
である。
例
パックマンの場合
このゲームの中心的な面白さは、
「敵に追われながら、迷路の中のエサを集める緊張感」
である。
クルクルランドの場合
このゲームの中心的な面白さは、
「自由に止まれない操作で、隠された絵を探す面白さ」
である。
デビルワールドの場合
このゲームの中心的な面白さは、
「迷路が動く中で、進む道を考える面白さ」
である。
ドンキーコングJRの場合
このゲームの中心的な面白さは、
「登る、避ける、助けるを組み合わせたアクションの面白さ」
である。
記入欄
このゲームの中心的な面白さは、
「__________________________」
である。
手順3 6つの項目でゲームを評価する
次に、ゲームの面白さを6つの項目に分けて評価します。
点数は、1点から5点でつけます。
点数の意味
| 点数 | 意味 |
|---|---|
| 5点 | とても良い。今でも十分に面白い |
| 4点 | 良い。少しだけ改善できそう |
| 3点 | 普通。悪くはないが、強い特徴とも言えない |
| 2点 | 弱い。もっと工夫できそう |
| 1点 | かなり弱い。足りない部分として考えやすい |
手順4 「ルールのわかりやすさ」を評価する
見るポイント
- 何をすればよいか、すぐに分かるか
- クリア条件や目的が分かりやすいか
- プレイヤーが迷わず遊び始められるか
考えるヒント
次のようなことを考えてください。
- 目的は分かりやすいか
- 失敗条件は分かりやすいか
- ルールを覚えるのに時間がかかるか
- 初めて遊んだ人でも理解できそうか
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| ルールのわかりやすさ | _点 | __________________ |
手順5 「操作の気持ちよさ」を評価する
見るポイント
- 操作していて気持ちよいか
- 思った通りに動かせるか
- 操作の不自由さが、面白さにつながっているか
- 操作のしにくさが、ただのストレスになっていないか
考えるヒント
操作が不自由だから悪い、とは限りません。
たとえば、クルクルランドのように
「自由に止まれないこと」
が面白さになっている場合もあります。
大事なのは、操作の特徴が面白さにつながっているかどうかです。
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| 操作の気持ちよさ | _点 | __________________ |
手順6 「報酬の気持ちよさ」を評価する
見るポイント
- 得点を取るのが気持ちよいか
- アイテムを取るのが嬉しいか
- クリアしたときに達成感があるか
- 高得点を狙いたくなる仕組みがあるか
考えるヒント
報酬とは、プレイヤーが
「やってよかった」
と感じるものです。
たとえば、次のようなものがあります。
- 得点が入る
- アイテムを取る
- 敵を倒せる
- ステージをクリアする
- ボーナスが入る
- 音やエフェクトで気持ちよく感じる
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| 報酬の気持ちよさ | _点 | __________________ |
手順7 「敵・障害物の面白さ」を評価する
見るポイント
- 敵や障害物が、プレイヤーに考えさせているか
- 敵の動きに特徴があるか
- 障害物によって、ただの移動ではない遊びになっているか
- 敵や障害物の種類に違いがあるか
考えるヒント
敵や障害物は、プレイヤーを邪魔するだけの存在ではありません。
よい敵や障害物は、プレイヤーに
「どっちへ進むか」
「今行くか、待つか」
「危険をおかして得点を狙うか」
のような判断をさせます。
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| 敵・障害物の面白さ | _点 | __________________ |
手順8 「ステージの変化」を評価する
見るポイント
- ステージごとに違いがあるか
- 場所によって考えることが変わるか
- 同じことの繰り返しになりすぎていないか
- ステージ構造が面白さを作っているか
考えるヒント
ステージの変化とは、見た目が変わることだけではありません。
次のような変化も含みます。
- 通路の形が変わる
- 逃げ道が少ない場所がある
- 危険な場所と安全な場所がある
- アイテムの配置が違う
- 敵の出方が変わる
- 動く床や動く迷路などの仕掛けがある
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| ステージの変化 | _点 | __________________ |
手順9 「もう一度遊びたさ」を評価する
見るポイント
- もう一度挑戦したくなるか
- 前よりうまくなったと感じられるか
- 高得点を狙いたくなるか
- 毎回少し違う遊びになるか
考えるヒント
「もう一度遊びたさ」は、リプレイ性とも言います。
次のような要素があると、もう一度遊びたくなりやすいです。
- 高得点を狙える
- タイムを縮められる
- ランダム性がある
- 上達が分かりやすい
- 失敗しても「次はできそう」と思える
- 攻め方を変えられる
記入欄
| 項目 | 点数 | 理由 |
|---|---|---|
| もう一度遊びたさ | _点 | __________________ |
手順10 6項目の点数をまとめる
ここまでつけた点数を、1つの表にまとめます。
記入欄
| 評価項目 | 点数 |
|---|---|
| ルールのわかりやすさ | _点 |
| 操作の気持ちよさ | _点 |
| 報酬の気持ちよさ | _点 |
| 敵・障害物の面白さ | _点 |
| ステージの変化 | _点 |
| もう一度遊びたさ | _点 |
手順11 点数を見える化する
点数を、星の数で見えるようにします。
5点なら星5つ、3点なら星3つです。
星の書き方
| 点数 | 星 |
|---|---|
| 5点 | ★★★★★ |
| 4点 | ★★★★☆ |
| 3点 | ★★★☆☆ |
| 2点 | ★★☆☆☆ |
| 1点 | ★☆☆☆☆ |
記入例
| 評価項目 | 点数 | 星 |
|---|---|---|
| ルールのわかりやすさ | 5点 | ★★★★★ |
| 操作の気持ちよさ | 4点 | ★★★★☆ |
| 報酬の気持ちよさ | 2点 | ★★☆☆☆ |
| 敵・障害物の面白さ | 4点 | ★★★★☆ |
| ステージの変化 | 2点 | ★★☆☆☆ |
| もう一度遊びたさ | 3点 | ★★★☆☆ |
記入欄
| 評価項目 | 点数 | 星 |
|---|---|---|
| ルールのわかりやすさ | _点 | _____ |
| 操作の気持ちよさ | _点 | _____ |
| 報酬の気持ちよさ | _点 | _____ |
| 敵・障害物の面白さ | _点 | _____ |
| ステージの変化 | _点 | _____ |
| もう一度遊びたさ | _点 | _____ |
手順12 足りない部分ランキングを作る
点数が低い項目を見つけます。
点数が低いものほど、足りない部分です。
やり方
- 点数が一番低い項目を探す
- その項目を「足りない部分1位」にする
- 次に低い項目を「足りない部分2位」にする
- さらに次に低い項目を「足りない部分3位」にする
記入例
| 順位 | 足りない部分 | 点数 | 理由 |
|---|---|---|---|
| 1位 | 報酬の気持ちよさ | 2点 | エサを集める以外のごほうびが少ないから |
| 2位 | ステージの変化 | 2点 | ステージごとの違いが少なく感じたから |
| 3位 | もう一度遊びたさ | 3点 | 高得点以外の目標が少ないから |
記入欄
| 順位 | 足りない部分 | 点数 | 理由 |
|---|---|---|---|
| 1位 | ________ | _点 | ______________ |
| 2位 | ________ | _点 | ______________ |
| 3位 | ________ | _点 | ______________ |
手順13 一番補いたい項目を1つ選ぶ
足りない部分ランキングの中から、
自分が一番改善したい項目を1つ選びます。
この課題では、いきなり全部を直そうとしません。
1つだけ選んで、そこを強くする案を考えます。
記入欄
私が補いたい項目は、
「__________________」
である。
その理由は、
__________________________________
である。
手順14 なぜ足りないのかを考える
選んだ項目について、
「なぜ足りないと感じたのか」
を考えます。
考え方
次の文に当てはめてください。
このゲームは、
「__________」
が少ないため、
「__________」
と感じた。
記入例
このゲームは、
「ステージごとの新しい仕掛け」
が少ないため、
「後半が同じことの繰り返し」
と感じた。
記入欄
このゲームは、
「__________________」
が少ないため、
「__________________」
と感じた。
手順15 足りない部分を補うアイデアを3つ出す
いきなり1つに決めず、まずは3つアイデアを出します。
注意
元のゲームと関係ないアイデアにしないこと。
たとえば、パックマンを分析しているのに、
いきなり剣で敵を倒すアクションゲームにすると、
元の面白さが変わりすぎてしまいます。
アイデアの方向性
| 足りない項目 | アイデアの方向 |
|---|---|
| 報酬の気持ちよさ | コンボ、連続ボーナス、レアアイテム、演出の強化 |
| ステージの変化 | 新しい地形、動く床、変化する迷路、特殊エリア |
| 敵・障害物の面白さ | 新しい敵、動きの違う敵、待ち伏せする敵、進路をふさぐ敵 |
| 操作の気持ちよさ | 新アクション、操作補助、スピード変化、リスクのある技 |
| もう一度遊びたさ | タイムアタック、高得点ボーナス、ランダム配置、チャレンジ目標 |
| ルールのわかりやすさ | チュートリアル、見た目の整理、目的表示、危険表示 |
記入欄
| アイデア | 内容 |
|---|---|
| アイデア1 | __________________ |
| アイデア2 | __________________ |
| アイデア3 | __________________ |
手順16 3つのアイデアを比べる
出した3つのアイデアを比べます。
次の3つの基準で考えます。
- 元のゲームの面白さを壊していないか
- 足りない部分をちゃんと補えているか
- 実際にゲームとして遊びやすそうか
評価表
| アイデア | 元の面白さを壊していない | 足りない部分を補えている | 遊びやすそう | 合計 |
|---|---|---|---|---|
| アイデア1 | _点 | _点 | _点 | _点 |
| アイデア2 | _点 | _点 | _点 | _点 |
| アイデア3 | _点 | _点 | _点 | _点 |
各項目は、1点から5点で評価してください。
点数の意味
| 点数 | 意味 |
|---|---|
| 5点 | とても良い |
| 4点 | 良い |
| 3点 | 普通 |
| 2点 | 少し弱い |
| 1点 | あまり良くない |
手順17 一番よい進化案を1つ選ぶ
手順16で合計点が高かったアイデアを1つ選びます。
記入欄
私が選んだ進化案は、
「__________________」
である。
選んだ理由は、
__________________________________
である。
手順18 進化案の内容を詳しく説明する
選んだ進化案について、詳しく説明します。
書く内容
次の4つを必ず書いてください。
- 何を追加するのか
- いつ出てくるのか
- プレイヤーはどう行動するのか
- それによって何が面白くなるのか
記入欄
追加するもの
__________________________________
いつ出てくるか
__________________________________
プレイヤーはどう行動するか
__________________________________
それによって何が面白くなるか
__________________________________
手順19 元のゲームの面白さを壊していないか確認する
最後に、自分の進化案が
「元のゲームの面白さ」
を壊していないか確認します。
チェック項目
| チェック内容 | はい / いいえ |
|---|---|
| 元のゲームの目的は残っている | __ |
| 元のゲームの基本操作は残っている | __ |
| 元のゲームの緊張感や面白さは残っている | __ |
| 追加した要素が目立ちすぎていない | __ |
| 別のゲームになりすぎていない | __ |
判断
上のチェックで「いいえ」が多い場合は、
アイデアを少し小さくするか、元のゲームに合う形に直してください。
手順20 進化案を一文でまとめる
最後に、自分の進化案を一文でまとめます。
書き方
このゲームは、もともと
「__________」
という面白さがある。
しかし、
「__________」
が足りないと感じた。
そこで、
「__________」
を追加することで、
「__________」
という体験を強くする。
記入欄
このゲームは、もともと
「__________________」
という面白さがある。
しかし、
「__________________」
が足りないと感じた。
そこで、
「__________________」
を追加することで、
「__________________」
という体験を強くする。
提出するもの
次の内容を提出してください。
- ゲーム名
- 中心的な面白さ
- 6項目の評価表
- 星による見える化
- 足りない部分ランキング
- 補いたい項目
- アイデア3つ
- 選んだ進化案
- 元のゲームの面白さを壊していないかのチェック
- 進化案のまとめ文
書くときの注意
1. ただの追加要素で終わらせない
悪い例:
- 新しい敵を追加する
- アイテムを増やす
- ステージを増やす
これだけでは、なぜ面白くなるのかが分かりません。
良い例:
- 進路をふさぐ敵を追加することで、プレイヤーが逃げ道を考える場面を増やす
- 連続でアイテムを取るとボーナスが入るようにして、危険をおかして攻める判断を生む
- ステージの一部が動くようにして、毎回安全な道が変わる緊張感を作る
2. 「AだからBになる」で書く
説明するときは、できるだけ次の形で書きます。
- 〇〇があるから、プレイヤーは□□を考える
- 〇〇を追加することで、□□という行動が生まれる
- 〇〇が変化するため、□□という緊張感が生まれる
- 〇〇を集めると得点が増えるため、□□したくなる
3. 元のゲームを別物にしすぎない
進化案は、元のゲームを完全に作り変えることではありません。
たとえば、パックマンに次のような要素を足すと、別のゲームになりすぎる可能性があります。
- 剣で敵を倒す
- ジャンプアクションにする
- RPGのレベル上げを入れる
- 長いストーリーを入れる
元のゲームの面白さを残したまま、少し広げることを意識してください。
簡単な完成例
ゲーム名
パックマン
中心的な面白さ
敵に追われながら、迷路の中のエサを集める緊張感。
6項目評価
| 評価項目 | 点数 | 星 |
|---|---|---|
| ルールのわかりやすさ | 5点 | ★★★★★ |
| 操作の気持ちよさ | 4点 | ★★★★☆ |
| 報酬の気持ちよさ | 3点 | ★★★☆☆ |
| 敵・障害物の面白さ | 5点 | ★★★★★ |
| ステージの変化 | 2点 | ★★☆☆☆ |
| もう一度遊びたさ | 4点 | ★★★★☆ |
足りない部分ランキング
| 順位 | 足りない部分 | 点数 | 理由 |
|---|---|---|---|
| 1位 | ステージの変化 | 2点 | 迷路の形は変わるが、遊び方の変化が少ないと感じたから |
| 2位 | 報酬の気持ちよさ | 3点 | エサを集める以外の特別な報酬が少ないから |
| 3位 | 操作の気持ちよさ | 4点 | 操作は分かりやすいが、新しい行動は少ないから |
選んだ進化案
一定時間ごとに開閉するゲートを迷路に追加する。
進化案の説明
迷路の一部に、一定時間で開いたり閉じたりするゲートを追加する。
プレイヤーは、敵から逃げながら、ゲートが開くタイミングを見て進む必要がある。
これによって、ただ道を覚えるだけではなく、タイミングを見て進む判断が生まれる。
元の「追われながらエサを集める緊張感」を残したまま、ステージの変化を増やせる。
まとめ
このゲームは、もともと
「敵に追われながらエサを集める緊張感」
という面白さがある。
しかし、
「ステージごとの変化」
が足りないと感じた。
そこで、
「一定時間で開閉するゲート」
を追加することで、
「逃げ道を考えながらタイミングよく進む」
という体験を強くする。
状態遷移でモンスターを管理
ゲームAI課題:状態遷移でモンスターの行動を管理しよう
「今、モンスターは何をしているのか?」を設計する
1. 今日のテーマ
前回は、モンスターに「視覚」を与える考え方を学びました。
isFindPlayer = CheckCanSeePlayer();
このように、モンスターがプレイヤーを見つけているかどうかを判定しました。
そして、その結果を使って行動を変えました。
if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}
この書き方でも、最低限のAIは作れます。
しかし、行動が増えてくると問題が出てきます。
2. if文だけのAIの問題点
問題1:条件が増えると分かりにくい
例えば、モンスターに次の行動を追加したいとします。
- 巡回する
- プレイヤーを見つけたら追いかける
- 近づいたら攻撃する
- 見失ったら探す
- しばらく探して見つからなければ巡回に戻る
- HPが少なくなったら逃げる
これを if 文だけで書くと、次のように複雑になります。
if (isLowHp)
{
Escape();
}
else if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else if (isLostPlayer)
{
Search();
}
else if (isFarFromHome)
{
ReturnHome();
}
else
{
Patrol();
}
一見よさそうに見えます。
しかし、行動が増えるほど、
どの条件を先に書くべきか
今どの行動中なのか
行動の途中で何をすればよいのか
が分かりにくくなります。
問題2:「今何をしているか」が分かりにくい
例えば、モンスターが攻撃中だとします。
攻撃モーションには時間がかかります。
攻撃開始
↓
攻撃モーション中
↓
ダメージ発生
↓
攻撃終了
しかし、毎フレーム if 文だけで行動を決めていると、
if (isAttackRange)
{
Attack();
}
が何度も呼ばれて、攻撃モーションが毎回最初から始まってしまうことがあります。
このように、
今は攻撃中である
今は探索中である
今は巡回中である
という情報を持たないと、行動を管理しづらくなります。
問題3:「見失った後」の行動が作りにくい
視覚判定だけで作ると、プレイヤーが見えなくなった瞬間に待機してしまいます。
if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}
これだと不自然です。
本当は、次のような流れにしたいはずです。
プレイヤーを見つける
↓
追いかける
↓
壁の向こうに逃げられる
↓
見失う
↓
最後に見た場所へ行く
↓
少し探す
↓
見つからなければ巡回に戻る
このような「流れのある行動」を作るには、状態管理が必要です。
3. 状態とは何か
状態とは、
今、そのキャラクターが何をしているか
を表すものです。
例えば、モンスターには次のような状態があります。
| 状態 | 意味 |
|---|---|
| Idle | その場で待っている |
| Patrol | 巡回している |
| Chase | プレイヤーを追いかけている |
| Attack | 攻撃している |
| Search | プレイヤーを探している |
| Return | 持ち場に戻っている |
| Escape | 逃げている |
このように、行動を状態として整理します。
4. enum class で状態を作る
C++では、状態を enum class で作ると分かりやすくなります。
enum class EnemyState
{
Idle,
Patrol,
Chase,
Attack,
Search,
Return,
Escape
};
そして、敵が現在どの状態なのかを変数で持ちます。
EnemyState state = EnemyState::Patrol;
これで、モンスターは
今は Patrol 状態である
今は Chase 状態である
今は Attack 状態である
という情報を持てるようになります。
5. 状態ごとに行動を分ける
状態を持ったら、switch 文で行動を分けます。
switch (state)
{
case EnemyState::Idle:
Idle();
break;
case EnemyState::Patrol:
Patrol();
break;
case EnemyState::Chase:
ChasePlayer();
break;
case EnemyState::Attack:
Attack();
break;
case EnemyState::Search:
Search();
break;
case EnemyState::Return:
ReturnHome();
break;
case EnemyState::Escape:
Escape();
break;
}
これで、
現在の状態に応じて
呼び出す行動を変える
ことができます。
6. 状態遷移とは何か
状態遷移とは、
ある状態から、別の状態に切り替わること
です。
例えば、
Patrol
↓ プレイヤーを見つけた
Chase
これは、
if (state == EnemyState::Patrol)
{
if (isFindPlayer)
{
state = EnemyState::Chase;
}
}
と書けます。
7. 状態遷移の例
モンスターの基本的な状態遷移を考えると、次のようになります。
Patrol
↓ プレイヤーを見つけた
Chase
↓ 攻撃範囲に入った
Attack
↓ 攻撃範囲から出た
Chase
↓ プレイヤーを見失った
Search
↓ 見つからなかった
Patrol
このように、
何が起きたら
どの状態に変わるのか
を考えるのが、状態遷移の設計です。
8. 視覚判定と状態遷移をつなげる
前回作った視覚判定を使います。
isFindPlayer = CheckCanSeePlayer();
これを状態遷移に使います。
if (state == EnemyState::Patrol)
{
if (isFindPlayer)
{
state = EnemyState::Chase;
}
}
つまり、
視覚で見つける
↓
状態を切り替える
↓
行動が変わる
という流れになります。
9. 状態遷移表
状態遷移は、表で考えると分かりやすくなります。
| 現在の状態 | 条件 | 次の状態 |
|---|---|---|
| Patrol | プレイヤーを見つけた | Chase |
| Chase | 攻撃範囲に入った | Attack |
| Chase | プレイヤーを見失った | Search |
| Attack | 攻撃範囲から出た | Chase |
| Search | プレイヤーを見つけた | Chase |
| Search | 3秒探して見つからない | Patrol |
プログラムを書く前に、この表を作ると整理しやすくなります。
10. 状態遷移図
表だけでなく、図にするとさらに分かりやすくなります。
プレイヤー発見
+--------------------+
| v
+--------+ +--------+
| Patrol | | Chase |
+--------+ +--------+
^ |
| | 攻撃範囲
| 3秒探して v
| 見つからない +--------+
+--------+ | Attack |
| Search | +--------+
+--------+ |
^ |
| 見失った | 範囲外
+--------------------+
このように、状態同士のつながりを線で表します。
11. 今日の課題
課題テーマ
見失ったら探すモンスターAIを設計しよう
モンスターの仕様
今回設計するモンスターは、次のように動きます。
1. 普段は決められた場所を巡回している
2. プレイヤーが視界に入ったら追いかける
3. プレイヤーが攻撃範囲に入ったら攻撃する
4. プレイヤーが攻撃範囲から出たらまた追いかける
5. プレイヤーを見失ったら、その場で探す
6. 3秒探しても見つからなければ巡回に戻る
12. 使用してよい状態
今回は、次の状態を使います。
enum class EnemyState
{
Patrol,
Chase,
Attack,
Search
};
それぞれの意味は次の通りです。
| 状態 | 内容 |
|---|---|
| Patrol | 決められた場所を巡回する |
| Chase | プレイヤーを追いかける |
| Attack | プレイヤーを攻撃する |
| Search | プレイヤーを探す |
13. 使用してよいフラグ
今回は、次のフラグを使います。
bool isFindPlayer; // プレイヤーを見つけている
bool isAttackRange; // 攻撃範囲に入っている
bool isSearchTimeOver; // 探索時間が終わった
フラグの意味
| フラグ | 意味 |
|---|---|
| isFindPlayer | 視界にプレイヤーがいる |
| isAttackRange | プレイヤーが攻撃できる距離にいる |
| isSearchTimeOver | 探索時間が終わった |
14. 課題1:状態の意味を説明する
次の状態について、自分の言葉で説明しなさい。
| 状態 | 自分の説明 |
|---|---|
| Patrol | |
| Chase | |
| Attack | |
| Search |
15. 課題2:状態遷移表を完成させる
次の表を完成させなさい。
| 現在の状態 | 条件 | 次の状態 |
|---|---|---|
| Patrol | isFindPlayer == true |
|
| Chase | isAttackRange == true |
|
| Chase | isFindPlayer == false |
|
| Attack | isAttackRange == false |
|
| Search | isFindPlayer == true |
|
| Search | isSearchTimeOver == true |
16. 課題3:状態遷移図を描く
次の状態を使って、状態遷移図を描きなさい。
Patrol
Chase
Attack
Search
矢印には、状態が切り替わる条件を書きなさい。
例:
Patrol -- isFindPlayer --> Chase
17. 課題4:状態遷移の疑似コードを書く
次のひな形を使って、状態遷移の疑似コードを書きなさい。
if (state == EnemyState::Patrol)
{
if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Chase)
{
if (__________)
{
state = EnemyState::__________;
}
else if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Attack)
{
if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Search)
{
if (__________)
{
state = EnemyState::__________;
}
else if (__________)
{
state = EnemyState::__________;
}
}
18. 課題5:行動処理の疑似コードを書く
状態ごとに、どの関数を呼び出すかを書きなさい。
switch (state)
{
case EnemyState::Patrol:
__________();
break;
case EnemyState::Chase:
__________();
break;
case EnemyState::Attack:
__________();
break;
case EnemyState::Search:
__________();
break;
}
使ってよい関数名:
Patrol();
ChasePlayer();
Attack();
Search();
19. 課題6:最終的なUpdate処理を組み立てる
次の流れになるように、Update() の疑似コードを完成させなさい。
1. 視覚判定を行う
2. 攻撃範囲判定を行う
3. 探索時間が終わったか調べる
4. 状態遷移を行う
5. 現在の状態に応じた行動を行う
ひな形
void Enemy::Update()
{
// 1. 視覚判定
isFindPlayer = ____________________;
// 2. 攻撃範囲判定
isAttackRange = ____________________;
// 3. 探索時間判定
isSearchTimeOver = ____________________;
// 4. 状態遷移
if (state == EnemyState::Patrol)
{
if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Chase)
{
if (__________)
{
state = EnemyState::__________;
}
else if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Attack)
{
if (__________)
{
state = EnemyState::__________;
}
}
else if (state == EnemyState::Search)
{
if (__________)
{
state = EnemyState::__________;
}
else if (__________)
{
state = EnemyState::__________;
}
}
// 5. 状態ごとの行動
switch (state)
{
case EnemyState::Patrol:
__________();
break;
case EnemyState::Chase:
__________();
break;
case EnemyState::Attack:
__________();
break;
case EnemyState::Search:
__________();
break;
}
}
20. 課題7:このAIの弱点を考える
このモンスターAIには、まだ弱点があります。
例えば、
- Search状態でどこを探すのか決まっていない
- 見失った瞬間のプレイヤー位置を覚えていない
- 攻撃モーション中にすぐChaseへ戻るかもしれない
- Patrolのルートが決まっていない
- 壁を避けて移動できない
などです。
自分で、このAIの弱点を3つ書きなさい。
21. 課題8:改良案を考える
課題7で書いた弱点を1つ選び、改良案を書きなさい。
例:
弱点:
Search状態でどこを探すのか決まっていない。
改良案:
プレイヤーを最後に見た位置を lastPlayerPos として保存し、
Search状態ではその場所に移動してから周囲を探す。
22. 90分の進め方
| 時間 | 内容 |
|---|---|
| 0〜15分 | 資料前半を読む |
| 15〜25分 | 課題1:状態の意味を説明する |
| 25〜40分 | 課題2:状態遷移表を完成させる |
| 40〜55分 | 課題3:状態遷移図を描く |
| 55〜70分 | 課題4・5:疑似コードを書く |
| 70〜85分 | 課題6:Update処理を組み立てる |
| 85〜90分 | 課題7・8:弱点と改良案を書く |
時間が足りない場合、課題8は宿題にしてもよい。
23. まとめ
今回の重要ポイントは次の通りです。
if文だけでは、行動が増えるほど管理しづらくなる
状態とは「今何をしているか」を表す情報
状態遷移とは「条件によって状態が切り替わること」
視覚判定は、状態遷移の条件として使える
状態管理を使うと、見失う・探す・戻るなどの自然な行動を作りやすい
3Dの移動計算(回転して移動)[ゲーム数学、プログラミング基礎]
戦車を「向いている方向」に動かすプログラムを作ろう
1. 今日の目標
今回は、3D空間で戦車を動かす処理を作ります。
完成すると、次の操作ができるようになります。
| キー | 動き |
|---|---|
| Aキー | 左に回転する |
| Dキー | 右に回転する |
| Wキー | 戦車が向いている方向に進む |
今回のポイントは、ただ前に進むだけではありません。
ワールドの+Z方向に進むのではなく、
戦車が向いている方向に進む
という処理を作ります。
2. 今日作る処理の方針
今回の移動処理は、次の流れで作ります。
Aキー・Dキーで戦車の角度を変える
↓
現在位置を計算用のベクトル型に変換する
↓
まずは+Z方向に進む移動ベクトルを作る
↓
戦車のY軸回転角度から回転行列を作る
↓
移動ベクトルを回転行列で回転させる
↓
Wキーが押されたら、現在位置に移動ベクトルを足す
↓
計算結果をtransform_.position_に戻す
この流れを先に確認しておくと、これから書く1行1行の意味がわかりやすくなります。
3. なぜ移動ベクトルを回転させるのか
戦車を動かすだけなら、現在位置に移動量を足せば動きます。
しかし今回は、
戦車が向いている方向に進む
ようにしたいです。
例えば、移動ベクトルを常に
+Z方向に 0.1 進む
としてしまうと、戦車が右を向いても左を向いても、移動方向はずっと+Z方向のままです。
これでは、戦車の見た目は横を向いているのに、移動だけは+Z方向へ滑っていくような動きになります。
そこで、次のように考えます。
まず、前に進む移動ベクトルを作る
↓
その移動ベクトルを、戦車の向きと同じだけ回転させる
↓
回転した移動ベクトルを、現在位置に足す
今回一番大事なのは、
移動ベクトルを、戦車の向きに合わせて回転させる
という考え方です。
4. 3D空間での「戦車の向き」
今回の戦車は、最初は +Z方向 を向いているものとします。
上から見たイメージは次のようになります。
+Z
↑
|
|
-X ←---- 戦車 ----→ +X
|
|
↓
-Z
戦車が左右に向きを変えるときは、主に Y軸回転 を使います。
Y軸回転とは、上から見たときに向きを変える回転です。
5. 必要な情報
戦車を動かすには、次の情報が必要です。
| 必要な情報 | 内容 |
|---|---|
| 現在位置 | 戦車が今どこにいるか |
| 回転角度 | 戦車がどちらを向いているか |
| 移動量 | 1フレームでどれだけ進むか |
今回使う変数は、次のようなものだと考えます。
transform_.position_ // 戦車の位置
transform_.rotate_.y // 戦車のY軸回転
穴埋めでUpdate関数を作る
ここから、戦車の Update 関数を少しずつ完成させます。
最初から完成コードを見るのではなく、処理の意味を確認しながら、1行ずつ作っていきます。
6. AキーとDキーで回転させる
まず、AキーとDキーで戦車の角度を変えます。
| キー | 処理 |
|---|---|
| Aキー | Y軸回転角度を小さくする |
| Dキー | Y軸回転角度を大きくする |
穴埋め 1
次のコードを完成させなさい。
// Aキーが押されたら左回転
if (Input::IsKey(DIK_A))
{
transform_.rotate_.y ____ 2;
}
// Dキーが押されたら右回転
if (Input::IsKey(DIK_D))
{
transform_.rotate_.y ____ 2;
}
ヒント
左回転では角度を小さくします。
右回転では角度を大きくします。
7. 現在位置を計算用の型に変換する
transform_.position_ は、戦車の現在位置です。
ただし、DirectXMathでベクトル計算をするためには、XMVECTOR 型に変換して使います。
そのため、まず現在位置を XMVECTOR 型の変数に読み込みます。
穴埋め 2
次のコードを完成させなさい。
// 戦車の現在地をベクトル型に変換
XMVECTOR vPos = ____________________________;
使う関数
XMLoadFloat3(&transform_.position_)
8. まずは「前に進むベクトル」を作る
次に、1フレーム分の移動ベクトルを作ります。
今回は、戦車が最初に向いている方向を +Z方向 とします。
そのため、最初の移動ベクトルは次のように考えます。
X方向には進まない
Y方向には進まない
Z方向に 0.1 進む
つまり、移動ベクトルは次のようになります。
| 成分 | 値 | 意味 |
|---|---|---|
| X | 0.0 | 横方向には進まない |
| Y | 0.0 | 上下方向には進まない |
| Z | 0.1 | 前方向に進む |
| W | 0.0 | ここでは移動ベクトルとして扱う |
穴埋め 3
次のコードを完成させなさい。
// 1フレームの移動ベクトル
XMVECTOR vMove{ ____ , ____ , ____ , ____ };
入れる値
0.0f
0.0f
0.1f
0.0f
9. このままだと何が問題か
ここまでだと、移動ベクトルは常に次の意味になります。
+Z方向に0.1進む
しかし、戦車が回転している場合、このままでは問題があります。
戦車が右を向いても、左を向いても、移動方向はずっと +Z方向 のままです。
穴埋め 4
次の文章を完成させなさい。
このままだと、戦車の見た目が回転しても、
移動方向はずっと __________ 方向のままになる。
そのため、戦車が横を向いているのに、
__________ 方向へ滑るように進んでしまう。
10. 回転行列を作る
移動ベクトルを戦車の向きに合わせるには、移動ベクトルを回転させる必要があります。
ベクトルを回転させるには、回転行列を作ります。
今回はY軸まわりに回転させたいので、使う関数は次のものです。
XMMatrixRotationY()
ただし、この関数に渡す角度は 度 ではなく ラジアン です。
そのため、角度をラジアンに変換する必要があります。
使う関数は次のものです。
XMConvertToRadians()
穴埋め 5
次のコードを完成させなさい。
// transform_.rotate_.y度回転させる行列を作成
XMMATRIX mRotY = XMMatrixRotationY(____________________________);
使う式
XMConvertToRadians(transform_.rotate_.y)
11. 移動ベクトルを回転行列で変形する
回転行列を作ったら、移動ベクトルをその行列で変形します。
使う関数は次のものです。
XMVector3TransformCoord()
この処理によって、
+Z方向に進むベクトル
が、
戦車が向いている方向に進むベクトル
に変わります。
穴埋め 6
次のコードを完成させなさい。
// 移動ベクトルを戦車の向きに合わせて回転
vMove = XMVector3TransformCoord(__________, __________);
入れるもの
vMove
mRotY
12. Wキーで前進する
最後に、Wキーが押されたら戦車を移動させます。
移動は、次の考え方です。
現在位置 = 現在位置 + 移動ベクトル
つまり、現在位置に、今回作った移動ベクトルを足します。
そのあと、計算した位置を transform_.position_ に戻します。
穴埋め 7
次のコードを完成させなさい。
// Wキーが押されたら
if (Input::IsKey(DIK_W))
{
// 移動
vPos ____ vMove;
// 現在地をtransform_.position_に戻す
XMStoreFloat3(________________________, ________);
}
ヒント
現在位置に移動量を足すので、次のようになります。
vPos += vMove;
XMStoreFloat3 では、計算した vPos を transform_.position_ に戻します。
13. Update関数を完成させよう
ここまでの穴埋めをすべてつなげると、戦車の移動処理になります。
穴埋め 8
次の Update 関数を完成させなさい。
// 更新
void Tank::Update()
{
// Aキーが押されたら左回転
if (Input::IsKey(DIK_A))
{
transform_.rotate_.y ____ 2;
}
// Dキーが押されたら右回転
if (Input::IsKey(DIK_D))
{
transform_.rotate_.y ____ 2;
}
// 戦車の現在地をベクトル型に変換
XMVECTOR vPos = ____________________________;
// 1フレームの移動ベクトル
XMVECTOR vMove{ ____ , ____ , ____ , ____ };
// Y軸回転行列を作成
XMMATRIX mRotY = XMMatrixRotationY(____________________________);
// 移動ベクトルを戦車の向きに合わせて回転
vMove = XMVector3TransformCoord(__________, __________);
// Wキーが押されたら
if (Input::IsKey(DIK_W))
{
// 移動
vPos ____ vMove;
// 計算結果をtransform_.position_に戻す
XMStoreFloat3(________________________, ________);
}
}
14. 完成コード
自分で穴埋めを終えてから確認しましょう。
// 更新
void Tank::Update()
{
// Aキーが押されたら左回転
if (Input::IsKey(DIK_A))
{
transform_.rotate_.y -= 2;
}
// Dキーが押されたら右回転
if (Input::IsKey(DIK_D))
{
transform_.rotate_.y += 2;
}
// 戦車の現在地をベクトル型に変換
XMVECTOR vPos = XMLoadFloat3(&transform_.position_);
// 1フレームの移動ベクトル
XMVECTOR vMove{ 0.0f, 0.0f, 0.1f, 0.0f };
// transform_.rotate_.y度回転させる行列を作成
XMMATRIX mRotY = XMMatrixRotationY(XMConvertToRadians(transform_.rotate_.y));
// 移動ベクトルを戦車の向きに合わせて回転
vMove = XMVector3TransformCoord(vMove, mRotY);
// Wキーが押されたら
if (Input::IsKey(DIK_W))
{
// 移動
vPos += vMove;
// 現在地をtransform_.position_に戻す
XMStoreFloat3(&transform_.position_, vPos);
}
}
15. 処理の流れをもう一度確認する
今回の処理は、次の順番でした。
Aキー・Dキーで戦車の角度を変える
↓
現在位置を計算用のベクトル型に変換する
↓
まずは+Z方向に進む移動ベクトルを作る
↓
戦車のY軸回転角度から回転行列を作る
↓
移動ベクトルを回転行列で回転させる
↓
Wキーが押されたら、現在位置に移動ベクトルを足す
↓
計算結果をtransform_.position_に戻す
重要なのは、次の流れです。
前方向の移動ベクトルを作る
↓
戦車の向きに合わせて回転させる
↓
現在位置に足す
16. まとめ穴埋め
次の文章を完成させなさい。
戦車を向いている方向に進ませるには、
まず __________ 方向に進む移動ベクトルを作る。
しかし、そのままだと常に __________ 方向に進んでしまう。
そこで、戦車の __________ 角度を使って、
__________ 行列を作る。
その行列で __________ ベクトルを回転させることで、
戦車の向いている方向に進めるようになる。
最後に、現在位置に __________ を足すことで、
戦車の位置が更新される。
17. 確認問題
問題1
次の処理は何をしているか説明しなさい。
transform_.rotate_.y += 2;
問題2
次の移動ベクトルは、どの方向にどれだけ進むベクトルか答えなさい。
XMVECTOR vMove{ 0.0f, 0.0f, 0.1f, 0.0f };
問題3
なぜ、移動ベクトルを回転させる必要があるのか説明しなさい。
問題4
次の処理は何をしているか説明しなさい。
vPos += vMove;
問題5
XMMatrixRotationY() に角度を渡す前に、なぜ XMConvertToRadians() を使っているのか説明しなさい。
18. 解答例
穴埋め1
transform_.rotate_.y -= 2;
transform_.rotate_.y += 2;
穴埋め2
XMLoadFloat3(&transform_.position_)
穴埋め3
XMVECTOR vMove{ 0.0f, 0.0f, 0.1f, 0.0f };
穴埋め4
このままだと、戦車の見た目が回転しても、
移動方向はずっと +Z 方向のままになる。
そのため、戦車が横を向いているのに、
+Z 方向へ滑るように進んでしまう。
穴埋め5
XMConvertToRadians(transform_.rotate_.y)
穴埋め6
vMove = XMVector3TransformCoord(vMove, mRotY);
穴埋め7
vPos += vMove;
XMStoreFloat3(&transform_.position_, vPos);
穴埋め8
// 更新
void Tank::Update()
{
// Aキーが押されたら左回転
if (Input::IsKey(DIK_A))
{
transform_.rotate_.y -= 2;
}
// Dキーが押されたら右回転
if (Input::IsKey(DIK_D))
{
transform_.rotate_.y += 2;
}
// 戦車の現在地をベクトル型に変換
XMVECTOR vPos = XMLoadFloat3(&transform_.position_);
// 1フレームの移動ベクトル
XMVECTOR vMove{ 0.0f, 0.0f, 0.1f, 0.0f };
// Y軸回転行列を作成
XMMATRIX mRotY = XMMatrixRotationY(XMConvertToRadians(transform_.rotate_.y));
// 移動ベクトルを戦車の向きに合わせて回転
vMove = XMVector3TransformCoord(vMove, mRotY);
// Wキーが押されたら
if (Input::IsKey(DIK_W))
{
// 移動
vPos += vMove;
// 計算結果をtransform_.position_に戻す
XMStoreFloat3(&transform_.position_, vPos);
}
}
まとめ穴埋め
戦車を向いている方向に進ませるには、
まず +Z 方向に進む移動ベクトルを作る。
しかし、そのままだと常に +Z 方向に進んでしまう。
そこで、戦車の Y軸回転 角度を使って、
回転 行列を作る。
その行列で 移動 ベクトルを回転させることで、
戦車の向いている方向に進めるようになる。
最後に、現在位置に 移動ベクトル を足すことで、
戦車の位置が更新される。
19. まとめ
戦車の移動
1. A/Dキーで角度を変える
2. 前方向の移動ベクトルを作る
3. 戦車の角度から回転行列を作る
4. 移動ベクトルを回転させる
5. 現在位置に足す
重要:
位置を回転させるのではなく、
移動方向を回転させてから位置に足す。
ゲームにおけるフィードバックの研究
ゲーム研究レポート
ゲームにおける入力・出力・フィードバックを調べる
この授業でやること
この授業では、1つのゲームだけを詳しく調べるのではなく、
いろいろなゲームを見比べながら、
入力・出力・フィードバック の関係を調べます。
ゲームでは、プレイヤーがボタンやキーを押すと、
ゲーム内で何かが起こります。
そして、その結果をプレイヤーに伝えるために、
画面・音・エフェクト・振動・UIなどの反応が返ってきます。
この流れを、次のように考えます。
入力
↓
ゲーム内の処理
↓
出力
↓
フィードバック
↓
プレイヤーの理解
ゲームプログラマは、
「入力を受け取って処理する」だけではなく、
「処理結果をプレイヤーに分かりやすく返す」ことも考える必要があります。
今日のゴール
授業の最後に、次のことを説明できるようになることを目指します。
- ゲームにおける入力とは何か
- ゲームにおける出力とは何か
- フィードバックとは何か
- 視覚・聴覚・触覚のフィードバックにはどんな違いがあるか
- 入力とフィードバックがどうつながっているか
- プログラマとして、どんな処理を実装する必要があるか
- 自分のゲーム制作に、調べた内容をどう活かせるか
授業時間の目安
| 時間 | 内容 |
|---|---|
| 10分 | 入力・出力・フィードバックの説明 |
| 10分 | 調査するゲームを選ぶ |
| 15分 | 入力と出力を調べる |
| 20分 | 視覚・聴覚・触覚フィードバックを調べる |
| 15分 | 複数の感覚を組み合わせたフィードバックを調べる |
| 10分 | プログラマとして必要な処理を考える |
| 10分 | 最終まとめを書く |
手順1 入力・出力・フィードバックの意味を確認する
入力とは
入力とは、プレイヤーがゲームに対して行う操作のことです。
入力の例
- 方向キーを押す
- スティックを倒す
- ボタンを押す
- ボタンを長押しする
- ボタンを離す
- ボタンを連打する
- 複数のボタンを同時に押す
- マウスを動かす
- マウスをクリックする
- 画面をタップする
- 画面をスワイプする
- コントローラーを傾ける
出力とは
出力とは、入力やゲーム内の処理の結果として、
ゲームが画面・音・振動・UIなどで表すものです。
出力の例
- キャラクターが動く
- キャラクターがジャンプする
- 弾が出る
- 攻撃モーションが出る
- 敵がダメージを受ける
- HPが減る
- スコアが増える
- 効果音が鳴る
- エフェクトが出る
- 画面が揺れる
- メニューが開く
- コントローラーが振動する
フィードバックとは
フィードバックとは、プレイヤーに
「何が起きたか」
「操作が成功したか」
「失敗したか」
「危険なのか」
「報酬を得たのか」
を伝える反応のことです。
フィードバックの例
- ジャンプしたときにジャンプ音が鳴る
- 攻撃が当たったときにヒットエフェクトが出る
- ダメージを受けたときにキャラクターが点滅する
- コインを取ったときに取得音が鳴る
- 残り時間が少なくなると警告音が鳴る
- HPが減るとHPバーが短くなる
- 爆発が起きると画面が揺れる
- 攻撃を受けるとコントローラーが振動する
手順2 調査するゲームを複数選ぶ
今回は、1つのゲームだけでなく、複数のゲームを見比べます。
最低でも 3本以上 のゲームを調べてください。
余裕がある場合は、5本程度調べてもよいです。
実際に遊んでも、プレイ動画で確認してもかまいません。
選ぶゲームの例
できれば、違うジャンルのゲームを選んでください。
| ジャンル | 例 |
|---|---|
| アクション | スーパーマリオ、ロックマン、カービィ |
| シューティング | スペースインベーダー、グラディウス、ツインビー |
| パズル | テトリス、ぷよぷよ、ドクターマリオ |
| 格闘 | ストリートファイター、スマブラ |
| レース | マリオカート、F-ZERO |
| RPG | ドラクエ、ポケモン |
| 音楽ゲーム | 太鼓の達人、リズム天国 |
| スマホゲーム | パズドラ、モンスト、ツムツム |
記入欄
| 番号 | ゲーム名 | ジャンル | 調べ方 |
|---|---|---|---|
| 1 | __________ | ________ | 実際に遊んだ / 動画で見た / 調べた |
| 2 | __________ | ________ | 実際に遊んだ / 動画で見た / 調べた |
| 3 | __________ | ________ | 実際に遊んだ / 動画で見た / 調べた |
| 4 | __________ | ________ | 実際に遊んだ / 動画で見た / 調べた |
| 5 | __________ | ________ | 実際に遊んだ / 動画で見た / 調べた |
手順3 入力の種類を集める
調べたゲームの中から、プレイヤーが行う入力を集めます。
入力の種類の例
| 入力の種類 | 内容 |
|---|---|
| 押す | ボタンを1回押す |
| 長押し | ボタンを押し続ける |
| 離す | 押していたボタンを離す |
| 連打 | 何度もボタンを押す |
| 同時押し | 複数のボタンを同時に押す |
| 方向入力 | 十字キーやスティックで方向を入れる |
| タイミング入力 | 決まったタイミングで押す |
| タップ | 画面を触る |
| スワイプ | 画面をなぞる |
| ドラッグ | 押したまま動かす |
| マウス移動 | カーソルを動かす |
| ジャイロ操作 | 本体やコントローラーを傾ける |
記入欄
| ゲーム名 | 入力 | その入力で起きること |
|---|---|---|
| ______ | ______ | ____________ |
| ______ | ______ | ____________ |
| ______ | ______ | ____________ |
| ______ | ______ | ____________ |
| ______ | ______ | ____________ |
| ______ | ______ | ____________ |
手順4 入力から出力までの流れを書く
次に、入力したあとに何が起こるのかを調べます。
書き方
次の形で考えます。
プレイヤーが入力する
↓
ゲーム内で処理が行われる
↓
画面や音などに出力される
↓
プレイヤーが結果を理解する
記入例
入力:Aボタンを押す
↓
処理:キャラクターに上向きの速度を与える
↓
出力:キャラクターがジャンプする
↓
フィードバック:ジャンプ音が鳴り、ジャンプアニメーションになる
↓
プレイヤーの理解:ジャンプできたと分かる
記入欄
| ゲーム名 | 入力 | ゲーム内の処理・結果 | プレイヤーに返る反応 |
|---|---|---|---|
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
手順5 フィードバックを感覚ごとに分類する
ゲームのフィードバックは、プレイヤーの感覚ごとに分けて考えることができます。
主に次の3つに分けます。
| 種類 | 使う感覚 | 例 |
|---|---|---|
| 視覚フィードバック | 目で見る | エフェクト、点滅、UI、画面揺れ、アニメーション |
| 聴覚フィードバック | 耳で聞く | 効果音、BGM変化、警告音、決定音 |
| 触覚フィードバック | 手や体で感じる | コントローラー振動、スマホの振動、押した感触 |
手順6 視覚フィードバックを調べる
視覚フィードバックとは
視覚フィードバックとは、画面を見て分かる反応のことです。
プレイヤーは、画面の変化を見ることで、
自分の操作が成功したのか、失敗したのか、危険なのかを判断します。
視覚フィードバックの例
| 場面 | 視覚フィードバック | 伝えていること |
|---|---|---|
| 攻撃が当たった | ヒットエフェクトが出る | 攻撃が成功した |
| ダメージを受けた | キャラクターが点滅する | ダメージを受けた |
| 危険が近い | 床や敵が赤く光る | 危険な状態である |
| アイテムを取った | アイテムが消え、光る演出が出る | アイテム取得に成功した |
| HPが減った | HPバーが短くなる | 残り体力が減った |
| 強い攻撃が当たった | 画面が揺れる | 大きな衝撃があった |
プログラムで必要そうな処理
| 表現 | 必要そうな処理 |
|---|---|
| エフェクト表示 | エフェクト生成、位置指定、再生、削除 |
| 点滅 | 表示・非表示の切り替え、タイマー管理 |
| UI更新 | HPやスコアの値を変更し、画面に反映 |
| 画面揺れ | カメラ位置を短時間だけ変化させる |
| 色変更 | ダメージ時や警告時に色を変更する |
| アニメーション変更 | 状態に応じてモーションを切り替える |
記入欄
| ゲーム名 | 場面 | 視覚フィードバック | 何を伝えているか |
|---|---|---|---|
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
手順7 聴覚フィードバックを調べる
聴覚フィードバックとは
聴覚フィードバックとは、音で分かる反応のことです。
効果音やBGMによって、プレイヤーは画面を細かく見ていなくても、
成功・失敗・危険・報酬などに気づくことができます。
聴覚フィードバックの例
| 場面 | 聴覚フィードバック | 伝えていること |
|---|---|---|
| ジャンプした | ジャンプ音が鳴る | ジャンプした |
| 攻撃が当たった | ヒット音が鳴る | 攻撃が成功した |
| ダメージを受けた | ダメージ音が鳴る | 失敗・被害を受けた |
| アイテムを取った | 取得音が鳴る | 報酬を得た |
| メニューを決定した | 決定音が鳴る | 入力が受け付けられた |
| 危険が近い | 警告音が鳴る | 危険が迫っている |
| 残り時間が少ない | BGMが速くなる | 急ぐ必要がある |
プログラムで必要そうな処理
| 表現 | 必要そうな処理 |
|---|---|
| 効果音再生 | 条件成立時にSEを再生する |
| BGM変更 | 状態に応じてBGMを切り替える |
| 音量変更 | 距離や状況に応じて音量を変える |
| 警告音 | 危険状態の間、一定間隔で鳴らす |
| 決定音・キャンセル音 | UI操作時に入力に応じて音を鳴らす |
| 連続音の制御 | 同じ音が鳴りすぎないように制限する |
記入欄
| ゲーム名 | 場面 | 聴覚フィードバック | 何を伝えているか |
|---|---|---|---|
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
手順8 触覚フィードバックを調べる
触覚フィードバックとは
触覚フィードバックとは、手や体で感じる反応のことです。
代表的なものは、コントローラーやスマートフォンの振動です。
プレイヤーは振動によって、
衝撃・ダメージ・成功・危険などを感覚的に理解できます。
触覚フィードバックの例
| 場面 | 触覚フィードバック | 伝えていること |
|---|---|---|
| 攻撃が当たった | 短く振動する | 攻撃の手応え |
| ダメージを受けた | 強く振動する | 被害を受けた |
| 爆発が起きた | 大きく長めに振動する | 大きな衝撃 |
| 車が悪路を走る | 細かく振動する | 路面の感触 |
| 危険が近い | 弱い振動を繰り返す | 危険が近づいている |
| 釣りゲームで魚が引く | 不規則に振動する | 魚の動きや抵抗 |
プログラムで必要そうな処理
| 表現 | 必要そうな処理 |
|---|---|
| 短い振動 | 成功時やヒット時に短時間だけ振動 |
| 強い振動 | ダメージや爆発時に強めに振動 |
| 弱い振動の繰り返し | 危険状態をタイマーで管理して振動 |
| 振動の強弱 | 状況に応じて振動の強さを変える |
| 振動時間の制御 | 長すぎないように時間で止める |
| 機種対応 | 振動できる機器か確認する |
記入欄
| ゲーム名 | 場面 | 触覚フィードバック | 何を伝えているか |
|---|---|---|---|
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
| ____ | ____ | ________ | ________ |
手順9 複数の感覚を組み合わせたフィードバックを調べる
ゲームでは、1つの出来事に対して、
視覚・聴覚・触覚のフィードバックを同時に使うことが多いです。
例:攻撃が当たったとき
| 感覚 | フィードバック |
|---|---|
| 視覚 | ヒットエフェクト、敵の点滅 |
| 聴覚 | ヒット音 |
| 触覚 | 短い振動 |
このように複数の反応を組み合わせることで、
プレイヤーは「攻撃が当たった」と強く感じることができます。
記入欄
| ゲーム名 | 場面 | 視覚 | 聴覚 | 触覚 | プレイヤーに伝わること |
|---|---|---|---|---|---|
| ____ | 攻撃が当たった | ____ | ____ | ____ | ____ |
| ____ | ダメージを受けた | ____ | ____ | ____ | ____ |
| ____ | アイテムを取った | ____ | ____ | ____ | ____ |
| ____ | 危険が近い | ____ | ____ | ____ | ____ |
| ____ | クリアした | ____ | ____ | ____ | ____ |
手順10 UIによるフィードバックを調べる
UIは、視覚フィードバックの中でも、プレイヤーに状態を伝える重要な要素です。
UIの例
- HPバー
- スコア
- 残り時間
- 残機
- 弾数
- スタミナ
- アイテム欄
- ミニマップ
- クエスト表示
- ボタン表示
- チュートリアル表示
記入欄
| ゲーム名 | UI表示 | 何を伝えているか | 見やすさ |
|---|---|---|---|
| ____ | ________ | ________ | 見やすい / 見にくい |
| ____ | ________ | ________ | 見やすい / 見にくい |
| ____ | ________ | ________ | 見やすい / 見にくい |
| ____ | ________ | ________ | 見やすい / 見にくい |
| ____ | ________ | ________ | 見やすい / 見にくい |
手順11 フィードバックが弱いとどうなるか考える
フィードバックが弱いと、プレイヤーは何が起きたのか分かりにくくなります。
例
| フィードバックが弱い状態 | プレイヤーが感じること |
|---|---|
| 攻撃が当たっても音が鳴らない | 当たったか分からない |
| ダメージを受けても画面変化が少ない | いつHPが減ったか分からない |
| 危険な攻撃の予備動作がない | 急にやられたと感じる |
| ボタンを押しても反応が薄い | 操作している感じがしない |
| UIが見にくい | 状況を判断しにくい |
| 報酬を取っても演出が弱い | 嬉しさが弱い |
記入欄
フィードバックが弱いと、プレイヤーは
__________________________________
と感じる。
特にゲームで問題になるのは、
__________________________________
である。
その理由は、
__________________________________
である。
手順12 入力とフィードバックの関係を図にする
調べた内容から、入力とフィードバックの流れを図の形で整理します。
記入欄1
入力:__________
↓
処理:__________
↓
出力:__________
↓
視覚フィードバック:__________
↓
聴覚フィードバック:__________
↓
触覚フィードバック:__________
↓
プレイヤーの理解:__________
記入欄2
入力:__________
↓
処理:__________
↓
出力:__________
↓
視覚フィードバック:__________
↓
聴覚フィードバック:__________
↓
触覚フィードバック:__________
↓
プレイヤーの理解:__________
手順13 プログラマとして必要な処理を考える
フィードバックは、プログラムで実装する必要があります。
例
| やりたいこと | 必要な処理 |
|---|---|
| ジャンプさせたい | 入力判定、速度変更、重力、着地判定 |
| 攻撃を当てたい | 攻撃判定、敵との当たり判定、ダメージ処理 |
| 攻撃の手応えを出したい | 効果音、ヒットエフェクト、画面揺れ、ヒットストップ |
| HPを減らしたい | ダメージ計算、HP更新、UI更新 |
| 危険を知らせたい | 攻撃前の予備動作、警告表示、警告音 |
| アイテム取得を伝えたい | アイテム削除、スコア加算、取得音、エフェクト |
| 振動を入れたい | 対応機器確認、振動開始、振動時間管理、振動停止 |
記入欄
| 作りたいフィードバック | 必要そうなプログラム処理 |
|---|---|
| ________ | ______________ |
| ________ | ______________ |
| ________ | ______________ |
| ________ | ______________ |
| ________ | ______________ |
手順14 自分のゲームに使えそうなものを選ぶ
調べた中から、自分がゲーム制作で使ってみたいフィードバックを選びます。
記入欄
自分のゲームに使ってみたいフィードバックは、
__________________________________
である。
それを使いたい理由は、
__________________________________
である。
そのフィードバックを入れることで、プレイヤーに
__________________________________
を伝えたい。
実装するとしたら、必要そうな処理は、
__________________________________
である。
手順15 最終まとめを書く
ここでは、調べた内容をもとに、
ゲームプログラマとして自分の制作にどう活かせるかをまとめます。
単に
「音が鳴っていた」
「エフェクトが出ていた」
で終わらせないこと。
次の3つを意識して書きます。
- 何をプレイヤーに伝えるためのフィードバックなのか
- どの感覚に向けたフィードバックなのか
- 自分が作るなら、どんな処理として実装するのか
15-1 入力とフィードバックの関係のまとめ
ゲームでは、プレイヤーが
「__________________」
という入力をすると、ゲーム内で
「__________________」
という処理が行われる。
その結果、画面・音・振動などを通して、プレイヤーに
「__________________」
というフィードバックが返る。
このフィードバックがあることで、プレイヤーは
「__________________」
を理解しやすくなる。
15-2 視覚フィードバックのまとめ
今回調べたゲームでは、視覚フィードバックとして
「__________________」
が使われていた。
これは、プレイヤーに
「__________________」
を伝えるための表現である。
たとえば、
「__________________」
という場面で、
「__________________」
が表示されることで、プレイヤーは
「__________________」
と判断できる。
プログラムで実装する場合は、
「__________________」
という処理が必要になると考えられる。
15-3 聴覚フィードバックのまとめ
今回調べたゲームでは、聴覚フィードバックとして
「__________________」
が使われていた。
これは、プレイヤーに
「__________________」
を伝えるための音である。
音のフィードバックは、画面を細かく見ていなくても、
成功・失敗・危険・報酬などに気づきやすくする役割がある。
たとえば、
「__________________」
という場面で、
「__________________」
という音が鳴ることで、プレイヤーは
「__________________」
と理解できる。
プログラムで実装する場合は、
「__________________」
という処理が必要になると考えられる。
15-4 触覚フィードバックのまとめ
今回調べたゲームでは、触覚フィードバックとして
「__________________」
が使われていた。
これは、プレイヤーに
「__________________」
を手や体の感覚で伝えるための表現である。
触覚フィードバックは、特に
「衝撃」
「ダメージ」
「攻撃の手応え」
「危険の接近」
を伝えるときに効果的である。
たとえば、
「__________________」
という場面で、
「__________________」
という振動があることで、プレイヤーは
「__________________」
と感じる。
プログラムで実装する場合は、
「__________________」
という処理が必要になると考えられる。
15-5 複数の感覚を組み合わせたフィードバックのまとめ
ゲームでは、1つの出来事に対して、
視覚・聴覚・触覚のフィードバックを組み合わせることが多い。
今回調べた中で、特に分かりやすかった組み合わせは、
「__________________」
という場面である。
その場面では、次のフィードバックが使われていた。
| 感覚 | フィードバック |
|---|---|
| 視覚 | ______________ |
| 聴覚 | ______________ |
| 触覚 | ______________ |
この組み合わせによって、プレイヤーは
「__________________」
を強く理解できる。
プログラマとして実装する場合は、
1つのイベントが発生したときに、複数の処理を同時に行う必要がある。
例
攻撃が当たった
↓
敵にダメージを与える
↓
ヒットエフェクトを出す
↓
ヒット音を鳴らす
↓
敵を点滅させる
↓
短い振動を入れる
↓
スコアやHPを更新する
このように、フィードバックは単独ではなく、
「ゲーム内の出来事に対して、必要な反応をまとめて返す処理」
として考える必要がある。
15-6 フィードバックが弱い場合の問題点
フィードバックが弱いと、プレイヤーは
「__________________」
と感じやすくなる。
たとえば、
「__________________」
という場面でフィードバックが弱いと、プレイヤーは
「__________________」
を理解しにくくなる。
その結果、ゲームは
「__________________」
と感じられる可能性がある。
例
| フィードバックが弱い場面 | プレイヤーが感じる問題 |
|---|---|
| 攻撃が当たっても音やエフェクトがない | 当たったか分からない |
| ダメージを受けても画面変化がない | いつ失敗したか分からない |
| 危険な攻撃の予告がない | 理不尽にやられたと感じる |
| ボタンを押しても反応が薄い | 操作している感じがしない |
| UIの変化が小さい | 状況を判断しにくい |
15-7 プログラマとして意識したいこと
今回の研究を通して、ゲームプログラマとして特に意識したいことは、
「__________________」
である。
なぜなら、プレイヤーはゲーム内の処理を直接見ることはできず、
画面・音・振動・UIなどのフィードバックによって、
ゲーム内で何が起きたかを理解しているからである。
そのため、自分がゲームを作るときは、
次のような流れで考えたい。
プレイヤーが何を入力したか
↓
ゲーム内で何を処理するか
↓
プレイヤーに何を伝える必要があるか
↓
視覚・聴覚・触覚のどれで伝えるか
↓
どのタイミングで反応を返すか
↓
プログラムとしてどう実装するか
15-8 自分のゲーム制作に活かすこと
自分がゲームを作るとき、特に使ってみたいフィードバックは、
「__________________」
である。
それを使いたい理由は、
__________________________________
である。
そのフィードバックを入れることで、プレイヤーに
「__________________」
を伝えたい。
実装するとしたら、必要な処理は次のようになる。
| 必要な処理 | 内容 |
|---|---|
| 入力判定 | ______________ |
| ゲーム内処理 | ______________ |
| 視覚フィードバック | ______________ |
| 聴覚フィードバック | ______________ |
| 触覚フィードバック | ______________ |
| UI更新 | ______________ |
| タイミング制御 | ______________ |
実装チェックリスト
自分でゲームを作るときは、次の項目を確認します。
| チェック項目 | はい / いいえ |
|---|---|
| 入力したことがすぐ分かる反応がある | __ |
| 成功したことが分かるフィードバックがある | __ |
| 失敗したことが分かるフィードバックがある | __ |
| 危険が事前に分かるフィードバックがある | __ |
| 報酬を得たことが気持ちよく伝わる | __ |
| HPやスコアなどの状態変化が見やすい | __ |
| 音が情報として役立っている | __ |
| 必要に応じて振動などの触覚表現を使っている | __ |
| フィードバックが多すぎて見にくくなっていない | __ |
| プレイヤーに何を伝えるための表現か説明できる | __ |
研究全体のまとめ
今回、複数のゲームを調べたことで、
ゲームのフィードバックには
「__________________」
という共通点があると分かった。
特に、よく使われていたフィードバックは、
「__________________」
である。
それは、プレイヤーに
「__________________」
を伝えるために使われていた。
プログラマとしてゲームを作るときは、
入力を受け取って処理するだけでなく、
その結果をプレイヤーに分かりやすく返す必要がある。
そのために、
「__________________」
という場面では、
「__________________」
という視覚フィードバック、
「__________________」
という聴覚フィードバック、
「__________________」
という触覚フィードバックを組み合わせたい。
これにより、プレイヤーは
「__________________」
を理解しやすくなり、
ゲームの操作感や手応えが良くなると考える。
100〜150字まとめ
記入欄
今回の研究では、ゲームの入力に対して、
__________________________________
というフィードバックが返ることで、プレイヤーが
__________________________________
を理解しやすくなると分かった。
自分でゲームを作るときは、
__________________________________
を意識して実装したい。
提出するもの
次の内容を記入して提出すること。
- 調べたゲーム一覧
- 入力の種類の表
- 入力から出力までの流れ
- 視覚フィードバックの表
- 聴覚フィードバックの表
- 触覚フィードバックの表
- 複数の感覚を組み合わせたフィードバックの表
- UIによるフィードバックの表
- フィードバックが弱いとどうなるか
- 入力とフィードバックの流れ図
- プログラマとして必要な処理
- 自分のゲームに使いたいフィードバック
- 実装チェックリスト
- 研究全体のまとめ
- 100〜150字まとめ
書くときの注意
1. いろいろなジャンルを見る
同じジャンルだけでなく、できれば違うジャンルのゲームを見てください。
ジャンルが違うと、入力やフィードバックの使い方も違います。
2. 「入力」と「反応」をセットで見る
入力だけを書いても、ゲームの研究にはなりません。
悪い例
- Aボタンでジャンプする
良い例
- Aボタンを押すとキャラクターがジャンプし、ジャンプ音が鳴り、ジャンプアニメーションに変わるため、プレイヤーはジャンプできたと分かる
3. 「何を伝えているのか」を考える
フィードバックを見るときは、ただ
「音が鳴る」
「光る」
で終わらせないでください。
そのフィードバックが何を伝えているのかを考えます。
例
- アイテム取得音は、報酬を得たことを伝えている
- 敵の予備動作は、危険が近いことを伝えている
- HPバーの減少は、現在の危険度を伝えている
- 画面揺れは、攻撃の強さや衝撃を伝えている
4. プログラマ視点で考える
フィードバックは、プログラムで作るものです。
たとえば、攻撃が当たったときには、次のような処理が必要です。
攻撃ボタン入力
↓
攻撃アニメーション開始
↓
攻撃判定を出す
↓
敵との当たり判定を調べる
↓
当たったらダメージを与える
↓
ヒット音を鳴らす
↓
ヒットエフェクトを出す
↓
敵を点滅させる
↓
必要なら振動させる
↓
スコアやHPを更新する
このように、
「プレイヤーにどう見せるか」
だけでなく、
「内部でどんな処理が必要か」
も考えましょう。
簡単な完成例
調べたゲーム
- スーパーマリオブラザーズ
- ストリートファイター
- マリオカート
入力と出力の例
| ゲーム名 | 入力 | ゲーム内の処理・結果 | プレイヤーに返る反応 |
|---|---|---|---|
| スーパーマリオブラザーズ | ジャンプボタン | マリオがジャンプする | ジャンプ音、ジャンプアニメーション |
| ストリートファイター | 攻撃ボタン | キャラクターが攻撃する | 攻撃音、ヒットエフェクト |
| マリオカート | アイテムボタン | アイテムを使う | 効果音、アイテム演出 |
感覚ごとのフィードバック例
| ゲーム名 | 場面 | 視覚 | 聴覚 | 触覚 |
|---|---|---|---|---|
| スーパーマリオブラザーズ | コイン取得 | コインが消える、スコアが増える | コイン取得音 | なし |
| ストリートファイター | 攻撃ヒット | ヒットエフェクト、のけぞり | ヒット音 | 振動対応版では振動 |
| マリオカート | ダート走行 | 車体の動きが変わる | 走行音が変わる | 振動 |
プログラマ視点のまとめ例
プレイヤーが入力した結果を分かりやすく伝えるためには、
キャラクターの動きだけでなく、音・エフェクト・UI・振動を組み合わせる必要がある。
たとえば、攻撃が当たった場面では、
ダメージ処理だけでなく、ヒット音、ヒットエフェクト、敵のリアクション、必要に応じた振動を同時に発生させることで、プレイヤーに手応えを伝えられる。
自分でゲームを作るときは、入力処理とゲーム内処理だけで終わらせず、
その結果をプレイヤーにどう返すかまで考えて実装したい。
状態遷移ふたたび
ゲームAI課題
センサー・視界・状態遷移表・状態遷移図を書いてみよう
1. 今日の課題
今回の課題では、モンスターAIの行動を
センサー
視界
状態遷移表
状態遷移図
として整理します。
プログラムは書きません。
しかし、最後に C++ の switch case や State パターンへ変換できるように、条件や状態をきちんと整理します。
2. 今日のゴール
この課題のゴールは、次の4つです。
1. モンスターが何を調べているのかを考える
2. センサーの結果をフラグとして整理する
3. 状態遷移表を書ける
4. 状態遷移図を書ける
3. AIは何を見ているのか
ゲームAIは、人間のように画面を見ているわけではありません。
AIは、ゲーム内の情報を数値や条件として調べています。
例えば、
プレイヤーが見えているか
プレイヤーが攻撃範囲にいるか
探索時間が終わったか
などです。
このような情報を調べる仕組みを、ここでは センサー と呼びます。
4. センサーとは何か
センサーとは、
AIが状況を判断するために使う情報を調べる処理
です。
例えば、モンスターAIでは次のようなセンサーが考えられます。
| センサー | 調べること |
|---|---|
| 視界センサー | プレイヤーが見えているか |
| 距離センサー | プレイヤーが近いか |
| 攻撃範囲センサー | 攻撃できる距離にいるか |
| 探索時間センサー | 探索時間が終わったか |
| 壁センサー | プレイヤーとの間に壁があるか |
5. 視界センサーの考え方
視界センサーは、
プレイヤーが見えているか?
を調べるセンサーです。
ただし、ゲームAIは本当に映像を見ているわけではありません。
多くの場合、次のような条件で「見えている」と判断します。
1. プレイヤーが一定距離以内にいる
2. プレイヤーがモンスターの前方にいる
3. プレイヤーとの間に壁がない
6. 視界センサーの例
距離が近い
かつ
前方にいる
かつ
壁で隠れていない
↓
プレイヤーを見つけた
C++で使うなら、次のような関数にできます。
bool CheckCanSeePlayer();
この関数が true なら、
isFindPlayer = true;
と考えます。
7. センサーからフラグを作る
AIの判断では、センサーの結果をフラグとして使うと分かりやすくなります。
bool isFindPlayer; // プレイヤーを見つけた
bool isAttackRange; // 攻撃範囲に入った
bool isLostPlayer; // プレイヤーを見失った
bool isSearchTimeOver; // 探索時間が終わった
つまり、
センサーで調べる
↓
フラグにする
↓
状態遷移の条件に使う
という流れです。
8. 状態とは何か
状態とは、
今、そのキャラクターが何をしているか
を表すものです。
例えば、モンスターには次のような状態があります。
| 状態 | 意味 |
|---|---|
| Patrol | 巡回している |
| Chase | プレイヤーを追いかけている |
| Attack | 攻撃している |
| Search | プレイヤーを探している |
9. 状態遷移とは何か
状態遷移とは、
ある状態から、別の状態に切り替わること
です。
例えば、
Patrol
↓ プレイヤーを見つけた
Chase
これは、
巡回中に視界センサーがプレイヤーを発見したので、
追跡状態に変わった
という意味です。
10. 今回作るモンスターAI
今回のモンスターは、次のように動きます。
普段は決められた道を巡回している。
視界センサーでプレイヤーを見つけたら追いかける。
距離センサーで攻撃範囲に入ったと分かったら攻撃する。
攻撃中に視界センサーでプレイヤーを見失ったら、周囲を探す。
探索時間センサーで時間切れになったら、巡回に戻る。
11. 使用する状態
今回使う状態は、次の4つです。
| 状態名 | 内容 |
|---|---|
| Patrol | 決められた道を巡回する |
| Chase | プレイヤーを追いかける |
| Attack | プレイヤーを攻撃する |
| Search | プレイヤーを探す |
12. 使用するセンサー
今回使うセンサーは、次の3つです。
| センサー名 | 内容 | C++向けの関数名 |
|---|---|---|
| 視界センサー | プレイヤーが見えているか | CheckCanSeePlayer() |
| 攻撃範囲センサー | 攻撃できる距離にいるか | CheckAttackRange() |
| 探索時間センサー | 探索時間が終わったか | CheckSearchTimeOver() |
13. 使用するフラグ
今回使うフラグは、次の4つです。
| フラグ名 | 意味 |
|---|---|
isFindPlayer |
プレイヤーを見つけた |
isAttackRange |
攻撃範囲に入った |
isLostPlayer |
プレイヤーを見失った |
isSearchTimeOver |
探索時間が終わった |
14. センサーとフラグの関係
センサーからフラグを作ると、次のようになります。
isFindPlayer = CheckCanSeePlayer();
isAttackRange = CheckAttackRange();
isSearchTimeOver = CheckSearchTimeOver();
isLostPlayer = !isFindPlayer;
!isFindPlayer は、
プレイヤーを見つけていない
という意味です。
つまり、プレイヤーが見えていなければ、見失ったと考えることができます。
15. 課題1:センサーの意味を説明する
次のセンサーについて、自分の言葉で説明しなさい。
| センサー | 自分の説明 |
|---|---|
| 視界センサー | |
| 攻撃範囲センサー | |
| 探索時間センサー |
16. 課題2:視界センサーの条件を考える
視界センサーで「プレイヤーが見えている」と判断するには、どのような条件が必要ですか。
下の表を完成させなさい。
| 条件 | 説明 |
|---|---|
| 距離 | |
| 向き | |
| 壁 |
17. 課題3:状態の意味を説明する
次の状態について、自分の言葉で説明しなさい。
| 状態 | 自分の説明 |
|---|---|
| Patrol | |
| Chase | |
| Attack | |
| Search |
18. 課題4:状態遷移を文章で整理する
次の状態遷移を、センサーやフラグの言葉を使って説明しなさい。
例
Patrol → Chase
説明:
巡回中に視界センサーでプレイヤーを見つけたら、追跡状態に変わる。
問題
| 遷移 | 説明 |
|---|---|
| Patrol → Chase | |
| Chase → Attack | |
| Attack → Search | |
| Search → Patrol |
19. 課題5:状態遷移表を完成させる
次の表を完成させなさい。
| 現在の状態 | センサー・条件 | フラグ | 次の状態 |
|---|---|---|---|
| Patrol | 視界センサーでプレイヤーを見つけた | ||
| Chase | 攻撃範囲センサーで攻撃範囲に入った | ||
| Attack | 視界センサーでプレイヤーを見失った | ||
| Search | 探索時間センサーで時間切れになった |
20. 課題6:C++向けの状態遷移表を書く
課題5を使って、C++で使いやすい形の状態遷移表を書きなさい。
| 現在の状態 | 条件 | 次の状態 |
|---|---|---|
| EnemyState::Patrol | EnemyState::Chase | |
| EnemyState::Chase | EnemyState::Attack | |
| EnemyState::Attack | EnemyState::Search | |
| EnemyState::Search | EnemyState::Patrol |
21. 課題7:状態遷移図を描く
次の4つの状態を使って、状態遷移図を描きなさい。
Patrol
Chase
Attack
Search
矢印には、状態が変わる条件を書きなさい。
図の例
Patrol -- isFindPlayer --> Chase
作図スペース
22. 課題8:C++のswitch caseに変換する準備
次のひな形の空欄を埋めなさい。
switch (state)
{
case EnemyState::Patrol:
if (__________)
{
state = EnemyState::__________;
}
break;
case EnemyState::Chase:
if (__________)
{
state = EnemyState::__________;
}
break;
case EnemyState::Attack:
if (__________)
{
state = EnemyState::__________;
}
break;
case EnemyState::Search:
if (__________)
{
state = EnemyState::__________;
}
break;
}
23. 課題9:AIとして自然に見えるか考える
今回のAIは、次の流れでした。
Patrol
↓ 視界センサーでプレイヤーを見つけた
Chase
↓ 攻撃範囲センサーで攻撃範囲に入った
Attack
↓ 視界センサーでプレイヤーを見失った
Search
↓ 探索時間センサーで時間切れ
Patrol
このAIには、まだ不自然なところがあります。
例えば、
Chase中にプレイヤーを見失ったらどうなるのか?
Attack中に攻撃範囲から出たらどうなるのか?
Search中にプレイヤーを再発見したらどうなるのか?
などです。
このAIに追加した方がよい状態遷移を2つ考えなさい。
| 追加したい遷移 | 使うセンサー・条件 | 理由 |
|---|---|---|
24. 45分の進め方
| 時間 | 内容 |
|---|---|
| 0〜5分 | センサー・視界・状態遷移の説明を読む |
| 5〜10分 | 今回のモンスターAIの仕様を確認する |
| 10〜17分 | 課題1・2:センサーと視界条件を整理する |
| 17〜24分 | 課題3・4:状態と遷移を文章で説明する |
| 24〜32分 | 課題5・6:状態遷移表を作る |
| 32〜40分 | 課題7:状態遷移図を描く |
| 40〜43分 | 課題8:switch caseの形にする |
| 43〜45分 | 課題9:改善案を考える |
25. 提出物
次の5つを提出しなさい。
1. センサーの説明
2. 視界センサーの条件
3. 状態遷移表
4. 状態遷移図
5. switch case のひな形
26. まとめ
今回の課題では、プログラムを書く前にAIの行動を整理しました。
重要なのは、次の流れです。
センサーで状況を調べる
↓
フラグにする
↓
状態を決める
↓
状態が変わる条件を決める
↓
状態遷移表を書く
↓
状態遷移図を書く
↓
C++のswitch caseに変換する
AIの行動は、いきなりコードを書くよりも、先に表や図で整理すると分かりやすくなります。