Yz個人メモ

個人的なメモ

アルゴリズムの授業

1. 線形探索 (Linear Search)

1.1 概要

配列(またはリスト)の先頭から順番に目標値と比較し、一致したらその位置を返す。見つからなければ最後まで調べ終えて「未発見」を返す。

1.2 計算量

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 計算量

2.3 特徴・利点・欠点

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 計算量

3.3 特徴・利点・欠点

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; // 最後まで見つからなかった
}

● 説明ポイント


🌓 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; // 見つからない
}

● 説明ポイント


🔳 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 衝突で上書きされた
    }
}

● 説明ポイント


✨ 比較まとめ(覚えやすく)

種類

ソート必要

速さ(目安)

簡単さ

特徴

線形探索

不要

遅い O(n)

◎ 簡単

一番素直な方法

二分探索

必要

速い O(log n)

○ 普通

中央から半分に絞っていく

ハッシュ探索

不要

速い O(1)

△ やや難

直接位置を計算して探す



C++の授業

C++講義計画(C言語からの移行・ゲームオブジェクト構築まで)

🎯 目標

C言語を半年学んだ学生が、C++の基礎文法からオブジェクト指向(継承・ポリモーフィズム)を学び、最終的に GameObject クラスを継承してプレイヤーや敵を実装できるようにする。


🧭 全体構成(全15回想定)

テーマ技術目標演習例

1

C++導入・Cとの違い

iostream / 名前空間 / new / delete

Cのprintf版→C++出力へ書き換え

2

関数と参照・オーバーロード

参照渡し・デフォルト引数・関数多重定義

距離計算関数のオーバーロード

3

クラス・コンストラクタ入門

class / public / private / this

Vector2構造体→Vector2クラス化

4

クラスの設計とヘッダ分離

.h / .cpp 分離と include 管理

Vector2, Rect クラス実装

5

動的メモリとコンテナ基礎

new/delete, std::vector

std::vectorで敵リストを作る

6

コンストラクタ/デストラクタ/コピー

RAIIの考え方

ファイル読み込み管理クラス

7

継承入門

Base / Derived クラス / protected

EnemySlime, Bat など作る

8

仮想関数とポリモーフィズム

virtual, override, GameObject*管理

GameObject 基底で Draw/Updateを仮想化

9

純粋仮想関数・抽象クラス

=0 の概念、基底クラス設計

GameObject を抽象化して管理リストに追加

10

スマートポインタ・メモリ安全化

std::unique_ptr, std::shared_ptr

std::vector<unique_ptr<GameObject>>

11

継承+多態性の活用

virtual呼び出しと override確認

Enemy / Player / Item を一括更新

12

クラス間関係・静的メンバ

static, const, クラス内定数

ObjectManager 実装

13

名前空間・クラス分割整理

namespace, include guard

game/, engine/ に分離

14

総合演習:ミニゲーム制作

ゲームループ構築

コンソール or DxLibで動作確認

15

発表・振り返り

設計比較・自己評価

ソース+UML提出・口頭説明


🧩 ステップ詳細

STEP 1〜4:C++基礎をCとの違いで理解

STEP 5〜6:クラスとメモリ管理

STEP 7〜9:継承とポリモーフィズム

for (auto obj : objects) obj->Update();

STEP 10〜11:メモリ安全化

STEP 12〜15:まとめとゲーム制作


🎮 最終課題例

タイプ

内容

コンソールRPG

Character 継承で Player / Enemy を共通管理

2Dアクション (DxLib)

GameObject継承で Ball / Block / Paddle 管理

スネーク風ゲーム

GameObject多態性で SnakeHead / Fruit 管理


📘 授業設計のポイント


💡 まとめ

15回で「C構文→クラス→継承→仮想関数→安全なゲーム構造」まで自然に到達できる構成。 最終的には、ポリモーフィズムを使ってGameObjectを扱う“C++らしいゲーム設計”を完成させる。

1回目 導入回+文字列

第1回授業:C++導入とCとの違い(入出力・文字列・書式制御)

🎯 目標

C言語の知識をもとに、C++の基本文法と文字列処理を理解する。


🧩 授業構成(90分想定)

時間

内容

ポイント

0〜10分

CとC++の関係

「C++はCの拡張」だが、文字列処理や入出力が高機能であることを紹介。

10〜30分

入出力の基本

#include <iostream>

std::cout

/

std::cin

の使い方。

using namespace std;

の意味。

30〜55分

文字列の扱い

char[]

std::string

の違いを説明。代入・連結・比較などを実演。

55〜80分

書式制御

std::setw

std::setfill

std::fixed

std::setprecision

など

<iomanip>

の使用。表形式出力の練習。

80〜90分

まとめ・課題説明

文字列と入出力のまとめ。次回予告:「関数と参照」へ。


🧠 授業のねらい


📘 板書・スライド例

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 a = "Hello";
string b = "World";
string c = a + ", " + b;
if (a == "Hello") cout << c << endl;

💡 比較:

操作

C (char[])

C++ (std::string)

代入

strcpy(s, "abc")

s = "abc";

結合

strcat(a, b)

a += b;

比較

strcmp(a, b) == 0

a == b

長さ

strlen(a)

a.size()


✏️ 書式制御の例

#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;
}

💬 ポイント


🧩 演習課題

  1. 名前と年齢を入力し、整形して出力するプログラムを作成。
    Name: _____
    Age : __ years old
  2. std::stringを使って2つの単語を入力し、結合して表示する。
  3. <iomanip>を使って3つの数値を整列表示する。

💬 授業後課題(宿題)

  1. std::stringchar[]の違いを、3つの観点(代入・結合・安全性)で説明せよ。
  2. 書式制御を使って表のようなスコア表示を作るプログラムを書け。

🔎 次回予告

第2回:関数と参照・オーバーロード
C++独自の関数設計(参照渡し・デフォルト引数)を学び、関数の柔軟性を理解する。


補足資料

第1回授業補足編:書式制御とfmtライブラリ入門

🎯 目的

C++における出力整形の幅を広げ、より実践的なフォーマット出力を理解する。


🧩 <iomanip>による標準フォーマットまとめ

操作

操作関数

効果

使用例

桁幅指定

setw(n)

指定幅に右寄せ

cout << setw(5) << 42;

42

左寄せ

left

出力を左寄せ

cout << left << setw(5) << 42;

42

埋め文字

setfill(c)

空白を指定文字で埋める

cout << setfill('-') << setw(5) << 42;

---42

進数指定

dec

/

hex

/

oct

10進・16進・8進表示

cout << hex << 255;

ff

小数点桁数

setprecision(n)

小数点以下の桁数指定

cout << fixed << setprecision(2) << 3.14159;

3.14

浮動小数点表記

