「とりあえず動くけど、読みにくくて無駄な処理が多い…」そんなコードに心当たりはありませんか?バグ修正や機能追加を繰り返すうちに、コードが複雑化し、気づけば誰も触りたくない「ぐちゃぐちゃコード」になっていることがあります。
このページでは、なぜそのようなコードが生まれてしまうのか、そしてどうすれば改善できるのかを、具体的なジャンプ処理の例を通して解説します。重要なのは、「問題の本質」を理解することです。
以下は、あるゲームのキャラクタージャンプ処理の(よくない)例です。一見動いているように見えますが、多くの問題を含んでいます。
void update() {
// ジャンプキーが押された時の処理
if (jumpKeyPressed) {
// まだジャンプしていない場合
if (!isJumping) {
// 地面にいる場合のみジャンプ開始(空中ジャンプ防止)
if (velocityY == 0) {
isJumping = true;
velocityY = -10; // 上向きの初速度
}
} else {
// すでにジャンプ中の場合(キーを離したらジャンプ中断の意図?)
if (isJumping) { // この if(isJumping) は冗長
if (velocityY < 0 && jumpKeyPressed == false) { // この条件、ここにあるべきか?
isJumping = false;
}
}
}
}
// ジャンプ中にキーが離されたらジャンプ終了(上のelse節と重複?)
if (isJumping && velocityY < 0 && jumpKeyPressed == false) {
isJumping = false;
}
// ジャンプ中なら物理演算を適用
if (isJumping) {
positionY += velocityY; // 位置を更新
velocityY += 1; // 重力(下向きの加速度)
}
}
このコードには、条件の重複、冗長なチェック、意図が不明確な分岐が含まれています。なぜこのようなコードになってしまったのでしょうか?
このようなコードは、多くの場合、場当たり的な修正の積み重ねによって生まれます。
| 状態 | 開発者の内心 | 結果として追加されたコード(イメージ) |
|---|---|---|
| 最初の実装 | 「キーが押されたらジャンプさせればいいや」 | if (jumpKeyPressed) { velocityY = -10; } |
| 不具合①発覚 | 「あれ?空中で何回もジャンプできちゃうぞ…」 | ジャンプ中か判定するフラグ isJumping を導入し、if (!isJumping) を追加。 |
| 不具合②発覚 | 「地面にいるときだけジャンプさせないと!」 | 速度が0(地面にいる)かチェックする if (velocityY == 0) を追加。 |
| 不具合③発覚 | 「ジャンプ中にキーを離したら、上昇を止めたいな…」 | else 節や、別の場所に if (jumpKeyPressed == false) の条件を追加。 |
| 不具合④発覚 | 「なんか条件が被ってる?でも直すと怖いし…」 | さらに条件分岐を追加したり、既存の条件を複雑化。 |
| 現在 | 「とりあえず動いてるみたいだから、これでいいか…」 | 意図不明で複雑怪奇なコードの完成。 |
このプロセスで問題なのは、発生した「現象」に対して「対処」するコードを都度追加しているだけで、「ジャンプ」という機能全体の「仕組み」や「状態」を整理・再設計していないことです。
このコードの根本的な問題は、「ジャンプ処理とは何か」「どのような状態があり、どう遷移するのか」という本質を捉えずに、目先のバグだけを潰そうとした点にあります。
結果として、条件が複雑に絡み合い、重複や矛盾が生まれ、コードの可読性とメンテナンス性が著しく低下してしまったのです。
ジャンプ処理の本質(状態と遷移)を捉え、関心事を分離することで、コードは以下のように改善できます。
// --- 状態判定のためのヘルパー関数 ---
bool canStartJump() {
// ジャンプを開始できる条件:キーが押され、ジャンプ中でなく、地面にいる(速度0)
return jumpKeyPressed && !isJumping && velocityY == 0;
}
bool shouldEndJumpEarly() {
// ジャンプを早期終了する条件:ジャンプ上昇中(速度が負)にキーが離された
return isJumping && velocityY < 0 && !jumpKeyPressed;
}
// --- 状態遷移と処理を実行する関数 ---
void startJump() {
// ジャンプ開始処理
isJumping = true;
velocityY = -10; // 上向きの初速度を与える
// (効果音再生などもここで行う)
}
void endJumpEarly() {
// ジャンプ早期終了処理(上昇を止める)
if (velocityY < 0) { // 上昇中なら速度をリセット(例)
velocityY = 0;
}
// isJumping = false; はここではしない(物理演算は継続させるため)
// あるいは上昇力を弱めるなどの調整
}
void applyJumpPhysics() {
// ジャンプ中の物理演算(重力など)
positionY += velocityY;
velocityY += 1; // 重力加速度
// 地面に到達したら着地処理
if (positionY >= groundLevel && velocityY > 0) { // 地面より下に行かず、下降中なら
positionY = groundLevel;
velocityY = 0;
isJumping = false; // ジャンプ状態終了
// (着地エフェクトなどもここで行う)
}
}
// --- メインの更新処理 ---
void update() {
// 1. ジャンプを開始できるかチェックし、可能なら開始する
if (canStartJump()) {
startJump();
}
// 2. ジャンプを早期終了すべきかチェックし、該当すれば終了処理を行う
if (shouldEndJumpEarly()) {
endJumpEarly();
}
// 3. ジャンプ中(または落下中)であれば、物理演算を適用する
if (isJumping) { // isJumping は「空中状態」を示すフラグとして使う
applyJumpPhysics();
}
}
※上記コードは一例であり、実際のゲームループや物理エンジンの設計によって詳細は異なります。(例:isJumping の代わりに isGrounded フラグを使うなど)
改善後のコードは、元のコードのどこがどのように良くなったのでしょうか?
canStartJump(): ジャンプを開始する「条件」を判定する責任。shouldEndJumpEarly(): ジャンプを早期終了する「条件」を判定する責任。startJump(): ジャンプを開始する「アクション」を実行する責任。endJumpEarly(): ジャンプを早期終了する「アクション」を実行する責任。applyJumpPhysics(): ジャンプ中の「物理演算」を実行する責任。if 文のネストがなくなり、update() 関数の処理の流れが明確になりました。「ジャンプ開始判定 -> 早期終了判定 -> 物理演算」というステップが直感的に理解できます。canStartJump() や shouldEndJumpEarly() といった関数名は、その条件が何を意味するのかを自然言語に近い形で表現しており、コードを読むだけで意図が伝わりやすくなっています。isJumping フラグの役割が曖昧でしたが、改善後は「空中状態であること」を示すフラグとしてより明確に使われています。(着地処理も applyJumpPhysics 内で行うことで、状態遷移が整理されました)startJump) と終了 (endJumpEarly, 着地処理) が明確に分離されました。else節と外側のif文)がなくなりました。startJump, canStartJump)を修正すればよく、影響範囲を特定しやすくなります。元のコードのように全体に手を入れる必要性が減ります。これらの改善はすべて、「ジャンプ処理の本質(状態、遷移、条件、アクション)」を理解し、それをコード構造に反映させた結果です。
なぜ、問題の本質を理解せずにコードを書くと、無駄だらけになってしまうのでしょうか?
| 理解不足な点 | コードに起こる問題 | 結果 |
|---|---|---|
| 状態の設計をしていない | どの変数が何を示し、どう変化するのか曖昧になる | 場当たり的なフラグや条件が乱立し、矛盾や重複が発生する |
| 「なぜ」その処理が必要かを考えない | 目の前の現象(バグ)を抑えるためだけの対症療法的なコードになる | 根本原因が解決されず、別の箇所で新たなバグを生む。コードが複雑化する。 |
| 処理の目的や条件を言語化できない | 関数名や変数名が不適切になったり、処理の塊を適切に分割できない | 再利用できず、似たようなコードがコピペされる。修正時にどこを直せばいいか分からない。 |
| 全体の構造を意識しない | 部分的な修正が全体の整合性を破壊する可能性があることに気づかない | 変更に弱く、修正コストが非常に高い「負債コード」が積み上がる。 |
バグ修正や仕様追加に追われると、つい目の前の現象に対処するコードを書きがちです。しかし、一度立ち止まって「この機能の本質は何か?」「どのような状態があり、どう遷移すべきか?」を考えることが、長期的に見て読みやすく、メンテナンス性の高いコードを書くための鍵となります。
無駄のない、意図が明確なコードを目指しましょう。