🙌 なぜ Unity は C# を選び、Unreal は選ばなかったのかについての個人の考察

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに ゲームエンジンの言語の話になると、よくこんな対比が出てきます。 Unity は C#。 Unreal は C++ と Blueprint。 この違い、ただの好みではありません。 エンジンの設計思想そのものに関わっています。 C# が優れているとか、C++ が古いとか、そういう単純な話ではない。 両者はそもそも目指している開発体験が違うのです。 Unity が辿った言語統一の歴史 今でこそ「Unity といえば C#」という印象がありますが、最初からそうだったわけではありません。 初期の Unity では、開発者のバックグラウンドに合わせて複数の言語を選択できるのが特徴でした。 具体的には次の3つです。 C# UnityScript (JavaScript 風の独自言語) Boo (Python 風の言語) 当時の Unity は「スクリプト言語の自由度」を売りの一つにしており、Web 開発者は UnityScript、Python 系に慣れている人は Boo、という具合に選べるようになっていました。 しかし、この方針は長くは続きませんでした。 Unity 2017 頃から段階的に非推奨となり、 Unity 2019.1 を最後に UnityScript と Boo は完全に削除 されます。 結果として、Unity のスクリプト言語は C# のみ に統一されました。 参考 https://discussions.unity.com/t/which-release-removed-support-unityscript-and-boo/1624299 なぜこうなったのか。理由はいくつかあります。 まず、複数言語の維持コストです。 エンジン側はコンパイラ、ツールチェーン、デバッグ環境などをすべてサポートし続ける必要があります。 さらに重要なのは、UnityScript と Boo の実態です。 これらは見た目こそ別言語でしたが、内部では Mono 上で動作するスクリプトでした。 ...

2026年3月11日 · 2 分 · 397 文字 · Wuyukwi

🎉 Git を「完全に理解」するためのデータ構造の話

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに Git を使っていて、こんな感覚になったことはありませんか? コマンドは打てるけど、何が起きているか分からない merge と rebase の違いを、説明しろと言われると詰まる conflict が出ると、なぜ衝突したのか理解できない 多くの Git 解説記事は、 「何を打つか」 は丁寧に教えてくれますが、 「なぜそうなるか」 までは踏み込んでくれません。 その結果、 なんとなく使えているが、仕組みはよく分からない という状態に陥りがちです。 この記事では、コマンドの説明はほぼ行いません。 代わりに、Git の内部構造をデータ構造として捉え直す ことで、 「あ、そういうことだったのか」 と腑に落ちる理解を目指します。 少しだけ グラフ理論 と ポインタの概念(C 言語) の素養があると、 かなりスムーズに理解できると思います。 Git の正体:木構造ではなく DAG(有向非巡回グラフ) 多くの人は、Git の履歴を「木構造」としてイメージしています。 しかし、これは正確ではありません。 Git の本質は 有向非巡回グラフです。 Commit は「ノード」 すべての commit は、グラフ上の ノード です。 各 commit は、 ファイルのスナップショット 親 commit への参照(1 個または 2 個) を持っています。 親が 2 個になる瞬間 = merge 通常の commit は親が 1 つですが、 merge commit は 親を 2 つ 持ちます。 ...

2026年2月25日 · 3 分 · 437 文字 · Wuyukwi

😎 「リンクリストはもう使われない」のか?