fixed

/

scientific

固定小数点/指数表記

cout << scientific << 0.00123;

1.23e-03

💡 ポイント


🧠 応用例:表形式出力

#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

{: <10}

→ 左寄せ

:>10

右寄せ・幅10

{: >10}

→ 右寄せ

:.2f

小数点以下2桁

{:.2f}

3.14

:06d

6桁ゼロ埋め

{:06d}

000042

{}

自動推論

デフォルト出力

💬 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

書式構文

%d %f %s

関数チェーン形式

{}

ベースのテンプレート式

型安全性

❌ 弱い

✅ 強い

✅ 強い

可読性

柔軟性

◎(動的組み立て可)


💡 まとめ


🔎 次にやるとよい演習

  1. 数値を右寄せ・左寄せしてスコア表を整形。
  2. fmt::print を使ってデバッグメッセージを出すツール関数を作る。
  3. std::format でログ出力を作り、cout版と比較する。

2回目 関数と参照・アインズ・ウール・ゴウン

🎯 第2回 関数と参照・オーバーロード

🏁 授業目標


🧠 関数の基本

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;
}

💬 ポイント


🧩 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; }

🔎 注意


💡 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 を整数/実数でオーバーロード


🏆 まとめ


補足資料(1.5回目) C言語の関数

🧩 補足資料:C言語の関数の基本

🧠 関数とは?

C言語の関数は、処理をひとまとめにして何度も呼び出せる仕組みです。
プログラムの再利用性可読性を高めるために使います。

#include <stdio.h>

int main() {
    hello(); // 関数呼び出し
    return 0;
}

// 関数定義
void hello() {
    printf("Hello, world!\\n");
}

🔹 宣言と定義の違い

名称

役割

書く場所

関数宣言(プロトタイプ宣言)

関数があることをコンパイラに知らせる

mainより上

int add(int a, int b);

関数定義

実際の処理内容を書く

mainより下でもOK

int add(int a, int b) { return a + b; }

💡 ポイント


🔸 引数と仮引数の違い

種類

意味

仮引数(parameter)

関数の受け取り側の変数

int add(int a, int b)

の a,b

実引数(argument)

関数を呼び出すときに渡す値

add(3, 5)

の 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
}

💬 ポイント


⚙️ 戻り値・void・手続きと関数

C言語の関数には、値を返す関数と**値を返さない手続き(void関数)**の2種類があります。

// 値を返す関数
int add(int a, int b) {
    return a + b;
}

// 値を返さない(void関数)
void printHello() {
    printf("Hello!\\n");
}

💡 return文


🧾 C言語の関数まとめ

分類

説明

宣言(プロトタイプ)

関数の存在を知らせる

int add(int a, int b);

定義

実際の処理を書く

{ return a + b; }

実引数/仮引数

呼び出す値と受け取る変数

add(3, 5)

int add(int a, int b)

値渡し

値のコピーを渡す

元の変数は変わらない

ポインタ渡し

アドレスを渡す

元の変数が変わる

戻り値

処理結果を返す

return

を使う

void関数

戻り値なしの手続き

画面出力など


🧠 まとめ

練習問題!

🧮 第1問:平均点と評価を返す関数

3科目の点数を入力し、平均点を返す関数と、
その平均に応じて評価(A〜D)を返す関数を作れ。

float getAverage(int a, int b, int c);
char getRank(float avg);

📘 仕様

💡
入力: 80 70 90
出力:

平均点 = 80.0
評価 = A

🔄 第2問:配列の最大値と最小値を求める関数

配列とその要素数を受け取り、最大値と最小値をポインタ経由で返す関数を作れ。

void getMinMax(int arr[], int n, int* min, int* max);

📘 仕様

💡
入力: {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);

📘 仕様

💡
入力: "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);

📘 仕様

💡
入力: {70, 85, 60, 90, 75}
出力:

平均点: 76.0
最高点: 90
最低点: 60

DirectXTK関連(Commonstateってなんぞ)

✅ 主な機能

CommonStates は以下のような「よくある組み合わせの状態オブジェクト」を提供します。 GitHub+1


🧮 使い方(例)

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


🔍 なぜ使うといいか


⚠️ 注意すべきこと

よく使う設定集

statesstd::make_unique<CommonStates>(device) 済み前提です。


1) 不透明3D(デフォルト)

ctx->OMSetBlendState(states->Opaque(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthDefault(), 0);
ctx->RSSetState(states->CullCounterClockwise()); // 右手系ならCCW, 左手系なら適宜

2) 透明(アルファブレンド)

ctx->OMSetBlendState(states->AlphaBlend(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthRead(), 0); // 深度テストON/書き込みOFF
ctx->RSSetState(states->CullNone()); // 両面必要なら

3) 加算(パーティクル/発光)

ctx->OMSetBlendState(states->Additive(), nullptr, 0xFFFFFFFF);
ctx->OMSetDepthStencilState(states->DepthRead(), 0);
ctx->RSSetState(states->CullNone());

4) 2D/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);

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に

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) サンプラの目安(用途別)


10) Reverse-Z を使う場合(上級)

ctx->OMSetDepthStencilState(states->DepthReverseZ(), 0);        // 不透明
// or 透過時
ctx->OMSetDepthStencilState(states->DepthReadReverseZ(), 0);

使い分けのコツ(超要約)

DirectXTK関連(頂点情報)

🎯 DirectXTK の主要な頂点構造体一覧(VertexTypes.h)

構造体名

含まれる要素

セマンティクス

主な用途

備考

VertexPosition

位置

POSITION

最小構成(点・線など)

シェーダで

SV_Position

だけ使う場合

VertexPositionColor

位置+頂点色

POSITION

,

COLOR

頂点ごとに色を持つラインやデバッグ描画

2D線やワイヤーフレーム表示向き

VertexPositionTexture

位置+UV

POSITION

,

TEXCOORD

テクスチャ貼り付け(Quad, Billboard, Sprite3Dなど)

✅ 最も汎用的で、現在あなたが使っている構成

VertexPositionNormal

位置+法線

POSITION

,

NORMAL

照明(ライティング)を行う3Dメッシュ

影・陰影処理に使う基本形

VertexPositionNormalColor

位置+法線+頂点色

POSITION

,

NORMAL

,

COLOR

頂点色と照明を併用する場合

ゲームモデルなどでよく使われる

VertexPositionNormalTexture

位置+法線+UV

POSITION

,

NORMAL

,

TEXCOORD

ライティング+テクスチャ

✅ モデル描画で最も一般的

VertexPositionColorTexture

位置+色+UV

POSITION

,

COLOR

,

TEXCOORD

色つきUI・デカールなど

色とテクスチャを混ぜたいとき

VertexPositionNormalTangentTexture

