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

📌 この記事は 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

📃 UE5 における Gameplay Ability System & Game Features を「いつ使うべきか」という個人的な考察

📌 この記事は Zenn に投稿した内容のアーカイブです。 UE5 には Gameplay Ability System(GAS) や Game Features(GF) といった、拡張性と再利用性を強く意識した公式フレームワークが用意されています。 最近では Epic Games もこれらの利用を積極的に推奨しており、 「公式が用意しているのだから、最初から使うべきでは?」 と感じる方も少なくないと思います。 しかし、すべてのプロジェクト・すべての開発フェーズで導入すべきか というと、 個人的には 明確にそうではない と考えています。 この記事では なぜプロジェクト初期で GAS / Game Features を導入すると負担になりやすいのか どの段階で導入すると真価を発揮するのか について、自身の経験をもとに整理します。 結論:GAS / Game Features は優秀だが「導入時期」が重要 まず前提として GAS は長年運用され、大規模タイトルで実績のあるシステム Game Features も 機能分離・モジュール化を重視した現代的な設計 であり、品質や思想そのものに問題があるわけではありません。 問題になるのは 「いつ導入するか」 です。 プロジェクト初期・プロトタイプ段階で起きやすい問題 ① フレームワークが過剰に重い GAS や Game Features は 高い汎用性 拡張を前提とした設計 長期運用への耐性 を重視しています。 その結果 クラス構造が複雑 Ability / Attribute / Effect / GameplayTag など前提概念が多い 正しく使い始めるまでの学習コストが高い という特徴があります。 ...

2026年1月21日 · 1 分 · 169 文字 · 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

🧠 モダンなフレームワークは「低レイヤの理解」から遠ざけるか?

📌 この記事は Zenn に投稿した内容のアーカイブです。 〜意図(ビジネス要件)と低レイヤ知識の役割について考える〜 「最近のエンジニアは低レイヤを知らない」──そんな声を耳にすることが増えました。 それは本当に問題なのでしょうか? 1. プログラミングの本質:意図をコンピュータに伝えること プログラミングとは、人間の意図をコードという形でコンピュータに伝える行為 です。 言語やフレームワークはそのための「伝達チャンネル」にすぎません。 用途や目的が異なるからこそ、多様な言語やフレームワークが存在しています。 業務アプリ:可読性・保守性を重視 ゲームやリアルタイム処理:性能やレイテンシを重視 研究・プロトタイピング:表現力・試行錯誤のしやすさを重視 重要なのは「何を伝えたいのか(ビジネスの意図) 」であり、 低レイヤ(ハードウェアや OS の仕組み)はそれを実現するための手段 にすぎません。 2. 抽象化は「ノイズ除去」の進化である ソフトウェア開発の歴史を振り返ると、それは常に「ノイズを減らす」試みの連続でした。 アセンブリ言語:レジスタやメモリ操作のノイズを扱う 高級言語:ポインタ操作や手動メモリ管理のノイズを隠蔽 モダンフレームワーク:接続プールやリトライなど運用上のノイズを隠蔽 この観点から見ると、抽象化は「情報理論的に正しい方向への進化」です。 不要な実装詳細を隠すことで、開発者はより本質的な意図に集中できる ようになります。 3. それでも低レイヤ知識は不要にならない 抽象化が進んだからといって、低レイヤの知識が不要になるわけではありません。 実際には「抽象の漏れ(Leaky Abstraction)」や、フレームワークの誤用によって問題が生じることがあります。 そうしたときに求められるのは、下のレイヤを理解し、意図が正しく実現されているかを検証する力 です。 ただし、低レイヤの学習そのものが目的化してしまうと、肝心の「ビジネス意図の理解」が置き去りになります。 4. 最適化の本質は「情報の圧縮」である 最適化とは、突き詰めれば「情報の圧縮」です。 プログラムはビジネスの意図を機械命令に符号化したものとも言えます。 良い最適化とは、業務知識(確信)をもとに冗長性を取り除くこと によって成立します。 たとえば: キャッシュ導入の根拠は「そのデータは一定期間変化しない」という業務知見 インデックス設計の根拠は「特定フィールドの検索頻度が高い」という利用傾向 つまり、最適化とは単なる技術的チューニングではなく、業務理解と技術の融合による意思決定 なのです。 5. 戦術的勤勉 vs 戦略的怠惰 低レイヤを極めること自体は悪いことではありません。 しかし、それが「業務理解を放棄する言い訳」になってしまっては本末転倒です。 戦術的勤勉:低レイヤを丹念に学ぶ(手段) 戦略的怠惰:業務要件の理解を怠る(目的の喪失) まずは正しい問題設定──すなわち「ビジネスの意図」を明確にすること。 そのうえで、低レイヤの知識を活用して実装や最適化を行う。 この順序こそが、エンジニアリングにおける本質的なアプローチです。 6. 結論:抽象化の流れは正しい。ただし、バランスが鍵 モダンな抽象化は「意図に近づく」ための進化であり、退化ではない。 低レイヤ知識は「検証・トラブルシューティング・高性能化」のために必要だが、目的ではない。 成功するエンジニアは、**業務理解(何を伝えるか)と 技術力(どう実現するか)**をバランスよく使い分ける。 本質は「技術」ではなく「意図」。 技術は、意図を実現するための道具にすぎない。

2025年11月6日 · 1 分 · 69 文字 · 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

