LINUX.ORG.RU

Правильный MVC для игры


1

2

Есть хитрая настольная игра, хочется сделать её компьютерную реализацию, и хочется сделать её по принцыпам, схожим с MVC - есть модель, описывающая правила и ход игры, есть контроллер, передающий дествия игрока, и есть вид - собственно отображение происходящего в игре. Нужно это чтобы легко можно было менять контроллеры (локальный игрок/удалённый игрок/AI) и виды (GUI/Curses/Web UI, заодно контролировать из модели кому какие данные разрешено видеть). На деле получилось как-то не особо по канонам MVC, в связи с чем следующие непонятки:

1) Взаимодействие модели и контроллера. У игры куча фаз хода игрока, которые могут прерываться другими игроками, и ОЧЕНЬ хочется реализовать это в виде императивного кода, дёргая контроллер когда нужно действие игрока. Т.е.

void Game::Turn() {
  DoFoo();
  m_Controller->AskForAction(SELECT_UNIT);
  DoBar();
  m_Controller->AskForAction(CAST_SPELL);
  DoBaz();
  m_Controller->AskForAction(BUY_STUFF);
  ...
}

допустим, это будет работать с Curses, где в контроллере можно просто ждать нажатия клавиши. Но как быть, например, с GUI, где есть внешний цикл обработки событий? Придётся либо запустить копию цикла в контроллере, что, как мне кажется, криво и скорее всего возможно не во всех тулкитах, либо GUI банально не будет отрисовываться, пока пользователь не нажмёт нужную кнопку. Можно сделать отдельный поток, но это выглядит как костыль. Единственная альтернатива что мне видится - выходить из Turn когда нужно действие и его перезапуск когда действие получено (что-то типа FSM):

RequiredPlayerAction Game::Turn() {
  switch (m_TurnPhase) {
  case 0:
    DoFoo();
    m_Phase = 1;
    return SELECT_UNIT;
  case 1:
    DoBar();
    m_Phase = 2;
    return CAST_SPELL;
  case 2:
    DoBaz();
    m_Phase = 3;
    return BUY_STUFF;
    ...
  }
}

Но так код катастрофически раздувается, теряет наглядность, очень легко ошибиться, и становится невозможно использовать вложенные функции, что, в свою очередь, ещё больше уродует код.

Как бы вы решили задачу, может я упустил какой-нибудь вариант?

2) Взаимодействие модели и вида: push или pull? Вроде в классическом MVC вид просит у модели данные и отображает их. Мне же более удобным видится вариант когда модель при изменении данных пинает вид. Второй вариант выгдятит гораздо лучше потому что взаимодействие происходит через жёстко определённый интерфейс вида, что обеспечивает хорошую абстракцию, тогда как в первом случае придётся либо давать виду const-доступ к кишкам модели, либо гененировать структуру данных, представляющую состояние игры (что по сути просто копия тех же кишок), а структура этих кишок может меняться в процессе разработки модели.

Ну тут вроде всё понятно, но на всяких случай спрошу - push модель это в целом нормально?

★★★★★

> У игры куча фаз хода игрока
Подумайте о контроллере как о стейт-машине. Управляемой моделью. Модель содержит маску допустимых в данный момент действий игрока. Контроллер с одной стороны знает про допустимые сейчас действия - с другой, следит за действиями игрока (и ограничивает эти действия по маске).

По второму - пуш рулит.

svu ★★★★★
()
Ответ на: комментарий от svu

> Подумайте о контроллере как о стейт-машине. Управляемой моделью. Модель содержит маску допустимых в данный момент действий игрока. Контроллер с одной стороны знает про допустимые сейчас действия - с другой, следит за действиями игрока (и ограничивает эти действия по маске).