位置+法線+接線+UV

POSITION

,

NORMAL

,

TANGENT

,

TEXCOORD

法線マップ(ノーマルマップ)を使う高品質シェーディング

⚙️ 高級ライティング用

VertexPositionDualTexture

位置+UV×2

POSITION

,

TEXCOORD0

,

TEXCOORD1

マルチテクスチャ合成

例:ライトマップ+ディフューズ


🧩 それぞれの使い分け方(実例つき)

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 の SkinnedEffectNormalMapEffect と組み合わせると最短で使える。


4️⃣ VertexPositionColor

VertexPositionColor v[] = {
    { {0,0,0}, Colors::Red },
    { {1,0,0}, Colors::Green },
};

デバッグ線・グリッド表示・ワイヤーフレーム描画など。
BasicEffectSetVertexColorEnabled(true) をオンにすれば、色付き描画可能。


⚙️ 裏側の仕組み(共通点)

DirectXTK の各頂点構造体は以下のような定義を持っています:

struct VertexPositionTexture
{
    DirectX::XMFLOAT3 position;
    DirectX::XMFLOAT2 textureCoordinate;

    static const D3D11_INPUT_ELEMENT_DESC InputElements[2];
    static const UINT InputElementCount = 2;
};

InputElements の定義はライブラリ内にあり、
BasicEffectCreateInputLayout() にそのまま渡せます:

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のときまた頭おかしくなる用)

結論(このリポの現状)

  • 座標系:FBX読込時に 右手系・Y up へ正規化してからロードしています(opts.target_axes = ufbx_axes_right_handed_y_up)。描画側も RH の LookAt / PerspectiveFov を使っているため、手系・Up軸は一致しています。

    UfbxAnimSkeleton

    D3DApp

  • 行列の積み上げEvaluateAt()local を親の world に掛ける(W = L * Pw) という標準的な階層合成を行い、かつ Skin = W * InvBind でスキン行列を作っています。FBX特有の pre/post-rotation は ufbx_evaluate_transform() が面倒を見てくれるので、アニメの動きはFBXと同一になります。

    UfbxAnimSkeleton

  • 行列レイアウトの差ufbx_matrix (4x3)DirectX の XMFLOAT4X4(行メジャ/平行移動は _41/_42/_43) に詰め替えており、転置+配置は関数で一貫させています。可視化はこの _41/_42/_43 を使って正しくワールド位置を取っています。

    UfbxAnimSkeleton

    BoneVisualizer

  • カメラ:ボーンのワールド行列群から 中心と半径を推定 → その球が画面に“ちょうど入る”距離に RH ビュー&プロジェクションを自動計算。画角・距離が素直な値なので、モデリングビュー相当の見え方になります(もちろんツールのプリセットFOVと同じにすればより一致)。

    WinMain

    WinMain

  • タイトル/FPS:Unicode タイトル更新や 60fps リミッタは描画の見え方に影響しませんが、WAPI での文字化け対策含め堅牢です。

    D3DApp

同じ見え方にするためのチェックリスト

  1. 手系/Up軸の一致

    • すでに FBX ロードで RH, Y-up へ正規化済み。描画も RH を使用。→ OK。
      opts.target_axes = ufbx_axes_right_handed_y_up / XMMatrixLookAtRH & XMMatrixPerspectiveFovRH

      UfbxAnimSkeleton

      D3DApp

  2. 行列の詰め替え規約

    • ToDX_4x4_FromUfbxMatrix4x3()回転・並進を DirectX のフィールド配置に揃える。可視化側は _41/_42/_43 から位置を取得。→ OK。

      UfbxAnimSkeleton

      BoneVisualizer

  3. アニメ評価の一貫性

    • ufbx_evaluate_transform() を local として取り、親子で掛け上げて world を得る。FBXの pre/post 回りは ufbx が吸収。→ OK。

      UfbxAnimSkeleton

  4. カメラ/FOV

    • ツール側の FOV と合わせると「同じ見え」。コードは 対象を包む球で距離計算して RH の VP を生成。ツール側の FOV が 45°なら XM_PIDIV4 と一致させる。

      WinMain

      WinMain

  5. クリッピング/遠近

    • zn は距離に応じて 0.5 以上、zf は余裕を持たせて設定。D3DApp のリサイズ時も far=10000。極端に大きい/小さいモデルでも破綻しにくい。

      WinMain

      D3DApp

  6. 単位(スケール)

    • いまのコードは 単位スケールを特別扱いしていません(メートル/センチの換算はしない)。ただし カメラ自動フィットがあるので見た目は実用的に問題なし。モデル同士の相対スケール厳密一致が必要なら、UFbxの scale_factor 等を使うスケーリング導入を検討(今は未使用)。

      ufbx

  7. Z反転の重複適用に注意

    • ルートに FlipZ を掛けるコードはコメントアウトのまま。target_axes で RH 化しているので 追加の反転は不要。もしモデルが「鏡像」に見えたら、反転の二重適用がないかをまず疑う。

      UfbxAnimSkeleton

  8. 可視化の基準

    • ボーンラインは 親→子の _41/_42/_43 の位置を結ぶだけなので、座標が正しければ「骨の形」は一致します。

      BoneVisualizer

ありがちな“ズレ”と対処

  • 左右反転/鏡像に見える
    → どこかで 手系変換+Z反転を二重にしていないかチェック。今のコードでは target_axes が RH 化を担うので FlipZ を ON にしないのが正解。

    UfbxAnimSkeleton

  • 大きすぎ/小さすぎで消える
    FitCameraToBounds()半径から距離を幾何学的に決定しているため、初期表示は安定。遠近クリップは 0.1~10000 なので、通常用途は問題なし。

    WinMain

    D3DApp

  • モデリングツールの FOV と違う
    → ツール側の FOV に合わせて XM_PIDIV4 の値を変更すれば一致が近づきます。

    D3DApp


まとめ

  • FBX読込時に RH/Y-up へ正規化し、DirectX 側も RH の VP行列の詰め替えも統一アニメ評価は ufbx に準拠しているので、理屈上はモデリングソフトの見えと一致します。

  • 残る実務的な差異は FOV/単位スケール/反転の二重適用。この3点を上のチェックリスト通りに押さえれば、同じ見え方に落ち着きます。


クソIT系

💻 「IT系」と言われる仕事の実態

日本の専門学校や一般企業が「IT系」と呼ぶ職種は、大きく次のように分かれます:CS=Computer Science

