
Сюда входят MVC, MVP, MVVM - разные способы разделить игру на три части: данные, отображение, управление.
Допустим, мы пишем игру. Начинаем просто - один класс, в нём координаты игрока, здоровье, обработка клавиш, отрисовка.
class Game:
def __init__(self):
self.player_x = 100
self.player_y = 200
self.player_hp = 100
self.player_sprite = pygame.image.load("player.png")
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.player_x -= 5
if keys[pygame.K_RIGHT]:
self.player_x += 5
# ... 500 more lines for enemies, items, UI
def draw(self, screen):
screen.blit(self.player_sprite, (self.player_x, self.player_y))
pygame.draw.rect(screen, RED, (10, 10, self.player_hp, 20))
# ... 300 more lines
public class MyGame : Game
{
int playerX = 100, playerY = 200, playerHp = 100;
Texture2D playerSprite;
SpriteBatch spriteBatch; // initialized in LoadContent
protected override void Update(GameTime gameTime)
{
var kb = Keyboard.GetState();
if (kb.IsKeyDown(Keys.Left)) playerX -= 5;
if (kb.IsKeyDown(Keys.Right)) playerX += 5;
// ... 500 more lines
}
protected override void Draw(GameTime gameTime)
{
spriteBatch.Draw(playerSprite, new Vector2(playerX, playerY), Color.White);
// health bar, enemies, UI... 300 more lines
}
}
Работает? Работает. Проблема появляется, когда проект растёт. Добавили врагов - update() раздулся. Добавили меню - draw() раздулся. Хотим поменять управление - копаемся в том же файле, где игровая логика и отрисовка. Это god object - классический антипаттерн.
А что, если разделить ответственность?
Model (модель) - хранит данные и бизнес-логику. Координаты, здоровье, столкновения. Ничего не знает про отрисовку и ввод.
View (представление) - отрисовка. Берёт данные из модели и рисует. Не содержит игровой логики.
Controller (контроллер) - обработка ввода. Принимает нажатия клавиш и говорит модели, что делать.

Т.е. вместо одного класса, который делает всё, мы получаем три, каждый со своей зоной ответственности.
class PlayerModel:
def __init__(self, x, y, hp):
self.x = x
self.y = y
self.hp = hp
self.speed = 5
def move(self, dx, dy):
self.x += dx * self.speed
self.y += dy * self.speed
def take_damage(self, amount):
self.hp = max(0, self.hp - amount)
class PlayerView:
def __init__(self, sprite):
self.sprite = sprite
def draw(self, screen, model):
screen.blit(self.sprite, (model.x, model.y))
bar_width = int(model.hp / 100 * 200)
pygame.draw.rect(screen, RED, (10, 10, bar_width, 20))
class PlayerController:
def __init__(self, model):
self.model = model
def handle_input(self):
keys = pygame.key.get_pressed()
dx = keys[pygame.K_RIGHT] - keys[pygame.K_LEFT] # bool: 1 or 0, difference gives -1, 0 or 1
dy = keys[pygame.K_DOWN] - keys[pygame.K_UP]
self.model.move(dx, dy)
# game loop
model = PlayerModel(100, 200, 100)
view = PlayerView(player_sprite)
controller = PlayerController(model)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
controller.handle_input()
view.draw(screen, model)
pygame.display.flip()
public class PlayerModel
{
public float X, Y;
public int Hp;
public float Speed = 5f;
public PlayerModel(float x, float y, int hp)
{
X = x; Y = y; Hp = hp;
}
public void Move(float dx, float dy)
{
X += dx * Speed;
Y += dy * Speed;
}
public void TakeDamage(int amount)
{
Hp = Math.Max(0, Hp - amount);
}
}
public class PlayerView
{
private Texture2D sprite;
private Texture2D pixel; // 1×1 white texture for drawing rectangles
public PlayerView(Texture2D sprite, Texture2D pixel)
{
this.sprite = sprite;
this.pixel = pixel;
}
public void Draw(SpriteBatch sb, PlayerModel model)
{
sb.Draw(sprite, new Vector2(model.X, model.Y), Color.White);
// health bar
int barWidth = (int)(model.Hp / 100f * 200);
sb.Draw(pixel, new Rectangle(10, 10, barWidth, 20), Color.Red);
}
}
public class PlayerController
{
private PlayerModel model;
public PlayerController(PlayerModel model)
{
this.model = model;
}
public void Update()
{
var kb = Keyboard.GetState();
float dx = 0, dy = 0;
if (kb.IsKeyDown(Keys.Right)) dx = 1;
if (kb.IsKeyDown(Keys.Left)) dx = -1;
if (kb.IsKeyDown(Keys.Down)) dy = 1;
if (kb.IsKeyDown(Keys.Up)) dy = -1;
model.Move(dx, dy);
}
}
// game loop
var model = new PlayerModel(100, 200, 100);
var view = new PlayerView(playerSprite, pixel);
var controller = new PlayerController(model);
// in Update():
controller.Update();
// in Draw():
view.Draw(spriteBatch, model);
Теперь каждая часть занимается своим делом:
PlayerModel.PlayerModel.PlayerView.PlayerController.Хотим поменять управление на геймпад? Меняем только контроллер. Хотим другой спрайт? Меняем только представление. Модель не трогаем.
Модель не знает, что существует View или Controller. Она просто хранит данные и предоставляет методы. Это ключевой принцип - модель независима.
В примере выше View получает модель напрямую и сам читает данные - это самый простой вариант (polling). Альтернатива - события: модель публикует событие ("здоровье изменилось"), View подписывается. Это паттерн Observer. Удобно, когда данные меняются редко или нужно уведомить несколько подписчиков.
Мы чётко разграничиваем зоны ответственности. Изменения в одном месте не ломают другое. Каждую часть можно тестировать отдельно - модель без отрисовки, контроллер без модели.
MVC не говорит, как именно реализован PlayerModel внутри. Класс? Структура? ООП? EC/ECS? Это уже отдельный вопрос.
MVP - развитие идеи MVC. Основное отличие: View и Model не общаются напрямую. Вся связь идёт через Presenter.