Ну это-то понятно, вопрос был в другом - модель должна отдать управление чтобы работал GUI, а чтобы это сделать нужно либо отдельный поток, либо делать стейт-машину из модели, что непомерно её усложняет, либо не отдавать и запускать ещё один цикл обработки сообщений, что криво и скорее всего не всегда возможно.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от rival

> * только не мтг, только не мтг *

Не, не мтг :)

slovazap ★★★★★
() автор топика

>Мне же более удобным видится вариант когда модель при изменении данных пинает вид

Делай так как тебе удобно и зажимай себя в рамки паттернов.

AST-PM-105
()

> допустим, это будет работать с Curses, где в контроллере можно просто ждать нажатия клавиши. Но как быть, например, с GUI, где есть внешний цикл обработки событий?

это не должно волновать вообще никак.
придумай абстракцию над любым UI и юзай ее.
и уж тем более контроллер не должны волновать детали реализации вида.

Единственная альтернатива что мне видится - выходить из Turn когда нужно действие и его перезапуск когда действие получено (что-то типа FSM):


бинго, тебе действительно нужен автомат
а выходить из turn'а не нужно, скорее нужно знать куда дальше ;)

Но так код катастрофически раздувается, теряет наглядность, очень легко ошибиться, и становится невозможно использовать вложенные функции, что, в свою очередь, ещё больше уродует код.


напиши правила на каком-нибудь DSL (своем собственном языке)

(если лень писать прямо язык-язык, достаточно набора грамотно подобранных абстракций)

Вроде в классическом MVC вид просит у модели данные и отображает их. Мне же более удобным видится вариант когда модель при изменении данных пинает вид.


самый универсальный вариант - когда за всё отвечает контроллер. В том числе, следит за сообщениями модели и вида. Это требует больше хорошего кода, зато это хорошо работает ;) Пинать из вида сразу модель, имхо, придумали просто для ускорения и упрощения кодинга, а это не твой случай.

но на всяких случай спрошу - push модель это в целом нормально?


если разберешься с разлинеиванием пушей - это даже хорошо

если не разберешься - то сделай как карта ляжет, лишь бы работало :)

stevejobs ★★★★☆
()
Ответ на: комментарий от stevejobs

> это не должно волновать вообще никак.

придумай абстракцию над любым UI и юзай ее.
и уж тем более контроллер не должны волновать детали реализации вида.

Если цикл обработки событий зашит в реализацию UI, абстракцию над таким UI сделать будет в принципе невозможно - в текущем коде мы просто войдём в ожидание события после чего всё умрёт, потому что другие события не будут обрабатываться. Скорее всего и ожидать события не получится. Как пример - GLUT, там glutMainLoop и функции типа onKeyDown. Код заходит в AskForAction() и .. всё.

бинго, тебе действительно нужен автомат
а выходить из turn'а не нужно, скорее нужно знать куда дальше ;)

Можно подробнее? Не вижу как сделать так, чтобы не выходить из Turn'а без второго потока.

slovazap ★★★★★
() автор топика

Я ещё вспомнил про coroutines и continuations - кажется это бы тут помогло: Controller::AskForAction, выздванный из Game::Tuen делает yield и управление попадает в главный цикл, оттуда по приходу события вызывается Controller::ProcessInputEvent, и если оно влияет на игру, опять yield и продолжает выполняться Game::Turn. Учитывая что с корутинами и продолжениями я никогда не работал, как это лучше реализовать, обязательно кросс-платформенно и желательно без тяжёлых зависимостей типа boost? getcontext/makecontext/swapcontext? Как там с производительностью? На каждое переключение будет копироваться стэк?

slovazap ★★★★★
() автор топика

и ОЧЕНЬ хочется реализовать это в виде императивного кода

Может часть логики вынести в какой-нибудь Lua и активно юзать сопрограммы.

//Сам я так не делал. Возможно есть ньюансы.

pathfinder ★★★★
()
Ответ на: комментарий от slovazap

Учитывая что с корутинами и продолжениями я никогда не работал, как это лучше реализовать, обязательно кросс-платформенно