分類 仕事内容 コンピュータサイエンスとの関係
① システムエンジニア(SIer系) 顧客の業務システムを設計・構築・納入する。要件定義、設計書作成、発注、ベンダー管理など。 理論よりも業務知識・調整力中心。CSとは距離がある。
② プログラマ(下請け開発) 上流設計に基づきコードを書く。 一部アルゴリズム理解が必要だが、工学というより技能職に近い。
③ インフラ・ネットワーク職 サーバやネットワークの構築・保守。 OS・通信・セキュリティの実務知識中心。理論的なネットワーク科学とは別物。
④ ITコンサル/導入支援 業務効率化やクラウド導入の提案、運用支援。 テクノロジー理解より業務コンサル色が強い
⑤ 企業の情報システム部門(情シス) 社内システムの選定・調達・保守。 CSとは無縁。購買・ヘルプデスク業務も多い。

🧠 一方の「コンピュータサイエンス」とは

CSは「情報処理の原理」そのものを研究・応用する分野で、主に以下のような内容を含みます。

これらは**「技術を創る側」**の知識体系であり、実務で言う「ITを使う/導入する側」とは明確に異なります。


🏢 なぜ専門学校の「IT系」と乖離しているのか

いくつか理由があります:

  1. 就職先がCSを必要としない業界が多い(SIer・受託開発・情シスなど)

  2. 教育課程が企業の即戦力(Word/Excel/資格)寄りに設計されている

  3. 「理論よりスキル(ツール操作)」の需要が高い

  4. 日本の「IT産業」が技術開発よりも“システム納入業”中心に発展した歴史

つまり、日本では「IT=情報システム導入業界」という構造的な事情があり、
「CS=情報科学的探求」とは異なる職能文化が形成されています。


🎯 まとめ

ドラクエ風「商人クラス」で学ぶ getter / setter / public / private

🎮 授業進行カンペ+教材一体版

ドラクエ風「商人クラス」で学ぶ getter / setter / public / private


🧭 授業概要


🧾 提示する資料(学生閲覧用 DokuWiki ページ)

BookStackからリンクまたはQRで提示。学生側にはDokuWiki版をコピー配布。

ページタイトル:

🧭 ドラクエ風「商人クラス」で学ぶ getter / setter / public / private

構成:

提示タイミング:


🧩 授業進行

時間 内容 教員アクション
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 で定義し、
それぞれに SetGet 関数を作成せよ。

Q2

SetPrice() で価格が 0未満なら09999より大きければ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;

✅ チェックリスト


🧩 発展予告(次回)


🎬 まとめコメント例(授業締め)

「今日の目的は“商人の金庫に鍵をかける”でした。
直接代入では壊れる。関数を通せば守れる。
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 → DrawIndexed

UfbxStaticModel のメッシュ関係で主役になるフィールドは(名前だけ):

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 = 150

ExpandAllNodes(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_ を引く

その後、各頂点について

  1. acc[v] を「weight の大きい順にソート」
  2. 上位 4 本に絞る
  3. 重みを正規化(合計 ≒ 1.0)
  4. 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 について

そして

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(): エフェクト&テクスチャ

ここでやっていること:

  1. CommonStatesBasicEffect を new
  2. BasicEffect にライティング設定を行う
  3. BasicEffect から VS バイトコードを取り出し、VertexPNT2 に合わせた InputLayout を作成
  4. FBX ファイルのディレクトリを求める
  5. 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);

まとめ(要点)

この構造が、今後 FbxMesh に切り出すときの「設計単位」になります。
ExpandAllNodes / CreateGpuBuffers / CreateEffectsAndTextures / Draw あたりがそのまま FbxMesh に移るイメージです。

ゲームの市場規模

🎮 日本のゲーム業界:最新市場規模と開発費まとめ(2024–2025)


1. 日本のゲーム市場規模

■ 国内ゲームコンテンツ市場規模(2024年)

要因として、物価上昇によるゲーム機・周辺機器・課金・サブスクの値上げが市場規模の拡大に直結している。

■ 国内ゲーム人口

PC+コンシューマーを併用するプレイヤーが増加し、複数デバイスを活用する層が2478万人に
プレイスタイルがマルチプラットフォーム化していることが市場の維持に寄与。


2. モバイルゲーム市場(2024年)

日本のゲーム市場の主軸であるモバイルゲームの最新傾向:

■ ダウンロード数

■ モバイルゲームの収益(IAP)

■ 収益ランキングトップタイトル

■ 収益が大きいIP


3. 世界市場の中での日本の位置づけ

日本は特に**モバイル3Dアニメゲームの世界最大市場(世界シェア49%)**であることが特徴。
[sensortower.com], [presswalker.jp]


4. ゲーム1本あたりの開発費の実態)

■ スマートフォンゲーム開発費(オンラインゲーム会社への調査)

■ ソーシャルゲームの開発費 相場(規模別)

(開発会社ヒアリングベース)

■ コンシューマー大作タイトル例

■ オンラインゲーム運営(1タイトルあたり)

JOGAレポートより:


5. その他、産業の雇用・構造

クリエイティブ産業としては突出して高い生産性があることが明らか。


6. 学科として強調できるポイント

✔ 日本のゲーム市場は「2.4兆円」の巨大産業で安定成長中

✔ 世界最大級のモバイルゲーム消費国(特に3Dアニメ系は世界シェア50%)

✔ 1タイトルあたりの開発費が急増(スマホ平均5億円 → AAAは300億円級)

✔ 高度な専門職が必要で平均年収も高く、人材育成の価値が高い

✔ 国内外パブリッシャーが継続投資を増やす構造で「人材不足」が深刻

(専門学校・大学のゲーム学科の存在意義が強い)

ダイクストラ法・BFS・A* を図で理解する課題(時間補習課題)

【学習目標】

この課題では次のことができるようになります。


🔶【課題図(グラフ構造)】


(1)        (1)        (1)
   [S] ------ [A] ------ [B] ------ [G]
     \
      \ (5)
       \
       [C]
         \
          \ (1)
           \
           [B](同じB)

◆ ① BFS(幅優先探索)とは?(穴埋め)

BFS は、
スタートから (    ) の順に探索していくアルゴリズムで、
道のコストが (    ) ときに最短経路を見つけることができる。

選択肢:


◆ ② ダイクストラ法とは?(穴埋め)

ダイクストラ法は、
スタートからの (    ) を更新しながら、
(    ) が最も小さいノードを順に確定する方法である。

選択肢:


◆ ③ A* アルゴリズムとは?(穴埋め)

A* は、ダイクストラ法に (    ) を追加した方法で、
より (    ) ゴールに到達することを目的としている。

選択肢:


◆ ④ 図を見て考える:BFS / ダイクストラ / A* の動き

Q1. BFS はどのルートを最初に見つける?

BFS が最初に見つけるルートは (         )
理由:__________________________________________________


Q2. ダイクストラ法が求める “最短経路” は?

ヒント:


Q3. A* の式を完成させよう

