Перейти к основному содержимому

Текстовая игра на C#

Текстовая игра на C#

Текстовая игра — это классический формат интерактивного программного обеспечения, в котором взаимодействие с пользователем происходит исключительно через текстовый ввод и вывод. Такие игры не требуют графического интерфейса, а игровой процесс строится на описаниях, выборе действий и логике принятия решений. В среде обучения программированию текстовые игры часто используются как практический способ освоить базовые конструкции языка: циклы, условия, методы, коллекции и работу с пользовательским вводом.

В данном примере реализуется консольная текстовая игра на C# с использованием современных возможностей .NET: асинхронности, объектно-ориентированного подхода, инкапсуляции логики и модульной структуры кода. Игра демонстрирует принципы проектирования небольших приложений, которые можно легко расширять и модифицировать.


Архитектура игры

Текстовая игра состоит из следующих ключевых компонентов:

  • GameLoop — основной цикл игры, управляющий последовательностью событий;
  • GameState — объект, хранящий текущее состояние игры (здоровье героя, уровень, инвентарь и т.д.);
  • Narrator — компонент, отвечающий за вывод текстовых сообщений;
  • PlayerInput — обработчик пользовательского ввода;
  • Scene — абстракция игровой сцены или локации;
  • Choice — вариант действия, доступный игроку в текущей сцене.

Такая структура позволяет отделить логику игры от её представления и упрощает добавление новых сцен, персонажей или механик.


Базовые классы и модели

Класс GameState

public class GameState
{
public string PlayerName { get; set; } = "Игрок";
public int Health { get; set; } = 100;
public int Score { get; set; } = 0;
public List<string> Inventory { get; set; } = new();
public string CurrentSceneId { get; set; } = "start";
}

Этот класс содержит всё, что определяет текущее положение игрока в мире игры. Он передаётся между сценами и изменяется по мере развития сюжета.

Класс Choice

public class Choice
{
public string Text { get; set; } = string.Empty;
public string TargetSceneId { get; set; } = string.Empty;
public Action<GameState>? OnSelect { get; set; }
}

Каждый выбор содержит:

  • текст, который видит игрок;
  • идентификатор следующей сцены;
  • необязательное действие, которое выполняется при выборе (например, изменение здоровья или добавление предмета в инвентарь).

Абстрактный класс Scene

public abstract class Scene
{
public string Id { get; init; } = string.Empty;
public abstract string Description { get; }
public abstract List<Choice> GetChoices(GameState state);
}

Каждая конкретная сцена наследуется от этого класса и реализует два метода:

  • Description — возвращает описание локации;
  • GetChoices — возвращает доступные действия в зависимости от состояния игры.

Реализация конкретных сцен

Стартовая сцена

public class StartScene : Scene
{
public override string Id => "start";
public override string Description =>
"Вы просыпаетесь в тёмной комнате. Перед вами две двери: одна красная, другая синяя.";

public override List<Choice> GetChoices(GameState state)
{
return new()
{
new Choice
{
Text = "Открыть красную дверь",
TargetSceneId = "red_room"
},
new Choice
{
Text = "Открыть синюю дверь",
TargetSceneId = "blue_room"
},
new Choice
{
Text = "Посмотреть инвентарь",
TargetSceneId = "inventory"
}
};
}
}

Сцена красной комнаты

public class RedRoomScene : Scene
{
public override string Id => "red_room";
public override string Description =>
"Вы входите в красную комнату. Здесь жарко, и вы замечаете сундук в углу.";

public override List<Choice> GetChoices(GameState state)
{
var choices = new List<Choice>
{
new Choice
{
Text = "Открыть сундук",
TargetSceneId = "chest",
OnSelect = gameState =>
{
if (!gameState.Inventory.Contains("золотой ключ"))
{
gameState.Inventory.Add("золотой ключ");
gameState.Score += 10;
}
}
},
new Choice
{
Text = "Вернуться в начальную комнату",
TargetSceneId = "start"
}
};

return choices;
}
}

Обратите внимание: при открытии сундука выполняется действие OnSelect, которое проверяет наличие предмета и добавляет его, если его ещё нет. Это предотвращает дублирование и обеспечивает согласованность состояния.

Сцена инвентаря

public class InventoryScene : Scene
{
public override string Id => "inventory";
public override string Description => "Ваш инвентарь пуст.";

public override List<Choice> GetChoices(GameState state)
{
var description = state.Inventory.Count == 0
? "Ваш инвентарь пуст."
: $"В инвентаре: {string.Join(", ", state.Inventory)}.";

// Динамическое обновление описания
_description = description;

return new()
{
new Choice
{
Text = "Назад",
TargetSceneId = state.CurrentSceneId == "inventory" ? "start" : state.CurrentSceneId
}
};
}

private string _description = string.Empty;
public override string Description => _description;
}