Если сопрограммы делать на сисечке/плюсах, то думаю ССЗБ.

pathfinder ★★★★
()
Ответ на: комментарий от AST-PM-105

> http://ru.wikipedia.org/wiki/%D0%A3%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82...

Ага, я как раз это сейчас читал. Во втором куске кода в первом сообщении как раз оно самое и реализовано, и проблема здесь - невозможность вложенных вызовов.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от pathfinder

> Может часть логики вынести в какой-нибудь Lua и активно юзать сопрограммы.

Мне не нужно активно, мне нужно ровно 2, а Lua будет совершенно избыточной.

Если сопрограммы делать на сисечке/плюсах, то думаю ССЗБ.

А в чём проблема? Не думаю что lua'шные корутины чем-то будут принципиально отличаться от сишных.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от slovazap

> Как пример - GLUT, там glutMainLoop и функции типа onKeyDown. Код заходит в AskForAction() и .. всё.

и всё это сделано только чтобы сэкономить ресурсы :)

Не вижу как сделать так, чтобы не выходить из Turn'а без второго потока.


Долго думал, как бы описать ответ _коротко_.

Ты думаешь в терминах блокирующей неизменяемой логики. Оторвись от нее!

Ты делаешь вызовы методов, это нифиговая оптимизация. Забудь про нее, у тебя случай сложнее :) Думай в терминах сообщений. Нет вызовов методов, есть посылка сообщений.

Например, вместо того, чтобы «рвать turn», ты можешь поставить в очередь сообщений новое сообщение «хочу прервать turn». Когда worker дойдет до этого пункта очереди сообщений, он проанализирует игровую ситуацию, спросит у планировщика (части контроллера), какие действия нужно предпринять и отреагирует.

То, что твоя очередь сообщений (пока что, временно) совпадает с очередью сообщений UI - это явление временное :)
Отдаленная аналогия - ты же не обновляешь состояние игры в зависимости от frame rate / FPS, которое может выдать видеосистема? В плохой игре скорость игры связана с FPS, в чуть более хорошей - render и update - это уже совсем разные процессы.
Отдели свою очередь сообщений от очереди сообщений UI. Синхронизацию между очередями можно сделать поабстрактнее, чтобы можно было переиспользовать, но это не главное. Главное чтобы ТВОЯ игра работала с ТВОЕЙ очередью сообщений, а не с какой-то лажей, навязанной очередным говнотулкитом.

stevejobs ★★★★☆
()
Ответ на: комментарий от slovazap

> Не думаю что lua'шные корутины чем-то будут принципиально отличаться от сишных.

LUA шный код можно генерить на лету, а сишный надо конпелять ;) //КО

stevejobs ★★★★☆
()

1) Примитивнейший вариант при получении события просто устанавливать флаг какой-то, который обрабатывать и сбрасывать в Game::Turn.
2) Не модель должна пинать вид, а контроллер, который собирает данные у модели. Так как ходы обрабатывает контроллер, то изменятся или не изменятся данные его не волнуют пока не начнется следующий тик. Состояние игры обычно хранится в модели, просто нужно геттеры нормальные сделать и интерфейс стандартный.

Tark ★★
()
Ответ на: комментарий от stevejobs

> Думай в терминах сообщений

Каких ещё сообщений? Нет никаких сообщений, очередей и worker'ов и взяться им неоткуда. Есть главный поток выполнения, а мне нужен второй, чтобы удобно было реализовывать игровую логику, при этом создавать pthread'овский поток я не хочу, потому что параллельности никакой нет и это будет избыточно, и прерывать Turn() return'ом я не хочу, потому что с Duff's device не получится реализовать вложенные функции, но как-то прерывать его, очевидно, надо. Итого, я пошел курить *context.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от stevejobs

> LUA шный код можно генерить на лету, а сишный надо конпелять ;) //КО