A* の評価値 f(n) は次の式で求める:

f(n) = g(n) + h(n)

選択肢:


◆ ⑤ BFSの理解を深める

追加Q1:BFS はどんな順番で探索を進める?

A と B どちらかを選んで、理由を書くこと。

回答:A / B(理由:___________________________________)


追加Q2:図を使って BFS の探索順を書こう

例:S → A → B → C → G のように記述。

探索順:_____________________________________


追加Q3:なぜ BFS は「コスト同じのときだけ」最短経路を求められるのか?

穴埋め:

BFS は、道のコストがすべて (    ) とき、
スタートからの (    ) の順に探索するため、
最短経路が求まる。

選択肢:


追加Q4:コストがバラバラのときに BFS が失敗する例

BFS はどちらを先に探索する?

回答:________________________________________
理由:______________________________________________________


追加Q5:ゲーム開発で BFS が役立つ場面は?

番号を選び、理由を書く。

  1. 敵から一定距離以内に逃げたいとき
  2. コストがバラバラのマップで探索したい
  3. 同じコストのタイルで広いマップを探索するとき

選んだ番号:___
理由:______________________________________


追加Q6:BFS の弱点

図のように“遠回りだが軽い道”があるとき、BFS はどんな失敗をするか?

回答:_______________________________________________


追加Q7:BFS を1行でまとめよ

「BFS は ________ を優先し、コストが ________ ときに最短経路を求められる」


追加Q8:BFS とダイクストラの共通点・相違点(穴埋め)


◆ ⑥ 3つのアルゴリズムの違いを1行ずつまとめよう


◆ ⑦ ゲーム開発での使い分け

  1. 草原でコストが全部同じ
    (        )
    理由:_________________________________________
  2. コストがバラバラの道(泥・坂・砂)
    (        )
    理由:_________________________________________
  3. ゴール(敵の位置)が分かっている
    (        )
    理由:_________________________________________

◆ ⑧ 最終まとめ(作文)

ダイクストラ法・BFS・A* の違いを、初心者にもわかるよう詳細に説明せよ。



GOMファイルフォーマットを作るよ

Game Oriented Model Format(GOMフォーマット)を作っていくよ。
シンプルで、アニメーションだけ含んだゲーム制作初心者向けフォーマットだよ。

GOMファイルフォーマットを作るよ

GOM(Game Oriented Model Format)草案

GOMフォーマット仕様書

Game Oriented Model Format (.gom) Version 1.0 (Complete Specification)


1. 目的

GOMは、FBX等のDCCデータをゲーム実行向けに最適化し、 オフライン変換して保存するためのフォーマットである。

設計目標


2. 空間設計ポリシー(不変条件)

Invariant A: 頂点空間

禁止事項


3. 行列規約(最終固定)

GOM v1では以下を固定する。


4. ファイル形式

GOMはテキスト形式(GOMT0001)とバイナリ形式(GOMB0001)を持つ。

マジックナンバー(先頭8バイト)


5. テキスト形式(GOMT0001)

UTF-8(BOMなし)、構造リテラル形式。

5.1 Skeleton

Skeleton = {
  boneCount = <int>,
  bones = [
    {
      parent = <int>,
      name   = "<string>",
      path   = "<string>",
      bindLocal = [[...],[...],[...],[...]]
    }
  ]
}

5.2 MeshPart

Meshes = [
  {
    name = "<string>",
    meshBindGeoToModel = [[...],[...],[...],[...]],
    vertices = [ { p,n,t,uv,bi,bw } ],
    indices = [ ... ],
    submeshes = [ { materialIndex, indexStart, indexCount } ],
    inverseBind = [ [[...]] ]
  }
]

ウェイト規約(必須)

5.3 Animation

Animations = [
  {
    name = "<string>",
    duration = <float>,
    rootBoneIndex = <int>,
    flags = ["HasRootMotion"],
    tracks = [ { bone, keys[] } ]
  }
]

補間:T/S=linear、R=slerp


6. MeshPart規約

Rule M1

Rule M2

以下の場合はMeshPartを分割する:


7. バイナリ形式(GOMB0001)

7.1 固定ヘッダ(512 bytes)

Little Endian固定。

7.1.1 固定値

7.1.2 列挙値(enum)定義

本仕様の enum はすべて uint32 とする。 未知の enum 値を受け取った場合、読み込みはエラーとする(v1)。

endianness(将来用)

v1ではファイルはLittle Endian固定だが、将来拡張のためヘッダに enum を持つ。

coordinateSystem

matrixConvention

行列の「格納順+演算規約」を表す。

定義:

7.1.3 validationFlags(bitfield)

validationFlagsuint32

予約:bit3以降は将来用。v1では0。

7.1.4 ChunkTableの指定


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

8.2 アライメント

8.3 チャンク基本構造

uint32 fourcc
uint32 size
uint32 version
uint32 flags
byte payload[size]
pad to 16

8.4 チャンクサイズ上限チェック(必須)

8.5 未知チャンク

8.6 ChunkTable(任意)

8.7 MSH0.flags(任意)


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. 変換ツール検証(必須)



11. 実装安全規約(v1推奨)

11.1 読み込み上限値(推奨)

実装は以下の上限を持つことを推奨する(超過時はエラー)。

※ 実装側で変更可能だが、無制限読み込みは禁止。


11.2 必須チャンク条件(バイナリ)

各 MSH0 は以下の子チャンクを必須とする:

GBTM は任意。

IB0.indexSizeBytes は 2 または 4 のみ許可。 それ以外は読み込みエラー。


11.3 アニメキー時刻規約


11.4 inverseBind の定義

inverseBind[i] は、bind姿勢における boneWorld[i] の逆行列に相当する。

スキニングパレットは以下で構成される:

palette[i] = boneWorld_current[i] * inverseBind[i]

これにより、geometry 空間内でスキニングが完結する。


End of GOM Specification v1.0

GOMファイルフォーマットを作るよ

GOM Format quick Reference

GOM + MTA フォーマット 簡易仕様書

Game Oriented Model Format + Motion TimeLine Animation

バージョン: GOM v1.0 / MTA v1.0
最終更新: 2026-03-18

📖 目次

  1. 概要
  2. GOM フォーマット
  3. MTA フォーマット
  4. 基本規約
  5. ファイル構造
  6. 主要チャンク
  7. ウェイト正規化
  8. 決定性保証
  9. 互換性
  10. 実装ガイド
  11. テキストフォーマット
  12. よくある質問(FAQ)
  13. まとめ

概要

GOM(Game Oriented Model)

目的: FBX等のDCCデータをゲーム実行向けに最適化し、オフライン変換して保存する

設計目標:

MTA(Motion TimeLine Animation / GOMTA)