💡 【Unity UGUI】Maskを使わずに実現する「穴あき」ハイライト演出

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに UGUIで部分的にハイライトを当てたい場面、特にチュートリアル中の誘導UI では「特定のボタンだけを明るく見せ、他の部分を半透明で覆う」演出がよく使われます。 一般的には Mask や RectMask2D を利用しますが、これらにはいくつかの課題があります。 Mask :Stencilバッファを使うため、描画コストが高い RectMask2D :矩形領域しか扱えず、穴あき表現(逆マスク) はできない そこで今回は、MaskableGraphic を継承し、完全プログラム制御で「逆マスク(穴あきマスク)」を生成する方法 を紹介します。 最終的には、ハイライト部分がクリック可能で、他の部分はブロックされる 、高性能な HollowOutMask コンポーネントを実装していきます。 目標仕様 機能 説明 視覚効果 自身の RectTransform 領域内に半透明のマスクを描画し、指定の RectTransform 部分を「くり抜く」 射線処理 穴の部分はクリックが通過 し、マスク部分はクリックをブロック 動的追従 対象オブジェクトが動いたりスケールが変化しても、くり抜き領域が追従 これを、Mask 系コンポーネントを使わずに、頂点操作 だけで実現します。 実装の基本構造 HollowOutMask は MaskableGraphic を継承し、OnPopulateMesh() をオーバーライドします。 頂点生成の考え方 想像してみてください。 マスク全体を表す「外側の矩形(Outer Rect)」と、くり抜く「内側の矩形(Inner Rect)」があるとします。 この2つの矩形を組み合わせ、外側を塗りつつ内側を抜くには、8つの頂点 と8枚の三角形 が必要です。 コード例:OnPopulateMesh() protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); if (_isTargetNull) { base.OnPopulateMesh(vh); return; } // Outer Rect(自身のRectTransform基準) float outerLx = rectTransform.rect.xMin; float outerBy = rectTransform.rect.yMin; float outerRx = rectTransform.rect.xMax; float outerTy = rectTransform.rect.yMax; vh.AddVert(new Vector3(outerLx, outerTy), color, Vector2.zero); vh.AddVert(new Vector3(outerRx, outerTy), color, Vector2.zero); vh.AddVert(new Vector3(outerRx, outerBy), color, Vector2.zero); vh.AddVert(new Vector3(outerLx, outerBy), color, Vector2.zero); // Inner Rect(穴部分) float innerLx = _targetMin.x; float innerBy = _targetMin.y; float innerRx = _targetMax.x; float innerTy = _targetMax.y; vh.AddVert(new Vector3(innerLx, innerTy), color, Vector2.zero); vh.AddVert(new Vector3(innerRx, innerTy), color, Vector2.zero); vh.AddVert(new Vector3(innerRx, innerBy), color, Vector2.zero); vh.AddVert(new Vector3(innerLx, innerBy), color, Vector2.zero); // Triangles(外側と内側の間を埋める) vh.AddTriangle(0, 1, 5); vh.AddTriangle(5, 4, 0); vh.AddTriangle(1, 2, 6); vh.AddTriangle(6, 5, 1); vh.AddTriangle(2, 3, 7); vh.AddTriangle(7, 6, 2); vh.AddTriangle(3, 0, 4); vh.AddTriangle(4, 7, 3); } これで、中央が透明な「リング状ポリゴン を生成できます。 ...

2025年10月20日 · 2 分 · 341 文字 · Wuyukwi

📚 UI開発を10倍速くするための実戦ポイント

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに 多くの人は「UI開発=手の速さ」だと思っています。 でも実際のプロジェクトでは、進行を遅らせる原因はコードの速さではなく、データフローとUI構造設計の悪さ です。 Unity(UGUI)で時間を食う典型的なポイントを挙げると、以下のようになります。 データ前処理がバラバラ :ソートやフィルタが各UIに散らばっていると、UI層が処理待ちになる。 階層構造が深すぎる :1回の更新で大量のTransformを走査、処理も重くバグも出やすい。 アニメーション・状態管理が未抽象化 :新しい状態を追加するたびにコードを壊す。 同じランキングUIでも、ある人は半日で完成、ある人は3日かかる。 その差は、手の速さではなく設計の差 です。 データ層とUI層を明確に分離する ありがちなアンチパターンは、OnClick() の中で データ取得 → オブジェクト生成 → UI更新を全部やるケース。 短期的には動いても、UI改修のたびに全部作り直しになります。 もっと良い設計はこうです👇 DataService / Manager にデータロジックをまとめる UI は DTO(Data Transfer Object) を受け取って表示専念 データ前処理は Python や外部ツールで事前に済ませる こうすると要件変更が来ても、UI側はほとんど触らずに済みます。 データ層が変わってもUIは安定、これが真の開発効率化です。 リスト描画は「オブジェクトプール」が必須 ランキング、インベントリ、スキルリストなど、 毎回 Instantiate + Destroy していると、 処理が重くなるだけでなく、開発時間も無駄に増えます。 対策: オブジェクトプールを構築する : 再利用可能なUI要素をキャッシュして、再描画時はデータだけ差し替え。 シンプルな PoolManager でOK : 使い終わった要素は SetActive(false) して再利用。 ScrollRect の仮想化レンダリングと併用 : 数千件でも軽く動作。 あるプロジェクトでは、インベントリUIの描画時間が 500セルで200ms → プール導入後は30msに短縮。 体感で「カクつき」が消えました。 ...

2025年10月9日 · 1 分 · 133 文字 · 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