Мне не нужно генерить LUA'шный код налету.

1) Примитивнейший вариант при получении события просто устанавливать флаг какой-то, который обрабатывать и сбрасывать в Game::Turn.

Перечитываем тред - поток один, Game::Turn должна вернуть управление, а значит надо как-то вернуться в ту-же точку Turn. Как? Duff's device не канает. Видимо, swapcontext.

2) Не модель должна пинать вид, а контроллер

А вот это уже бред. http://ru.wikipedia.org/wiki/MVC -> Наиболее частые ошибки

slovazap ★★★★★
() автор топика
Ответ на: комментарий от slovazap

А в чём проблема? Не думаю что lua'шные корутины чем-то будут принципиально отличаться от сишных.

lua'шные корутины отличаются от сишных только тем, что они гарантированно должны работать и не вызывать никаких «неожиданных поведений». Разница примерно такая, как между шаманством и использованием стандартного функционала.

Хотя,если подумать, тебе вроде не надо писать супер-пупер надежный софт. Так что может и имеет смысл юзать всякие сомнительные методики. В крайнем случае скажешь: «Ну, не получилось.» :)

pathfinder ★★★★
()
Ответ на: комментарий от pathfinder

> lua'шные корутины отличаются от сишных только тем, что они гарантированно должны работать и не вызывать никаких «неожиданных поведений»

Правда заключается в том, что lua написан на C, и lua'шные корутины = сишные корутины, которые, между тем, можно реализовать и без недоvm, недоязыка и всей сопутствующей обвязки.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от slovazap

> Нет никаких сообщений, очередей и worker'ов и взяться им неоткуда.

пока ты их сам не напишешь - действительно неоткуда ;)

и прерывать Turn() return'ом я не хочу