目的: GOMのアニメーション部分のみを独立させた兄弟フォーマット

特徴:


GOM フォーマット

ファイル形式

形式 マジックナンバー 説明
テキスト GOMT0001 UTF-8、構造リテラル形式
バイナリ GOMB0001 Little Endian、16-byte アライン

含まれる要素

ファイルサイズ


MTA フォーマット

ファイル形式

形式 マジックナンバー 説明
バイナリ MTAB0001 Little Endian、16-byte アライン

注意: v1 ではテキスト形式(MTAT0001)は非サポート。詳細は テキストフォーマット セクション参照。

含まれる要素

除外される要素

スケルトン互換性チェック

実行時判定:

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: 頂点空間

禁止事項:

描画時の変換式:

// 正しい変換順序
vec4 skinnedPos = vertex * skinning;           // geometry 空間
vec4 worldPos = skinnedPos * GBTM * instanceWorld;  // world 空間
vec4 clipPos = worldPos * viewProj;            // clip 空間

この設計の利点:

  1. インスタンシング安全: 同一メッシュを異なる位置/回転で配置可能
  2. アニメーション独立: インスタンスごとに異なるアニメ時間を適用可能
  3. FBX差の吸収: ノード階層の違いを完全に隠蔽
  4. 事故防止: ワールド直スキニングによる破綻を防止

行列規約

項目
数値型 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(...)
| ... (複数クリップ可)    |
+-------------------------+

主な違い:

ヘッダ構造(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埋め

重要な違い:

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) {
  // 警告: 未知の検証フラグが立っている
}

重要な規約:


チャンク共通構造(16 bytes)

struct ChunkHeader {
  uint32_t fourcc;     // チャンク識別子(4文字)
  uint32_t size;       // ペイロードサイズ(バイト)
  uint32_t version;    // v1 では 1 固定
  uint32_t flags;      // v1 では 0 固定
};

fourcc 規約

フォーマット:

3文字 fourcc の扱い:

3文字の識別子(例: VB0, IB0)は、null 終端で 4 バイトに格納する:

'VB0\0'  // null終端で4バイト
'IB0\0'  // null終端で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 チャンク処理
}

注意事項:


主要チャンク

STR0(文字列テーブル)- 必須

構造:

stringCount: uint32
offsets: uint32[stringCount]
stringData: char[]  // UTF-8, null終端

規約:

MATL(Material Slots)- GOM 必須

構造:

materialSlotCount: uint32
slots: MaterialSlot[materialSlotCount]

MaterialSlot:

デフォルト白マテリアル:

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

規約:

MSH0(MeshPart Container)- GOM 必須

内部チャンク:

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[]

補間規約:



ウェイト正規化

規約

GOM v1 では、すべての頂点のボーンウェイトは以下の規約に従う:

  1. 最大4影響: 5つ以上の影響は上位4つのみ保持

  2. 合計1.0: ウェイト合計を 1.0 に正規化

  3. ソート規約:

    • 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]

検証

変換ツールは以下を保証しなければならない:



決定性保証

ソート規約

すべての順序は UTF-8 byte-wise 辞書順 で確定:

  1. bonePath: SKEL の boneIndex 順序
  2. meshNodePath: MSH0 の MeshPart 順序
  3. materialKey: MATL の materialIndex 順序
  4. clipName: MTA の CLP0 順序

比較方法

// UTF-8 byte-wise 比較(memcmp / strcmp)
// ロケール依存禁止
// 大文字小文字折りたたみ禁止

浮動小数点


互換性

GOM v1.0

サポート範囲:

制限:

MTA v1.0

サポート範囲:

制限:

実行時互換性チェック

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

検証規約

必須チェック:

  1. Bind Pose Consistency: inverseBind * bindLocal = Identity
  2. Weight Normalization: sum(boneWeight) = 1.0 ± 1e-4
  3. Bone Index Range: 0 <= boneIndex < boneCount
  4. Index Range: 0 <= index < vertexCount
  5. Submesh Coverage: 重複なし、隙間なし、3の倍数
  6. 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 テキスト形式)

概要:

基本構造

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 でサポートされる形式:

v2 以降での検討事項:

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 単独でも動作します。

使い分け:


Q3: テキスト形式とバイナリ形式、どちらを使うべき?

A: 用途によります。

用途 推奨形式 理由
デバッグ GOMT(テキスト) 人間が読める、Git diff が見やすい
プロトタイピング GOMT(テキスト) 手書き可能、素早い修正
プロダクション GOMB(バイナリ) 高速、ファイルサイズ小
MTA MTAB(バイナリのみ) v1 ではテキスト非サポート

Q4: skeletonSig が一致しないエラーが出ます

A: 以下を確認してください:

チェックリスト:

  1. bonePath が一致しているか

    • GOM と MTA で同じボーン階層か?
    • ボーン名が完全に一致しているか?
  2. parentIndex が一致しているか

    • 親子関係が同じか?
  3. 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: はい、可能です。

条件:

  1. すべて同じ skeletonSig
  2. 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 の内容:

v2 以降の拡張予定:


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  # 差分なし

保証される項目:

なぜ重要か:


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(スケールが変わる)

対策:


まとめ

GOM の利点

MTA の利点

詳細仕様

詳細は以下を参照:


作成日: 2025-01-XX
バージョン: GOM v1.0 / MTA v1.0
ライセンス: プロジェクト固有


End of Quick Reference

Visual Studioの更新うざいよね?

設定色々変えたけど、更新のお知らせが来るよ

 表題のとおり、残念ながら何回もいろんな設定試したんだけど、更新のお知らせが来ます。
もうどうしようもないです。

仕方がないので、個人設定を変更してこの更新の通知を止めていきます。
以下に、やり方を書くので各自やってみてください。
(学校のPCだけね。おうちのPCはちゃんとアップデートすればいいと思うよ)

1.メニューから、ツール→オプションを選ぶスクリーンショット 2026-05-07 120517.png

2.左の設定項目から、環境を選択

スクリーンショット 2026-05-07 120551.png

3.環境の設定項目がずらずら右ペインに出るので、一番下に行く

スクリーンショット 2026-05-07 120624.png

4.出てきたダイアログ(子ウィンドウ)の検索フォームに注目

スクリーンショット 2026-05-07 120648.png

5.検索フォームに「更新」と入力

スクリーンショット 2026-05-07 120700.png

6.「拡張機能マネージャー」を選択→更新プログラムを自動的にダウンロードする、のチェックを外す
スクリーンショット 2026-05-07 120714.png

7.同じく「製品の更新プログラム」を選択→更新プログラムを自動的に。。。のチェックを外す

スクリーンショット 2026-05-07 120718.png

8.「OK」を押して設定完了。これで更新の通知が出ないはず。

