総合演習 解答¶
結論だけでなく、判断理由・他の正解候補・トレードオフを含めて解説します。自分の解答と「結論」ではなく「根拠」を比較してください。
総合1. 設計レビュー 解答¶
設計の観点:
- 責務の混在: 敵の更新・毒処理・スコア・UI更新・入力・セーブが1クラスに(SRP、神クラス)
- 依存の方向: ゲームロジックがUI(scoreLabel)を直接更新(依存の方向違反)
- static instance: 隠れ依存とテスト不能(Singleton)
C++の正しさの観点(こちらが緊急):
vector<Enemy*>+newで所有権が不明。deleteが見当たらずリーク確定、eraseしてもEnemy本体は解放されない(所有権)- 範囲for中のerase: イテレータ無効化で未定義動作(STL)。「たまに落ちる」の実弾
- fwrite直書きセーブ: パディング・型サイズ依存で互換性が壊れる(セーブデータ)
改善計画(優先度順):
- UBとリークの根絶(即時):
vector<std::unique_ptr<Enemy>>化+削除はerase_if/erase-removeイディオムに。動作を変えずに正しくする - セーブのバージョン化(次のセーブ形式変更前に必須): version+名前付き形式へ移行(不可逆な決定を先に手当て)
- 責務の分離(触る痛みが出た箇所から段階的に): スコアとUIをイベント購読へ、敵更新をEnemy/専用システムへ。一括書き直しはしない(リファクタリング)
別解: 2と3の順序はプロジェクト状況次第(セーブ形式を当分変えないなら3が先)。1が最優先であることだけは動かない(UBは全ての検証を無効にするため)。
総合2. 仕組みの説明 解答¶
- コンパイル時:
std::vector<std::unique_ptr<Enemy>>とstd::unique_ptr<Enemy>、std::make_unique<Boss>がこの翻訳単位で実体化される(実体化)。unique_ptr<Boss>→unique_ptr<Enemy>の変換は派生→基底の暗黙変換として解決される - 実行時のメモリ: vector本体(3ポインタ分)はenemiesの置かれた場所(スタックまたは所有者の中)。要素配列(unique_ptrの列=生ポインタ8バイトずつ ※64bit環境の場合)はヒープ。Boss本体もヒープ(make_uniqueが確保)。Bossは仮想関数を持つのでvptr付き(メモリレイアウト、vtable)
- clear(): 各要素のunique_ptrのデストラクタが順に走り、それぞれが
delete ptrを実行。deleteは基底ポインタ経由なので、Enemyの仮想デストラクタが必須(なければ未定義動作)。vtable経由で~Boss→~Enemyの順に実行され、メモリが解放される。vectorのサイズは0になるが容量(capacity)は残る
総合3. パターン選定 解答例¶
選定(3つ):
- Type Object(対象: 「種類が毎週追加」)— 装備の定義(名前・数値・効果リスト)をデータ化し、追加をデータ作業にする
- 効果リスト(Decoratorのリスト方式)(対象: 「パッシブ効果が重複して乗る」)— 装備・バフを同じ修飾パイプラインに載せる(ステート異常/アイテム効果と同じ基盤)
- Observer/Pub-Sub(対象: 拾得・装備変更をUI・保存が知る)— 装備システムがUIを知らない構造に
[ItemDef(データ)] ←参照─ [装備スロット(所有と着脱ルール)]
│装備/解除で効果リストへ登録/除去
[EffectList] ─参加→ [ステータス計算パイプライン]
│イベント発行
[UI/セーブ(購読)]
使わない候補: 装備品のクラス階層(Sword : Equipment)— 効果が合成で表現できるため階層は不要で、種類追加がコード作業になってしまう(継承かコンポジションか)。別解: 特殊装備(発動型スキル持ち)が多いなら、効果に振る舞いID+登録テーブルを足す(それでも階層は作らない)。
総合4. クロスエンジン設計 解答例¶
対応選定: イベントチャネル → Gameplay Message Subsystem(または自作のGameInstanceSubsystem+MulticastDelegate)。C# event → C++内部完結の購読は非Dynamic Multicast Delegate、デザイナーがBPで購読を足す箇所だけDynamic(UEのDelegate)。ScriptableObjectチャネル → DataAsset+Subsystemの組で近似。
設計変更が必要になる点:
- 購読者の寿命管理: C#はdelegateが購読者を延命するが、UEはUPROPERTYで追跡されない参照はGCに見えず、Actor破棄後の購読が危険。
AddUObject系の自動無効化+EndPlayでのRemoveを規約化(UEのGC) - 例外前提のエラー処理の排除: C#側でtry-catchに頼っていた失敗処理は、UEでは戻り値・check/ensureベースに書き直す(例外、C++例外とC#例外)
総合5. バグ調査総合 解答例¶
症状の読み: 「無関係なデータが化ける」=メモリ破壊系(メモリバグの逆引き表)。「長時間+まれに」=再利用タイミング依存。「リリースのみ」=UBが最適化で顕在化(最適化とUB)。
候補(優先度順):
- use-after-free: 敵死亡時に解放された敵への参照(ターゲット・イベント購読・エフェクトの親)が残り、再利用されたメモリ(UIテキスト!)を書き換えている——「倒した瞬間」「無関係なUI」という症状と完全に整合
- buffer overflow: 死亡演出・ログ処理での配列外書き込み
- (次点)data race: 非同期ロードやオーディオスレッドと絡むなら
検証手段: ASan付きビルドで同シナリオを自動周回(敵を大量に倒すbot)。MinGW等ASan不可環境ならMSVCの/fsanitize=address、またはデバッグヒープ+データブレークポイント(化けるUIテキストのアドレスに書き込み監視 → デバッガ)。
恒久対策: 敵への保存参照を世代付きハンドル/TWeakObjectPtr相当に統一し、死亡通知で参照をクリアする規約。CIにASanテストを常設(品質ツール)。
総合6. ミニプロジェクト 評価観点¶
実装の「正解コード」は一つではありません。以下のチェックリストで自己評価してください。
- [ ] 所有の木が描ける(World→敵/弾プール、借用はハンドルor非所有ポインタで明示)
- [ ] 通知の3分類が守られている(ドロップ・スコア=直呼び、実績=バス、UI・SE=キュー)(イベント通知)
- [ ] ゲームロジックのクラスがRule of Zero(特殊メンバ関数)
- [ ] 読み取り関数・引数がconst(const correctness)
- [ ] 敵30種を想定してもクラスが増えない構造(Type Object)
- [ ] 「この規模なら過剰だった部分」を1つ以上自己申告できる(過剰設計の自覚は設計力の一部)
- [ ]
-Wall -Wextraで警告ゼロ