ты мешаешь предметную область с ЯП, на котором программируешь. Это называется «оптимизация». Пока ты хочешь использовать эту оптимизацию, мне нечего посоветовать :(

stevejobs ★★★★☆
()
Ответ на: комментарий от slovazap

>Перечитываем тред - поток один, Game::Turn должна вернуть управление, а значит надо как-то вернуться в ту-же точку Turn. Как? Duff's device не канает. Видимо, swapcontext.
Поток один и что? Получаем флагами все данные от управления и потом обрабатываем их последовательно в функции Turn. Один поток как и должно быть. Все равно 2 раза за ход человек ходить не должен.

А вот это уже бред. http://ru.wikipedia.org/wiki/MVC -> Наиболее частые ошибки

Таким образом Контроллер становится «тонким» и выполняет исключительно функцию связующего звена (glue layer) между отдельными компонентами системы.


Я и написал про связующее звяно. Не должна же модель напрямую обращаться к виду. Иначе нельзя будет поменять модель не меняя вид.

Tark ★★
()
Ответ на: комментарий от slovazap

> А вот это уже бред. http://ru.wikipedia.org/wiki/MVC -> Наиболее частые ошибки

та «ошибка» была написана для случая, когда MVC не умеет быть вложенным. Это относится только к простым архитектурам, типа сайтов, где более сложную архитектуру тупо дольше делать.

stevejobs ★★★★☆
()
Ответ на: комментарий от stevejobs

> ты мешаешь предметную область с ЯП, на котором программируешь. Это называется «оптимизация». Пока ты хочешь использовать эту оптимизацию, мне нечего посоветовать :(

Если вы можете посоветовать лишь абстрактную чушь типа написать свою событийную систему - лучше ничего и не советуйте.

Поток один и что? Получаем флагами все данные от управления и потом обрабатываем их последовательно в функции Turn.

Я уже писал про это 3 раза. Эти «флаги» надо менять много раз за время исполнения функции turn.

Я и написал про связующее звяно. Не должна же модель напрямую обращаться к виду. Иначе нельзя будет поменять модель не меняя вид.

Должна - интерфейс вида жёстко задан, соответственно и модель и вид можно менять независимо сколько угодно. Контроллер с видом в общем случае не связан вообще никак.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от Tark

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

Tark ★★
()
Ответ на: комментарий от slovazap

>Должна - интерфейс вида жёстко задан, соответственно и модель и вид можно менять независимо сколько угодно. Контроллер с видом в общем случае не связан вообще никак.
И все-таки я не понимаю. Вот есть у вас поле очков. Было оно простым текстовым полем, а потом вы переделали его на поле состоящее из битмапов, чтобы была красивая рисовка. Следовательно вывод данных в этом поле уже совсем по-другому. Как вы обеспечиваете универсальный интерфейс для вида без

либо гененировать структуру данных, представляющую состояние игры (что по сути просто копия тех же кишок), а структура этих кишок может меняться в процессе разработки модели.

Tark ★★
()
Ответ на: комментарий от Tark

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

Потому что с одним потоком, мы зависнем навсегда в AskForAction, потому что события ввода не будут генерироваться.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от Tark

> И все-таки я не понимаю. Вот есть у вас поле очков. Было оно простым текстовым полем, а потом вы переделали его на поле состоящее из битмапов, чтобы была красивая рисовка. Следовательно вывод данных в этом поле уже совсем по-другому. Как вы обеспечиваете универсальный интерфейс для вида без

m_pView->UpdateScore(new_score);

КАК его рисовать решает исключительно вид.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от slovazap

>Потому что с одним потоком, мы зависнем навсегда в AskForAction, потому что события ввода не будут генерироваться.
А блин. Я только что догнал, что делает AskForAction, прошу прощения.
Теперь я не понял другое. Если ход привязан к действиям игрока, то почему бы просто не вызывать Turn в обработчиках событий?
Если же он не привязан, тогда наверное стоит в обработчиках событий вызывать функцию меняющую состояние конечного автомата(типа Selling, Running или что у вас там) Player, а потом состояние состояние Player обрабатывать уже в функции Turn.
Может будет всем проще, если вы чуть больше расскажите о механике вашей игры?

Tark ★★
()

Я советую тебе не ломаться, как гимназистка в гусарском кабаке, а задействовать поток. Обоснование:

1) по смыслу подходит: если уж отделять модель так отделять.

2) поток потом можно сделать процессом и перетащить на сервер и брать абонплату^W^W^W

3) глобально и надёжно.

4) если у тебя будут анимации, то будет нелишним дать возможность ИИ думать над ходом лишнюю пару секунд пока человек разглядывает красивости.

И по поводу push/pull: у тебя настолка, значит данных немного и они нужны всегда полностью, значит вью всегда будет запрашивать данные в полном объёме, значит можно сразу их ему слать без всяких запросов.

legolegs ★★★★★
()
Ответ на: комментарий от Tark

Не понял насчёт привязан/не привязан к действиям игрока, но по-моему в первом сообщении я всё описал.

Ну возьмём для примера любую стратегическую игру.

Game::Turn() {
  // Начинается ход игрока
  // Ему добавляется некоторое количество ресурсов
  // Юниты восстанавливают некий процент хитов из-за лечения

  while (1) {
    // Далее, ожидается действие игрока, а он может двигать юнитов, покупать здания, или закончить ход
    action = m_pController->AskForAction(MOVE_UNIT | BUY_BUILDING | END_TURN)
    // который вернётся только когда будет получено одно из указанных действий

    if (action.type == END_TURN) {
      break;
    } else if (action.type == MOVE_UNIT) {
      // передвинуть указанный юнит
    } else if (action.type == BUY_BUILDING) {
      // нужно спросить игрока куда нужно поставить здание
      placeaction = m_pController->AskForAction(PLACE_BUILDING)
      // <поставить здание куда указано>
    }
  }
}

Как видно, логика описывается простым последовательным кодом, что очень удобно, читабельно и важно, потому что на деле логика куда сложнее.

