C#コンソールゲームで学ぶOOP入門: 手続き型とオブジェクト指向の比較
本記事では、C#コンソールアプリを使って、手続き型とオブジェクト指向の違いを楽しく学びます。一次元の“ターゲット”とプレイヤーを操作しながら、コードの扱いやすさや拡張性を体験。チャレンジ課題では、リストに追加するだけで新しいプレイヤーを増やせる実装を紹介します。初心者でも気軽に取り組めるサンプル付きで、OOPの魅力をゆっくり確かめてみましょう。
講師からの口頭依頼
「実装方法はあえて固定せず、自由なアプローチでチャレンジしてみましょう」
コンソールアプリで、一次元空間(X座標のみ)の世界に存在している“ターゲット”(X座標 = 5)と、複数の“プレイヤー”(初期X座標 = 1)を用意してください。
プレイヤー1は D キー、プレイヤー2は S キーを押すと、それぞれ右方向へ X 座標を +1 ずつ移動します。移動後には各プレイヤーの現在の X 座標と、ターゲットまでの距離をコンソールに表示してください。
もしいずれかのプレイヤーがターゲットと同じ X 座標になったら、「爆発!」と表示してアプリケーションを終了します。

チャレンジ課題:まず、上記が動作することが前提です
さらにプレイヤーを増やしたときにはMainメソッドを2、3行程度追加または変更するだけで対応できるようにしてほしいです。
ソリューション名は、ConsoleGameOOPComparisonとして作成してください
キーを押下した時点でキーを知るヒント
要件
- ターゲットは名前「ターゲット」、X座標 = 5
- プレイヤーは以下のとおり
- 「プレイヤー1」: 初期X座標 = 1、移動キー = D
- 「プレイヤー2」: 初期X座標 = 1、移動キー = S
- キー入力(D/S)で各プレイヤーが右に1移動する
- 移動後に以下を表示する
- プレイヤーのX座標
- ターゲットまでの距離(絶対値)
- プレイヤーがターゲットと同じX座標になったら「爆発!」と表示しアプリ終了
- 新規プレイヤーを追加する際は、リストにオブジェクトを追加するだけでMainメソッドを変更不要
1. 手続き型(オブジェクト指向を使わない実装)
using System;
namespace ProceduralDemo
{
class Program
{
static void Main()
{
// ターゲットとプレイヤーを個別の変数で管理
int targetX = 5;
int player1X = 1;
int player2X = 1;
Console.WriteLine("Dキー→プレイヤー1、Sキー→プレイヤー2 を右へ移動");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
// プレイヤー1 の処理
if (key == ConsoleKey.D)
{
player1X += 1;
Console.WriteLine($"プレイヤー1 の X 座標: {player1X}");
Console.WriteLine($"距離: {Math.Abs(targetX - player1X)}");
if (player1X == targetX)
{
Console.WriteLine("爆発!");
break;
}
}
// プレイヤー2 の処理
else if (key == ConsoleKey.S)
{
player2X += 1;
Console.WriteLine($"プレイヤー2 の X 座標: {player2X}");
Console.WriteLine($"距離: {Math.Abs(targetX - player2X)}");
if (player2X == targetX)
{
Console.WriteLine("爆発!");
break;
}
}
}
}
}
}
キー入力と分岐処理の説明
var key = Console.ReadKey(intercept: true).Key;
Console.ReadKey(...)
でキー入力を待機し、返されたConsoleKeyInfo
オブジェクトから.Key
で押されたキーの種類を取得します。intercept: true
を指定すると、押されたキーがコンソール画面に表示されず、ログを汚さずに入力を受け取れます。
if (key == ConsoleKey.D)
は、取得したキーが Dキー であった場合にだけブロック内の処理(プレイヤー1の移動・表示・衝突判定)を実行する条件分岐。- 同様に
else if (key == ConsoleKey.S)
で Sキー の入力を検知し、プレイヤー2の処理を行っています。
- 同様に
問題点
- 冗長:プレイヤーごとに同じロジックをコピー&ペースト
- 拡張性不足:新しいプレイヤーを追加する度にMain内部のif文を増やす必要がある
- 可読性・保守性:ロジックが散らばり、全体像が把握しづらい
2. オブジェクト指向を使った実装
using System;
using System.Collections.Generic;
namespace OOPDemoImproved
{
// ■GameObject クラス:名前・座標・動作だけを管理
class GameObject
{
public string Name { get; }
public int X { get; private set; }
public GameObject(string name, int initialX)
{
Name = name;
X = initialX;
}
// 右へ移動+表示+衝突判定
public void MoveRight(GameObject target)
{
X += 1;
Console.WriteLine($"{Name} の X 座標: {X}");
Console.WriteLine($"距離: {Math.Abs(X - target.X)}");
if (X == target.X)
{
Console.WriteLine("爆発!");
Environment.Exit(0);
}
}
}
class Program
{
static void Main()
{
var target = new GameObject("ターゲット", 5);
var player1 = new GameObject("プレイヤー1", 1);
var player2 = new GameObject("プレイヤー2", 1);
Console.WriteLine("Dキー→プレイヤー1, Sキー→プレイヤー2 を右へ移動");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
ProcessInput(key, player1, player2, target);
}
}
// ★ ここにキー判定+移動処理をまとめる
static void ProcessInput(ConsoleKey key,
GameObject player1,
GameObject player2,
GameObject target)
{
if (key == ConsoleKey.D)
{
player1.MoveRight(target);
}
else if (key == ConsoleKey.S)
{
player2.MoveRight(target);
}
// それ以外のキーは無視
}
}
}
以下のように、キー判定~移動処理を ProcessInput メソッドに切り出すことで、Main メソッドの変更を最小限に抑えられます。
以下、このコードの構成と動作を、前回と同じ観点で解説します。
1. 全体構成
using System;
using System.Collections.Generic;
namespace OOPDemoImproved
{
// … GameObject/Program クラスの定義 …
}
- using System;:Console や Environment を使うため
- using System.Collections.Generic;:List<T> を使うため
- namespace OOPDemoImproved:サンプル用の名前空間
2. GameObject クラス
2.1 プロパティ
名前 | 型 | 説明 |
---|---|---|
Name | string | オブジェクトの名前(読み取り専用) |
X | int | 現在の X 座標(外部からは読み取りのみ) |
2.2 コンストラクタ
public GameObject(string name, int initialX)
{
Name = name;
X = initialX;
}
- インスタンス生成時に名前・初期座標をセット
public void MoveRight(GameObject target)
{
X += 1;
Console.WriteLine($"{Name} の X 座標: {X}");
Console.WriteLine($"距離: {Math.Abs(X - target.X)}");
if (X == target.X)
{
Console.WriteLine("爆発!");
Environment.Exit(0);
}
}
- 移動処理:X += 1 で右に1増やす
- 情報表示
- 自分の現在座標
- ターゲットまでの距離(絶対値)
- 衝突判定:座標が一致したら「爆発!」表示&プログラム終了
3. Program クラスと Main メソッド
static void Main()
{
var target = new GameObject("ターゲット", 5);
var player1 = new GameObject("プレイヤー1", 1);
var player2 = new GameObject("プレイヤー2", 1);
Console.WriteLine("Dキー→プレイヤー1, Sキー→プレイヤー2 を右へ移動");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
ProcessInput(key, player1, player2, target);
}
}
- オブジェクト生成
- target: 初期 X = 5
- player1/player2: 初期 X = 1
- 説明メッセージ:どのキーで動かすかを表示
- 入力ループ
- Console.ReadKey(intercept: true) でキー取得(画面に表示しない)
- 取得した ConsoleKey を ProcessInput へ渡す
4. ProcessInput メソッド
static void ProcessInput(ConsoleKey key,
GameObject player1,
GameObject player2,
GameObject target)
{
if (key == ConsoleKey.D)
{
player1.MoveRight(target);
}
else if (key == ConsoleKey.S)
{
player2.MoveRight(target);
}
// それ以外のキーは無視
}
- キー判定と移動呼び出し
- D → player1.MoveRight(…)
- S → player2.MoveRight(…)
- Main 側は入力取得後、このメソッドを呼ぶだけで済む
5. 学習ポイント
- 責務の分割
- Main:入力ループと制御の流れ
- ProcessInput:キー判定と対応する MoveRight 呼び出し
- GameObject:移動ロジック/表示/衝突判定
- 可読性・保守性の向上
- Main がシンプルになり、どこを変えればよいか明確
- キー追加やプレイヤー数増加は ProcessInput/players 登録部だけ修正
- 拡張性
- 別の入力方法(マウスやゲームパッド)を導入する場合も、ProcessInput を差し替えるだけで済む
このように「入力取得」「判定」「動作呼び出し」「動作本体」を明確に分けることで、初心者でもコードの流れを追いやすく、OOPのメリットを実感できます。
using System;
using System.Collections.Generic;
namespace OOPDemoImproved_other
{
// ■GameObject クラス:名前・座標・操作キー・挙動をまとめる
class GameObject
{
public string Name { get; }
public int X { get; private set; }
public ConsoleKey MoveKey { get; }
public GameObject(string name, int initialX, ConsoleKey moveKey)
{
Name = name;
X = initialX;
MoveKey = moveKey;
}
// キー入力を受けて移動→座標表示→距離計算→衝突判定
public void HandleInput(ConsoleKey key, GameObject target)
{
if (key != MoveKey) return;
X += 1;
Console.WriteLine($"{Name} の X 座標: {X}");
Console.WriteLine($"距離: {Math.Abs(X - target.X)}");
if (X == target.X)
{
Console.WriteLine("爆発!");
Environment.Exit(0);
}
}
}
class Program
{
static void Main()
{
var target = new GameObject("ターゲット", 5, ConsoleKey.NoName);
var player1 = new GameObject("プレイヤー1", 1, ConsoleKey.D);
var player2 = new GameObject("プレイヤー2", 1, ConsoleKey.S);
// プレイヤーをリストでまとめるだけで拡張OK
var players = new List<GameObject> { player1, player2 };
Console.WriteLine("Dキー→プレイヤー1, Sキー→プレイヤー2 を右へ移動");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
foreach (var p in players)
{
p.HandleInput(key, target);
}
}
}
}
}
以下、このコードを以下の観点で解説します。
1. 全体構造
using System;
using System.Collections.Generic;
namespace OOPDemoImproved
{
// … GameObject/Program クラスの定義 …
}
- using System;:基本クラス(Console や Environment)を使うための名前空間。
- using System.Collections.Generic;:ジェネリックコレクション(List<T>)を使うための名前空間。
- namespace OOPDemoImproved:このサンプル全体をまとめる独自名前空間。
2. GameObject クラス
2.1 プロパティ
名前 | 型 | 説明 |
---|---|---|
Name | string | オブジェクトの名前(読み取り専用) |
X | int | 現在の X 座標(外部からは読み取りのみ) |
MoveKey | ConsoleKey | このオブジェクトを動かすキー |
2.2 コンストラクタ
public GameObject(string name, int initialX, ConsoleKey moveKey)
{
Name = name;
X = initialX;
MoveKey = moveKey;
}
- インスタンス生成時に名前・初期座標・操作キーを設定。
- MoveKey によって、どのキーを押すと動かすかをあらかじめ決めておける。
2.3 HandleInput メソッド
public void HandleInput(ConsoleKey key, GameObject target)
{
if (key != MoveKey) return;
X += 1;
Console.WriteLine($"{Name} の X 座標: {X}");
Console.WriteLine($"距離: {Math.Abs(X - target.X)}");
if (X == target.X)
{
Console.WriteLine("爆発!");
Environment.Exit(0);
}
}
- キー判定引数 key が自分の MoveKey と一致しなければ何もしない。
- 移動処理X += 1 で右に1マス移動。
- 情報表示
- 自分の新しい座標を出力
- ターゲットとの距離を絶対値で計算して出力
- 衝突判定座標が等しければ「爆発!」を出力し、アプリケーションを終了。
3. Program クラスと Mainメソッド
static void Main()
{
var target = new GameObject("ターゲット", 5, ConsoleKey.NoName);
var player1 = new GameObject("プレイヤー1", 1, ConsoleKey.D);
var player2 = new GameObject("プレイヤー2", 1, ConsoleKey.S);
var players = new List<GameObject> { player1, player2 };
Console.WriteLine("Dキー→プレイヤー1, Sキー→プレイヤー2 を右へ移動");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
foreach (var p in players)
{
p.HandleInput(key, target);
}
}
}
- オブジェクト生成
- target:移動しない「ゴール」オブジェクト(MoveKey は NoName)
- player1/player2:初期位置1、操作キーは D と S
- リストによる管理players リストにまとめることで、プレイヤーを増やす際もリストに追加するだけで OK。
- 入力→処理のループ
- Console.ReadKey(intercept: true) でキー入力を取得(画面には表示しない)
- リスト内すべての GameObject に対して HandleInput を呼び、キー判定+移動処理
4. この設計の学習ポイント
- OOP(オブジェクト指向)による責務分割
- GameObject:動作ロジック(移動・表示・判定)
- Program.Main:入力ループと実行の流れ
- 拡張性
- プレイヤーを増やしたいときは new GameObject(…) → players.Add(…) するだけ。
- キー割り当てもインスタンスごとに柔軟に設定可能。
- コードの可読性・保守性
- 「何を入れて」「どう動いて」「何を出すか」が メソッド・クラスごとにはっきり分かれる。
- 初学者でも、入力処理と移動処理の役割を追いやすい。
このサンプルをベースに、キーの種類を増やす・UI を変更する・マウス対応にする…といった拡張を行うときも、それぞれの責務が明確なので改修が容易です。
メリット
- 拡張性: 新規プレイヤーは
players.Add(new GameObject(...))
するだけでOK - DRY原則: 移動・表示・衝突判定ロジックを一度だけ実装
- 責務分離: Program は入力の受け渡し、GameObject が振る舞い管理
- 可読性・保守性: 構造がシンプルで、理解と修正が容易
3. 両者の比較
観点 | 手続き型 | オブジェクト指向 |
---|---|---|
拡張性 | if文を追加 | リストにAddするだけ |
冗長度 | コードが重複 | クラスにまとめて使い回し |
責務分担 | Mainに全ロジックが集中 | 入力管理と振る舞い管理で分離 |
保守性・可読性 | オブジェクト増で急激に低下 | 増えても構造はほぼ変わらず |
まとめ
- 手続き型は小規模なら手早いが、規模拡大で変更コストが増大
- オブジェクト指向は再利用性・拡張性・可読性が高く、継続的な開発に強い
- 本例では「プレイヤーを増やしたときの修正箇所の差」でOOPの良さを体感できる
ぜひ、早い段階からオブジェクト指向のメリットを活かした設計を心がけましょう!
ディスカッション
コメント一覧
まだ、コメントがありません