動画で設定の様子を見るならこちら

youtubeボタン押して、でかい画面で見るがいいさ。

DxLibで3Dキャラクターの移動(基本)

カメラの違いによるキャラクター移動操作

このサンプルでは、同じプレイヤー操作を使いながら、カメラの種類によって見え方や操作感がどのように変わるかを確認します。

使用するカメラは次の3種類です。

カメラの種類は CAMERA_MODE で管理しており、CAMERA_FIXEDCAMERA_TPSCAMERA_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);

固定カメラのサンプル動画


ラジコン操作の特徴

固定カメラでは、画面の上方向とキャラクターの前方向が常に一致するとは限りません。

そのため、操作感としてはラジコンに近くなります。

例えば、キャラクターが画面手前を向いている状態で 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カメラは、キャラクターの進行方向と画面の見え方が一致しやすいため、アクションゲームで使いやすいカメラです。

固定カメラと比べると、キャラクターを中心にした操作感になります。


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));

その後、カメラ位置を次のように調整しています。

  1. プレイヤー位置を基準にする
  2. プレイヤーの後ろに下げる
  3. 右方向にずらす
  4. 高さを加える

これにより、キャラクターの背後やや右側から見る「肩越し視点」になります。

さらに、注視点も少し左側にずらしています。
これにより、キャラクターが画面の中央に完全に重なるのではなく、少し横に寄った構図になります。

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_);

カメラ側では、その位置と向きを使って、現在のカメラモードに応じたカメラ位置を計算しています。

つまり、処理の流れは次のようになります。

  1. キー入力を確認する
  2. プレイヤーの回転角度を更新する
  3. 回転角度から進行方向 dir_ を作る
  4. W / S キーでプレイヤー位置を移動する
  5. プレイヤーの位置と向きをモデルに反映する
  6. プレイヤーの位置と進行方向をカメラに渡す
  7. カメラモードに応じてカメラ位置を更新する

まとめ

固定カメラ、TPSカメラ、OTSカメラは、同じキャラクター移動を使っていても、プレイヤーに与える操作感が大きく変わります。固定カメラでは、カメラが動かないため、キャラクター自身の向きに合わせて操作するラジコン操作になります。TPSカメラでは、カメラがキャラクターの後ろに回り込むため、進行方向が分かりやすく、3Dアクション向きの操作感になります。OTSカメラでは、キャラクターの肩越しから見ることで、狙う・注視する感覚が強くなり、シューティングやホラーゲームのような演出に向いています。


おまけ

固定カメラ+固定方向(上下左右)移動+なめらかな角度変化

ここまでの固定カメラでは、キャラクターの向きを基準にして前進・後退する「ラジコン操作」を説明しました。

このおまけでは、固定カメラ用の別操作として、画面方向に対して固定された移動を行います。

サンプルプログラムのURLは
https://github.com/youetsux/radioControllman2

ラジコン操作では、

という操作でした。

それに対して、固定方向移動では、

のように、キーと移動方向を固定します。

固定カメラの場合、カメラの位置が変わらないため、画面上の上下左右とワールド座標の前後左右を対応させやすくなります。



固定カメラのときだけ移動処理を切り替える

このサンプルでは、カメラモードが固定カメラのときだけ 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);

ここでは、

を表しています。

つまり、キャラクターが今どちらを向いているかに関係なく、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));

この処理では、押されたキーに対応する方向ベクトルを足し合わせています。

例えば、WD を同時に押した場合は、

forward + right

となるため、右奥方向への移動になります。


斜め移動が速くならないように正規化する

W だけを押した場合、移動方向の長さは 1 です。

しかし、WD を同時に押した場合、移動方向は斜めになります。
このまま移動すると、斜め移動だけ距離が長くなり、移動速度が速くなってしまいます。

そこで、移動方向の長さを求めています。

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の場合は、何もキーが押されていない状態なので移動処理を終了します。

キー入力がある場合は、moveVecmoveLen で割って、長さ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

宝箱に近づいている
↓
isPlayerNearTreasure

40〜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)

と書くことはできます。

しかし、isFindPlayertrue になる理由を作らなければ、モンスターはプレイヤーを見つけることができません。

つまり、

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)
};

ただし、モデルの正面方向や座標系によって、sinfcosf の使い方は変わることがあります。


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 の目安

条件

見える範囲の目安

dot > 0.5f

前方約120度

dot > 0.707f

前方約90度

dot > 0.866f

前方約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文 を書けばよいか考えなさい。

既存ゲームを改良するよ(何様じゃい!)

既存ゲームの「足りない部分」を見つけて進化案を考える手順書

この課題でやること

この課題では、前回分析したゲームをもとにして、
「このゲームはどこが弱いのか」
「どこを足せばもっと面白くなるのか」
を考えます。

ただし、完全に別のゲームを作るのではありません。

元のゲームの面白さを残したまま、
足りない部分を補って、少し進化させることが目的です。


対象ゲーム

前回分析した次のゲームの中から、自分が選んだものを使います。


手順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. その項目を「足りない部分1位」にする
  3. 次に低い項目を「足りない部分2位」にする
  4. さらに次に低い項目を「足りない部分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 _点 _点 _点 _点
アイデア2 _点 _点 _点 _点
アイデア3 _点 _点 _点 _点

各項目は、1点から5点で評価してください。

点数の意味

点数 意味
5点 とても良い
4点 良い
3点 普通
2点 少し弱い
1点 あまり良くない

手順17 一番よい進化案を1つ選ぶ

手順16で合計点が高かったアイデアを1つ選びます。

記入欄

私が選んだ進化案は、
「__________________」
である。

選んだ理由は、
__________________________________
である。


手順18 進化案の内容を詳しく説明する

選んだ進化案について、詳しく説明します。

書く内容

次の4つを必ず書いてください。

  1. 何を追加するのか
  2. いつ出てくるのか
  3. プレイヤーはどう行動するのか
  4. それによって何が面白くなるのか

記入欄

追加するもの

__________________________________

いつ出てくるか

__________________________________

プレイヤーはどう行動するか

__________________________________

それによって何が面白くなるか

__________________________________


手順19 元のゲームの面白さを壊していないか確認する

最後に、自分の進化案が
「元のゲームの面白さ」
を壊していないか確認します。

チェック項目

チェック内容 はい / いいえ
元のゲームの目的は残っている __
元のゲームの基本操作は残っている __
元のゲームの緊張感や面白さは残っている __
追加した要素が目立ちすぎていない __
別のゲームになりすぎていない __

判断

上のチェックで「いいえ」が多い場合は、
アイデアを少し小さくするか、元のゲームに合う形に直してください。


手順20 進化案を一文でまとめる

最後に、自分の進化案を一文でまとめます。

書き方