Здесь используется простой приём: описание генерируется динамически на основе текущего состояния. Это позволяет отображать актуальную информацию без необходимости хранить её отдельно.


Компонент Narrator

public static class Narrator
{
public static void Say(string message)
{
Console.WriteLine($"\n{message}\n");
}

public static void ShowChoices(List<Choice> choices)
{
for (int i = 0; i < choices.Count; i++)
{
Console.WriteLine($"{i + 1}. {choices[i].Text}");
}
}
}

Narrator отвечает только за вывод. Он не знает ничего о логике игры — это соответствует принципу разделения ответственности.


Обработка ввода

public static class PlayerInput
{
public static async Task<int> ReadChoiceAsync(int maxOptions)
{
while (true)
{
Console.Write("\nВаш выбор (введите номер): ");
var input = await Console.In.ReadLineAsync();

if (int.TryParse(input, out int choice) && choice >= 1 && choice <= maxOptions)
return choice - 1; // индексация с нуля

Console.WriteLine("Неверный ввод. Попробуйте снова.");
}
}
}

Метод ReadChoiceAsync запрашивает у пользователя число, проверяет его корректность и возвращает индекс выбранного варианта. Использование async/await делает код совместимым с потенциально асинхронными источниками ввода (например, сетевыми клиентами в будущем).


Основной игровой цикл

public class GameLoop
{
private readonly Dictionary<string, Scene> _scenes = new();
private readonly GameState _state = new();

public GameLoop()
{
// Регистрация всех сцен
RegisterScene(new StartScene());
RegisterScene(new RedRoomScene());
RegisterScene(new BlueRoomScene());
RegisterScene(new InventoryScene());
RegisterScene(new ChestScene());
// ... другие сцены
}

private void RegisterScene(Scene scene)
{
_scenes[scene.Id] = scene;
}

public async Task RunAsync()
{
Narrator.Say("Добро пожаловать в текстовую игру!");
Console.Write("Введите ваше имя: ");
_state.PlayerName = (await Console.In.ReadLineAsync())?.Trim() ?? "Игрок";

while (_state.Health > 0)
{
if (!_scenes.TryGetValue(_state.CurrentSceneId, out var currentScene))
{
Narrator.Say("Ошибка: неизвестная сцена. Игра завершена.");
break;
}

Narrator.Say(currentScene.Description);
var choices = currentScene.GetChoices(_state);
Narrator.ShowChoices(choices);

var selectedIndex = await PlayerInput.ReadChoiceAsync(choices.Count);
var selectedChoice = choices[selectedIndex];

// Выполнение действия при выборе
selectedChoice.OnSelect?.Invoke(_state);

// Переход к новой сцене
_state.CurrentSceneId = selectedChoice.TargetSceneId;

// Простая проверка завершения
if (_state.CurrentSceneId == "end")
{
Narrator.Say($"Игра окончена! Ваш счёт: {_state.Score}");
break;
}
}

if (_state.Health <= 0)
{
Narrator.Say("Вы погибли. Игра окончена.");
}
}
}

Цикл игры:

  1. Запрашивает имя игрока.
  2. Пока здоровье больше нуля:
    • загружает текущую сцену;
    • выводит описание;
    • показывает варианты выбора;
    • читает ввод;
    • выполняет действие (если есть);
    • обновляет текущую сцену.
  3. Завершает игру при достижении финальной сцены или смерти.

Запуск игры

// Program.cs
var game = new GameLoop();
await game.RunAsync();

В проекте .NET 6+ или выше достаточно поместить этот код в файл Program.cs. Приложение будет работать как полноценная консольная программа.


Расширяемость и модификация

Такая архитектура легко расширяется:

  • Добавление новых сцен: создаётся новый класс, наследующий Scene, и регистрируется в GameLoop.
  • Сложные условия: в GetChoices можно добавлять проверки (if (state.Inventory.Contains("ключ"))).
  • Случайные события: использование Random для генерации разных исходов.
  • Сохранение прогресса: сериализация GameState в JSON и запись в файл.
  • Локализация: вынос текстов в ресурсы или словари.

Пример сохранения:

public static void SaveGame(GameState state, string path)
{
var json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}

Преимущества подхода

  • Читаемость: каждая сцена — самостоятельный класс.
  • Тестируемость: можно писать unit-тесты для GetChoices.
  • Гибкость: логика выбора полностью отделима от вывода.
  • Поддерживаемость: изменения в одной сцене не влияют на другие.

Текстовая игра на C# — это не просто развлечение, а учебный проект, демонстрирующий фундаментальные принципы разработки: инкапсуляцию, абстракцию, управление состоянием и проектирование взаимодействия с пользователем.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).