📌 この記事は Zenn に投稿した内容のアーカイブです。 ――2026年のハードウェア前提で考えるデータ構造の現実 エンジニア向けの記事や SNS で、 「リンクリストはもう終わった」 という表現を見かけることがあります。 最初にこの話を聞いたとき、正直かなり極端だなと思いました。 ただ、実務でパフォーマンスを意識した設計や実装を重ねていくと、この言い方が完全に間違っているとも言い切れない 、という感覚になってきます。 この記事では、「リンクリストがなぜ避けられるようになったのか」「それでも今なお使われ続けている理由」を、現代の CPU・メモリ構成を前提に 整理してみます。 教科書的な理解と、現実のズレ 学生時代にデータ構造を学んだとき、ほとんどの人が次のように習ったはずです。 配列:途中への挿入・削除は O(N) リンクリスト:挿入・削除は O(1) 理論としては正しいです。 ただし、ここには重要な前提条件 があります。 メモリアクセスのコストはすべて同じである 現実のハードウェアでは、これは成立しません。 キャッシュ階層という「無視できない現実」 現代の CPU では、メモリは階層構造になっています。 L1 キャッシュ:数サイクル L2 キャッシュ:十数サイクル L3 キャッシュ:数十サイクル メインメモリ(RAM):数百サイクル この差は無視できるものではありません。 特に問題になるのが、アクセスパターンが予測できない場合 です。 リンクリストが不利になる理由 典型的なリンクリストは、ノードがメモリ上に散らばって配置されます。 Node A -> Node B -> Node C 見た目はシンプルですが、実際のアドレスは次のようになっているかもしれません。 0x1000 -> 0x8F20 -> 0x3A10 リンクリストを走査する場合、 現在のノードを読み込む next ポインタを読む 次のアドレスが分かってから、次の読み込みを行う という強い依存関係のある処理 になります。 CPU のプリフェッチ機構は、連続したアクセス(配列など)は得意ですが、 リンクリストのようなポインタ追跡はほとんど先読みできません。 結果として、キャッシュミスが頻発 します。 配列(vector)が速い本当の理由 std::vector や配列を順番に処理しているとき、 実際には CPU はすでに次のデータをキャッシュに読み込んでいます。 ...

2026年2月2日 · 1 分 · 187 文字 · Wuyukwi

🦔 Overwatchにおけるネットコードと予測技術の解説

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに:高速対戦ゲームにおけるネットコードの本質 ゲームプレイ(GamePlay)エンジニアにとって、最重要テーマのひとつが ネット同期(netcode) です。 特に Overwatch のような高速リアルタイムアクションでは、「レスポンスの速さ(responsive)」 がゲーム性を決定づけます。 そのため、プレイヤーの操作は サーバーの応答を待たずに即時反映 する必要があります。 これを実現するのが 予測(prediction / pre-presentation) です。 ただし、クライアントを信頼できない(チート対策)というFPSの前提は20年変わっていません。 そのうえで「即時応答」と「サーバー権威」の両立を図るのがネットコード設計の核心になります。 即時応答が必要な操作 移動 スキル 武器(発射や装填などのスキル付き武器) 当たり判定(Hit Registration) 共通原則はただ一つ: 「プレイヤーがボタンを押したら即、見た目の挙動が起きること」 どれほど ping が高くても、遅延を感じさせてはならない。 しかし、予測を行う以上、避けられない問題があります── それが 予測ミス(misprediction) です。 予測ミスは、 「クライアント側では成功したように見えた操作が、サーバー上では成立していない」 状態を指します。 Overwatch では、予測ミスが起きても「操作を遅延させる」のではなく、 できる限り予測ミスを減らす設計(=確定性 Determinism) が採用されています。 予測ミスの例:サーバーとの食い違い ping 250ms。 クライアントでは「ジャンプした」つもりでも、サーバー上では Mei の氷結を受けていた 。 その結果、クライアントは一度ジャンプの予測を行った後、サーバーからの正しい状態(氷結)で「強制的に巻き戻される」。 このように、超高速応答を目指すほど、予測ミスが発生しやすくなります。 ▼ ここから:低ミス予測を支える「確定性(Determinism)」 以下では Overwatch が採用している ECS + Deterministic Simulation の要点を整理します。 1. 時間の量子化:Command Frame 確定的なシミュレーションは、 時間の同期 固定更新周期 値の量子化 ...

2025年11月21日 · 2 分 · 229 文字 · Wuyukwi