このゲームは、もともと
「__________」
という面白さがある。

しかし、
「__________」
が足りないと感じた。

そこで、
「__________」
を追加することで、
「__________」
という体験を強くする。

記入欄

このゲームは、もともと
「__________________」
という面白さがある。

しかし、
「__________________」
が足りないと感じた。

そこで、
「__________________」
を追加することで、
「__________________」
という体験を強くする。


提出するもの

次の内容を提出してください。

  1. ゲーム名
  2. 中心的な面白さ
  3. 6項目の評価表
  4. 星による見える化
  5. 足りない部分ランキング
  6. 補いたい項目
  7. アイデア3つ
  8. 選んだ進化案
  9. 元のゲームの面白さを壊していないかのチェック
  10. 進化案のまとめ文

書くときの注意

1. ただの追加要素で終わらせない

悪い例:

これだけでは、なぜ面白くなるのかが分かりません。

良い例:


2. 「AだからBになる」で書く

説明するときは、できるだけ次の形で書きます。


3. 元のゲームを別物にしすぎない

進化案は、元のゲームを完全に作り変えることではありません。

たとえば、パックマンに次のような要素を足すと、別のゲームになりすぎる可能性があります。

元のゲームの面白さを残したまま、少し広げることを意識してください。


簡単な完成例

ゲーム名

パックマン

中心的な面白さ

敵に追われながら、迷路の中のエサを集める緊張感。

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:条件が増えると分かりにくい

例えば、モンスターに次の行動を追加したいとします。

これを 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には、まだ弱点があります。

例えば、

などです。

自分で、この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 では、計算した vPostransform_.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などで表すものです。

出力の例


フィードバックとは

フィードバックとは、プレイヤーに
「何が起きたか」
「操作が成功したか」
「失敗したか」
「危険なのか」
「報酬を得たのか」
を伝える反応のことです。

フィードバックの例


手順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の例

記入欄

ゲーム名 UI表示 何を伝えているか 見やすさ
____ ________ ________ 見やすい / 見にくい
____ ________ ________ 見やすい / 見にくい
____ ________ ________ 見やすい / 見にくい
____ ________ ________ 見やすい / 見にくい
____ ________ ________ 見やすい / 見にくい

手順11 フィードバックが弱いとどうなるか考える

フィードバックが弱いと、プレイヤーは何が起きたのか分かりにくくなります。

フィードバックが弱い状態 プレイヤーが感じること
攻撃が当たっても音が鳴らない 当たったか分からない
ダメージを受けても画面変化が少ない いつHPが減ったか分からない
危険な攻撃の予備動作がない 急にやられたと感じる
ボタンを押しても反応が薄い 操作している感じがしない
UIが見にくい 状況を判断しにくい
報酬を取っても演出が弱い 嬉しさが弱い

記入欄

フィードバックが弱いと、プレイヤーは
__________________________________
と感じる。

特にゲームで問題になるのは、
__________________________________
である。

その理由は、
__________________________________
である。


手順12 入力とフィードバックの関係を図にする

調べた内容から、入力とフィードバックの流れを図の形で整理します。

記入欄1

入力:__________
↓
処理:__________
↓
出力:__________
↓
視覚フィードバック:__________
↓
聴覚フィードバック:__________
↓
触覚フィードバック:__________
↓
プレイヤーの理解:__________

記入欄2

入力:__________
↓
処理:__________
↓
出力:__________
↓
視覚フィードバック:__________
↓
聴覚フィードバック:__________
↓
触覚フィードバック:__________
↓
プレイヤーの理解:__________

手順13 プログラマとして必要な処理を考える

フィードバックは、プログラムで実装する必要があります。

やりたいこと 必要な処理
ジャンプさせたい 入力判定、速度変更、重力、着地判定
攻撃を当てたい 攻撃判定、敵との当たり判定、ダメージ処理
攻撃の手応えを出したい 効果音、ヒットエフェクト、画面揺れ、ヒットストップ
HPを減らしたい ダメージ計算、HP更新、UI更新
危険を知らせたい 攻撃前の予備動作、警告表示、警告音
アイテム取得を伝えたい アイテム削除、スコア加算、取得音、エフェクト
振動を入れたい 対応機器確認、振動開始、振動時間管理、振動停止

記入欄

作りたいフィードバック 必要そうなプログラム処理
________ ______________
________ ______________
________ ______________
________ ______________
________ ______________

手順14 自分のゲームに使えそうなものを選ぶ

調べた中から、自分がゲーム制作で使ってみたいフィードバックを選びます。

記入欄

自分のゲームに使ってみたいフィードバックは、
__________________________________
である。

それを使いたい理由は、
__________________________________
である。

そのフィードバックを入れることで、プレイヤーに
__________________________________
を伝えたい。

実装するとしたら、必要そうな処理は、
__________________________________
である。


手順15 最終まとめを書く

ここでは、調べた内容をもとに、
ゲームプログラマとして自分の制作にどう活かせるかをまとめます。

単に
「音が鳴っていた」
「エフェクトが出ていた」
で終わらせないこと。

次の3つを意識して書きます。

  1. 何をプレイヤーに伝えるためのフィードバックなのか
  2. どの感覚に向けたフィードバックなのか
  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字まとめ

記入欄

今回の研究では、ゲームの入力に対して、
__________________________________
というフィードバックが返ることで、プレイヤーが
__________________________________
を理解しやすくなると分かった。

自分でゲームを作るときは、
__________________________________
を意識して実装したい。


提出するもの

次の内容を記入して提出すること。

  1. 調べたゲーム一覧
  2. 入力の種類の表
  3. 入力から出力までの流れ
  4. 視覚フィードバックの表
  5. 聴覚フィードバックの表
  6. 触覚フィードバックの表
  7. 複数の感覚を組み合わせたフィードバックの表
  8. UIによるフィードバックの表
  9. フィードバックが弱いとどうなるか
  10. 入力とフィードバックの流れ図
  11. プログラマとして必要な処理
  12. 自分のゲームに使いたいフィードバック
  13. 実装チェックリスト
  14. 研究全体のまとめ
  15. 100〜150字まとめ

書くときの注意

1. いろいろなジャンルを見る

同じジャンルだけでなく、できれば違うジャンルのゲームを見てください。

ジャンルが違うと、入力やフィードバックの使い方も違います。


2. 「入力」と「反応」をセットで見る

入力だけを書いても、ゲームの研究にはなりません。

悪い例

良い例


3. 「何を伝えているのか」を考える

フィードバックを見るときは、ただ
「音が鳴る」
「光る」
で終わらせないでください。

そのフィードバックが何を伝えているのかを考えます。


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の行動は、いきなりコードを書くよりも、先に表や図で整理すると分かりやすくなります。