Грубо говоря, в MVC View мог сам читать данные модели. В MVP - нет. Presenter полностью управляет потоком данных. Это упрощает тестирование: подменяем View на заглушку и тестируем только Presenter + Model.
Ключевое отличие от MVC - PlayerView.show() принимает уже готовые данные, а не объект модели:
class PlayerView:
def __init__(self, sprite):
self.sprite = sprite
def show(self, screen, x, y, hp):
screen.blit(self.sprite, (x, y))
bar_width = int(hp / 100 * 200)
pygame.draw.rect(screen, RED, (10, 10, bar_width, 20))
class PlayerPresenter:
def __init__(self, model, view):
self.model = model
self.view = view
def update(self):
keys = pygame.key.get_pressed()
dx = keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]
dy = keys[pygame.K_DOWN] - keys[pygame.K_UP]
self.model.move(dx, dy)
def render(self, screen):
# Presenter reads from Model and passes to View
self.view.show(screen, self.model.x, self.model.y, self.model.hp)
# game loop
model = PlayerModel(100, 200, 100)
view = PlayerView(player_sprite)
presenter = PlayerPresenter(model, view)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
presenter.update()
presenter.render(screen)
pygame.display.flip()
public class PlayerView
{
private Texture2D sprite;
private Texture2D pixel;
public PlayerView(Texture2D sprite, Texture2D pixel)
{
this.sprite = sprite; this.pixel = pixel;
}
public void Show(SpriteBatch sb, float x, float y, int hp)
{
sb.Draw(sprite, new Vector2(x, y), Color.White);
int barWidth = (int)(hp / 100f * 200);
sb.Draw(pixel, new Rectangle(10, 10, barWidth, 20), Color.Red);
}
}
public class PlayerPresenter
{
private PlayerModel model;
private PlayerView view;
public PlayerPresenter(PlayerModel model, PlayerView view)
{
this.model = model; this.view = view;
}
public void Update()
{
var kb = Keyboard.GetState();
float dx = 0, dy = 0;
if (kb.IsKeyDown(Keys.Right)) dx = 1;
if (kb.IsKeyDown(Keys.Left)) dx = -1;
if (kb.IsKeyDown(Keys.Down)) dy = 1;
if (kb.IsKeyDown(Keys.Up)) dy = -1;
model.Move(dx, dy);
}
public void Render(SpriteBatch sb)
{
// Presenter reads from Model and passes to View
view.Show(sb, model.X, model.Y, model.Hp);
}
}
// in Update():
presenter.Update();
// in Draw():
presenter.Render(spriteBatch);
View больше не знает о существовании модели. Это и есть главное отличие от MVC.
MVVM добавляет концепцию связывания данных (data binding): UI автоматически обновляется при изменении ViewModel, и наоборот.

В MVC и MVP такую связь мы создаём вручную - события, колбэки. В MVVM это автоматизировано фреймворком. Чаще встречается в WPF, UWP, Unity UI Toolkit.
Для проекта на pygame или MonoGame MVVM скорее всего будет избыточным. MVC или MVP - более практичный выбор.
Ок. Грубо говоря, каждый объект мы разбиваем на три части. Но зачем представление чему-то, что не отображается?
Допустим, система сохранений:
Где тут View? Нигде - пользователь не видит процесс сохранения. Model хранит логику сериализации. Controller определяет, когда сохраняться. А представления нет.
MVC применяется к тому, что отображается на экране. Для остального - достаточно модели и контроллера.
| MVC | MVP | MVVM | |
|---|---|---|---|
| Связь View<->Model | View читает Model напрямую | Только через Presenter | Через data binding |
| Кто управляет View? | Controller + View сам | Presenter полностью | ViewModel через привязки |
| Тестируемость | Средняя | Высокая | Высокая |
| Сложность | Низкая | Средняя | Выше (нужен фреймворк) |
| Где применяется | Игры, простые приложения | Мобильные приложения | WPF, Unity UI Toolkit |
Не существует "лучшего" паттерна. Для небольшой игры MVC - самый прямой путь. MVP - если хотите чистого разделения. MVVM - если фреймворк поддерживает data binding.
А как именно реализовать Model внутри - через ООП, EC или ECS, конечные автоматы для управления состояниями сцен - это уже следующий вопрос. MVC говорит что разделять, а не как реализовывать каждую часть.
Подробнее об архитектуре в целом - на отдельной странице.
| Тема | Зачем |
|---|---|
| Архитектура | Общая картина: как связать все части проекта |
| Событийная модель | Observer и события - способ связать Model и View без жёстких зависимостей |
| ООП | Наследование, композиция и когда что выбирать |
| EC и ECS | Альтернативный подход к организации данных внутри Model |
| Конечные автоматы | Управление состояниями сцен и объектов |