🌟 C++11 が左辺値と右辺値を5分類に拡張した理由

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに C++11 で「右辺値参照(rvalue reference)」が導入されたことで、C++ の値分類(value category)は大きく進化しました。 C++98 では「左辺値(lvalue)」と「右辺値(rvalue)」の二分法しかありませんでしたが、C++11 ではさらに細かく分類されます: C++11で導入された新しい概念である xvalue(期限切れ右辺値) と prvalue(純粋右辺値) が、 この章の中核的な進化ポイントです。 C++98 時代の「単純な世界」 C++98 の時代はとてもシンプルでした。 左辺値 (lvalue) :代入の左側に置けるもの。永続的なアドレスを持つ。 例:a, *ptr, array[i] 右辺値 (rvalue) :一時的な値。すぐに破棄される。 例:a + 5, 42, func() ルールも直感的でした: int a = 3; &a; // OK 左辺値はアドレスが取れる &(a + 5); // エラー!右辺値はアドレスが取れない しかしこの単純さが、性能の壁 になっていました。 問題:無駄なコピーが多すぎる std::vector<int> create_vector() { std::vector<int> temp; temp.push_back(1); temp.push_back(2); temp.push_back(3); return temp; // ここで何が起こる? } std::vector<int> v = create_vector(); C++98 の仕様では return temp; で コピーが発生 します。 temp のヒープメモリをまるごと複製し、呼び出し側に返すのです。 ...

2025年10月27日 · 2 分 · 319 文字 · Wuyukwi