Почему это не работает: в случае интерактивного GUI в программе уже есть цикл обработки событий скрытый от пользователя, и из него очевидно (возможно, в обработчике нажатия кнопки «New Game» в главном меню) вызвана Game::Turn. Чтобы m_pController->AskForAction как-то получил событие от игрока, надо вернуться в цикл обработки сообщений, а значит сделать return из Game::Turn(). Проблема во-первых, в том, как сделать return (а Turn на деле - это иерархия функций, так что только исключением что криво или проверкой возвращаемого значения каждой подфункции и каскадный return), во-вторых, в том, как вернуться в то же место когда событие будет получено. Из-за нелинейной структуры duff's device, который примерно реализован во втором куске кода из первого сообщения, использовать невозможно.

Альтернативные решения:

- заново запустить цикл обработки сообщений в pController->AskForAction. Криво, и может не поддерживаться всеми GUI тулкитами.

- использовать потоки. Не очень хочется, потому что никакой параллельности тут нет -> потоки избыточны

- К чему я склоняюсь: использовать swapcontext, который по сути делает то что мне нужно - сохраняет контекст исполнения, с помощью чего можно выпрыгивать из Game::Turn в главный цикл и при получении нужного события возвращаться на то же место Game::Turn, откуда выпрыгнули. Единственный минус - я не вижу этих функций в хидерах mingw, значит это не портабельно.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от legolegs

> Я советую тебе не ломаться, как гимназистка в гусарском кабаке, а задействовать поток. Обоснование:

Это в целом тоже вариант - скорее всего, с потоком будет меньше всего геморроя. На самом деле я сейчас понял что в итоге мне надо было просто прочитать про duff's device и понять что его тут использовать нельзя (из-за иерархии), а значит вариантов организации кода Game кроме первого не предвидится, а значит можно дальше спокойно реализовывать игровую логику обычным императивным кодом без извращений, а в конкретной реализации контроллера/вида использовать уже что удобно - потоки ли, корутины ли или очередь сообщений от тулкита - менять Game в любом случае не придётся.

И по поводу push/pull: у тебя настолка, значит данных немного и они нужны всегда полностью

Не всегда полностью, есть данные игроков которые другим знать не положено.

значит вью всегда будет запрашивать данные в полном объёме, значит можно сразу их ему слать без всяких запросов.

Мне не нравится гонять постоянно кучу неизменившихся данных, и мне не нравится что модель будет определять структуру этих данных, значит при изменении модели придётся изменять и вид. То ли дело

model->UnitMoved(Position, Position) model->BuildingPlaced(BuildingType, Position)

когда надо. Ещё один аргумент - в виде скорее всего будет использоваться своё представление игрового поля (например, игрок расположит карты как ему больше нравится), которое никак не связано собственно с игрой, а 2 преобразования всех данных игры (Кишки модели -> Промежуточное представление с якобы фиксированной структурой -> Кишки вида) это ужасно, когда на деле это всего маленькое событие.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от slovazap

> либо делать стейт-машину из модели, что непомерно её усложняет
Я в игрушечном деле чайник, но я не вижу, почему усложнение будет непомерным. ИМХО там ей самое место.

svu ★★★★★
()
Ответ на: комментарий от legolegs

Навскидку - состояния являются маской разрешенных действий пользователя. Те самые SELECT_UNIT, CAST_SPELL, BUY_STUFF. Ведь никто же не сказал, что в конкретный момент можно выполнять только одно действие? И вот контроллер должен адаптировать интерфейс под разрешенные в данный момент действия.

Это прикидочно. Я же ненастоящий сварщик.

svu ★★★★★
()

Сейчас набигут лисперы, объяснят тебе, что MVC — маркетинговый баззворд, всё надо делать на аппликативных функторах, игры не нужны, да и ты сам не нужен.

// тред не читал

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.