💡 C++ の「奇妙な再帰テンプレートパターン(CRTP)」を理解する

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに C++ のテンプレートには数多くの応用テクニックがありますが、その中でも「CRTP(Curiously Recurring Template Pattern:奇妙な再帰テンプレートパターン) 」は特にユニークで強力な手法です。 Boost をはじめとする有名ライブラリでも多用されており、C++ の表現力を大きく広げてくれます。 この記事では、CRTP の基本から実用例、さらには応用パターンまでを整理して解説します。 CRTP とは? CRTP の基本形は以下のようになります。 template <typename T> class Base { public: T& getDerived() { return static_cast<T&>(*this); } }; class Derived : public Base<Derived> { // Derived 自身を Base のテンプレート引数として渡す }; 一見すると「派生クラス Derived が、まだ完成していない自分自身を基底クラス Base に渡している」ように見えます。この“自己参照”こそが CRTP の特徴です。 コンパイラはこれを問題なく処理でき、基底クラスはコンパイル時に派生クラスの型情報を知ることができます。 実用例:比較演算子の自動生成 C++ では == や < などの比較演算子を定義するのはよくある作業ですが、すべてを個別に書くと冗長になりがちです。 CRTP を用いると、基底クラスに「共通の比較演算子」をまとめ、派生クラスは最小限の実装だけで済ませられます。 基底クラス template <typename Derived> class Comparable { public: bool operator==(const Derived& other) const { return static_cast<const Derived*>(this)->equalsImpl(other); } bool operator!=(const Derived& other) const { return !(*this == other); } bool operator<(const Derived& other) const { return static_cast<const Derived*>(this)->lessImpl(other); } bool operator>(const Derived& other) const { return other < *this; } bool operator<=(const Derived& other) const { return !(*this > other); } bool operator>=(const Derived& other) const { return !(*this < other); } }; 派生クラス例 class Point : public Comparable<Point> { public: Point(int x_val, int y_val) : x(x_val), y(y_val) {} bool equalsImpl(const Point& other) const { return x == other.x && y == other.y; } bool lessImpl(const Point& other) const { return (x != other.x) ? (x < other.x) : (y < other.y); } private: int x, y; }; これで ==, !=, <, >, <=, >= がすべて利用可能になります。 ...

2025年9月30日 · 2 分 · 235 文字 · Wuyukwi

😯 C++で高性能計算を書くときに意識すべき最適化ポイントまとめ

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに 本記事では、私自身が経験したことや、信頼できる実践的な知見を元に、C++で高速な数値処理や行列演算を行う際に意識すべき最適化ポイント をわかりやすく整理しました。 HPCやリアルタイム演算に関心のある方に参考になれば幸いです。 1. メモリのアライメントとキャッシュ効率 アライメント(__attribute__((aligned(n))) CPUはキャッシュライン(通常64バイト)単位 でメモリを読み込む。 メモリが正しく整列されていないと、キャッシュミスやfalse sharing が発生し、性能が大幅に低下。 GCC/Clangなら __attribute__((aligned(32))) など、MSVCでは __declspec(align(32)) を使う。 連続したメモリを使用する(std::vector<T>など) 行列は行優先(row-major)の順番で連続配置することで、CPUの事前読み込み(prefetch)が効きやすくなる。 std::vector<std::vector<float>> のような非連続構造は避ける。 2. メモリ確保の最適化:高速メモリアロケータ 標準の malloc や new は汎用的で安全ですが、性能面ではボトルネックになることがあります。 おすすめのアロケータ tcmalloc (Google製、スレッドセーフ) jemalloc (Facebook製、ゲームやサーバーで人気) Hoard (スケーラブルなスレッド対応) これらを使うと、多スレッド環境でのアロケーション性能が大幅に向上 します。 3. 行列演算の高速化手法 Strassenアルゴリズム(O(n^2.81)) 通常のO(n³)よりも理論上速い。 再帰的に分割しながら計算することで、掛け算回数を削減。 ブロック分割(Block-based Multiplication) 大きな行列を小さなサブブロックに分けて計算 すると、キャッシュが有効活用できる。 代表的なライブラリ(後述するBLASなど)ではこの最適化が組み込まれている。 4. 並列計算(マルチスレッド化) C++11の標準スレッド std::thread, std::async を使って並列化可能。 シンプルな制御だが、スレッド数の管理や同期は自前で対応が必要。 OpenMP #pragma omp parallel for のように、簡潔にfor文を並列化。 コンパイラ依存(GCC/Clang/MSVC全て対応)。 Intel TBB(Thread Building Blocks) 動的なタスクスケジューリング・負荷分散・ワークスティーリングが可能。 不規則な処理を高速並列化したいときに強力。 ...

2025年8月4日 · 1 分 · 204 文字 · Wuyukwi

💡 C++ の emplace_back は push_back を完全に置き換えられるか?

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに C++11 から導入された emplace_back は、コンテナに要素をその場で構築できる便利なメソッドです。 一方、それ以前から使われてきた push_back とはどんな違いがあり、emplace_back は完全に push_back を代替できるのでしょうか? 本記事では、 emplace_back が本当に push_back より常に速いのか 両者がどんなケースに使えるのか 実装の違いと注意点 について、自分なりに整理した考えをまとめます。 push_back と emplace_back の違い push_back // C++20以降 constexpr void push_back(const T& value); // コピー constexpr void push_back(T&& value); // ムーブ push_back は、すでに生成されたオブジェクトをコピーまたはムーブして格納する関数です。 emplace_back // C++20以降 template<class... Args> constexpr reference emplace_back(Args&&... args); // その場で構築 emplace_back は、コンストラクタに渡す引数をそのまま受け取り、コンテナ内部で直接オブジェクトを構築します。そのため、中間オブジェクトなし に要素を追加できる点が特徴です。 これらの大きな違いは、push_backが既に生成されたオブジェクト(T型)をコピーまたはムーブする一方で、emplace_backは任意のコンストラクタ引数からその場でT型のインスタンスを構築できる点です。 LLVMにおける内部実装 以下はLLVM(libc++)におけるpush_back内部の実装です。 if (this->__end_ != this->__end_cap()) { __construct_one_at_end(__x); // 残り容量がある場合、__xをコピーまたはムーブ構築 } else { __push_back_slow_path(__x); // 足りない場合、新規領域を確保して再配置 } __construct_one_at_end の中では、allocatorが提供する construct() を使って、指定されたメモリにオブジェクトを構築しています。 また、allocatorが construct() を持たない場合には単純に in-place new を呼び出す仕組みです。 ...

2025年6月24日 · 1 分 · 193 文字 · Wuyukwi