Ответ 1
(Отказ от ответственности: я никогда не программировал игры на Java, только на С++. Но общая идея должна быть применима и на Java. Идеи, которые я представляю, не являются моими собственными, но месиво решений, которые я нашел в книгах или "в Интернете", см. В разделе ссылок. Я использую все это сам, и до сих пор это приводит к чистому дизайну, где я точно знаю, где добавить новые функции, которые я добавляю.)
Я боюсь, что это будет длинный ответ, это может быть неясно при чтении в первый раз, так как я не могу описать его просто сверху вниз очень хорошо, поэтому будут ссылки туда и обратно, это из-за моих недостающих объяснений навыков, а не потому, что дизайн является ошибочным. Оглядываясь назад, я преувеличиваю и даже не в тему. Но теперь, когда я написал все это, я не могу заставить себя просто выбросить его. Просто спросите, что-то неясно.
Прежде чем приступать к разработке любого из пакетов и классов, начните с анализа. Какие функции вы хотите иметь в игре. Не планируйте "возможно, я добавлю это позже", потому что почти наверняка решения по дизайну, которые вы делаете, прежде чем вы начнете добавлять эту функцию всерьез, заглушкой, которую вы запланировали для нее, будет недостаточной.
И для мотивации, я говорю из опыта здесь, не думайте о своей задаче как написании игрового движка, пишите игру! Независимо от того, что вы думаете о том, что было бы здорово иметь для будущего проекта, отклоните его, если вы не поместите его в игру, которую вы сейчас пишете. Отсутствие непроверенного мертвого кода, отсутствие проблем с мотивацией из-за невозможности решить проблему, которая даже не является проблемой для ближайшего проекта. Идеального дизайна нет, но есть один хороший. Стоит иметь в виду это.
Как было сказано выше, я не считаю, что MVC пригодится при разработке игры. Разделение модели/вида не является проблемой, а материал контроллера довольно сложный, слишком много, чтобы его просто называли "контроллером". Если вы хотите иметь подпакеты с именем model, view, control, идите вперед. В эту схему упаковки можно включить следующее: хотя другие, по крайней мере, разумны.
Трудно найти отправную точку в моем решении, поэтому я просто начинаю топ-самый:
В основной программе я просто создаю объект Application, запускаю его и запускаю. Приложение init()
создаст серверы функций (см. Ниже) и добавит их. Также создается первое игровое состояние и нажимается сверху. (см. ниже)
Функциональные серверы инкапсулируют функции ортогональной игры. Они могут быть реализованы независимо и слабо связаны сообщениями. Примеры функций: звук, визуальное представление, обнаружение столкновения, искусственный интеллект/принятие решений, физика и т.д. Как организованы функции, описанные ниже.
Вход, поток управления и игровой цикл
Игровые состояния представляют собой способ организации управления вводом. Обычно у меня есть один класс, который собирает входные события или записывает входное состояние и затем опросает его (InputServer/InputManager). При использовании подхода, основанного на событиях, события присваиваются одному зарегистрированному состоянию активной игры.
При запуске игры это будет основное состояние игры в меню. Состояние игры имеет функцию init/destroy
и resume/suspend
. init()
будет инициализировать состояние игры, в случае главного меню он отобразит верхний уровень меню. Resume()
даст управление этому состоянию, теперь он принимает входные данные с InputServer. Suspend()
очистит вид меню с экрана, а destroy()
освободит любые ресурсы, необходимые для главного меню.
GameStates могут быть уложены в стек, когда пользователь запускает игру, используя опцию "новая игра", тогда состояние игры MainMenu приостанавливается, и PlayerControlGameState будет помещен в стек и теперь получает входные события. Таким образом, вы можете обрабатывать ввод в зависимости от состояния вашей игры. Когда только один контроллер активен в любой момент времени, вы значительно упрощаете поток управления.
Коллекция ввода запускается игровым циклом. Игровой цикл в основном определяет время кадра для текущего цикла, обновляет функциональные серверы, собирает входные данные и обновляет состояние игры. Время фрейма либо задается для функции обновления каждого из них, либо предоставляется одноточечным таймером. Это каноническое время, используемое для определения продолжительности времени с момента последнего вызова обновления.
Игровые объекты и функции
Сердцем этого дизайна является взаимодействие игровых объектов и функций. Как показано выше, функция в этом смысле представляет собой функциональность игры, которая может быть реализована независимо друг от друга. Игровой объект - это все, что взаимодействует с игроком или любыми другими игровыми объектами. Примеры: сам аватар игрока является игровым объектом. Факел - игровой объект, NPC - игровые объекты, такие как зоны освещения и источники звука или любая их комбинация.
Традиционно игровые объекты RPG являются высшим классом некоторой сложной иерархии классов, но на самом деле этот подход просто неверен. Многие ортогональные аспекты не могут быть помещены в иерархию и даже с использованием интерфейсов, в конце концов вы должны иметь конкретные классы. Элемент представляет собой игровой объект, элемент с возможностью выбора - игровой объект, в котором сундук представляет собой предмет, но создание сундука или нет - это либо решение с таким подходом, как если бы у вас был один иерархия. И это становится более сложным, когда вы хотите иметь разговорную волшебную сундук, который открывается только тогда, когда на загадку отвечают. Нет единственной подходящей иерархии.
Лучший подход состоит в том, чтобы иметь только один класс игрового объекта и поместить каждый ортогональный аспект, который обычно выражается в иерархии классов, в свой собственный класс компонентов/объектов. Может ли игровой объект удерживать другие предметы? Затем добавьте ContainerFeature к нему, поговорите, добавьте в него TalkTargetFeature и т.д.
В моем проекте GameObject имеет только уникальный уникальный идентификатор, имя и местоположение, все остальное добавляется как компонент компонента. Компоненты можно добавлять во время выполнения через интерфейс GameObject, вызывая addComponent(), removeComponent(). Чтобы сделать его видимым, добавьте VisibleComponent, сделайте его звуком, добавьте AudibleComponent, сделайте его контейнером, добавьте ContainerComponent.
VisibleComponent важен для вашего вопроса, так как это класс, который обеспечивает связь между моделью и представлением. Не все нуждается в взгляде в классическом смысле. Зона триггера не будет видна, окружающая звуковая зона тоже не будет. Будут видны только игровые объекты, имеющие VisibleComponent. Визуальное представление обновляется в основном цикле, когда обновляется VisibleFeatureServer. Затем он обновляет представление в соответствии с зарегистрированными на него VisibleComponents. Независимо от того, запрашивает ли он состояние каждой или только очереди сообщений, полученных от них, зависит от вашего приложения и базовой библиотеки визуализации.
В моем случае я использую Ogre3D. Здесь, когда VisibleComponent привязан к игровому объекту, он создает SceneNode, который привязан к графу сцены, и к сцене node Entity (представление 3d-сетки). Каждый TransformMessage (см. Ниже) обрабатывается немедленно. Затем VisibleFeatureServer делает Ogre3d перерисовыванием сцены в RenderWindow (по сути, детали сложнее, как всегда)
Сообщения
Итак, как эти функции, игровые состояния и игровые объекты общаются друг с другом? Через сообщения. Сообщение в этом проекте - это просто любой подкласс класса Message. Каждое конкретное сообщение может иметь собственный интерфейс, удобный для своей задачи.
Сообщения могут быть отправлены из одного GameObject в другие GameObjects, из GameObject в его компоненты и из FeatureServers в компоненты, за которые они отвечают.
Когда FeatureComponent создается и добавляется в игровой объект, он регистрируется в игровом объекте, вызывая myGameObject.registerMessageHandler(это MessageID) для каждого сообщения, которое он хочет получить. Он также регистрируется на своем функциональном сервере для каждого сообщения, которое он хочет получить оттуда.
Если игрок пытается поговорить с символом, который у него есть в фокусе, тогда пользователь каким-то образом вызовет действие разговора. Например: Если фокус char является дружественным NPC, то нажатием кнопки мыши запускается стандартное взаимодействие. Требование стандартного действия целевых игровых объектов запрашивается путем отправки ему GetStandardActionMessage. Целевой игровой объект получает сообщение и, начиная с первого зарегистрированного, уведомляет о своих компонентах компонента, которые хотят знать о сообщении. Первый компонент для этого сообщения затем установит стандартное действие на тот, который будет запускаться сам (TalkTargetComponent установит стандартное действие на Talk, которое он получит слишком первым), а затем отметьте сообщение как потребленное. GameObject будет тестировать потребление и видеть, что он действительно потребляется и возвращается к вызывающему. Затем измененное сообщение затем оценивается и результирующее действие вызывает
Да, этот пример кажется сложным, но он уже является одним из наиболее сложных. Другие, такие как TransformMessage для уведомления об изменении позиции и ориентации, легче обрабатывать. TransformMassage интересен многим функциональным серверам. VisualisationServer нуждается в обновлении визуального представления GameObject на экране. SoundServer для обновления положения 3D-звука и т.д.
Преимущество использования сообщений, а не методов вызова должно быть ясным. Между компонентами существует более низкая связь. При вызове метода вызывающий должен знать вызываемого абонента. Но, используя сообщения, это полностью развязано. Если нет приемника, то это не имеет значения. Также, как получатель обрабатывает сообщение, если вообще не является проблемой вызывающего. Возможно, делегаты - хороший выбор здесь, но Java пропускает чистую реализацию для них, и в случае сетевой игры вам нужно использовать какой-то RPC, который имеет довольно высокую задержку. И низкая латентность имеет решающее значение для интерактивных игр.
Сохранение и сортировка
Это приводит нас к тому, как передавать сообщения по сети. Инкапсулируя взаимодействие GameObject/Feature с сообщениями, нам остается только беспокоиться о том, как передавать сообщения по сети. В идеале вы приносите сообщения в универсальную форму и помещаете их в пакет UDP и отправляете его. Приемник распаковывает сообщение в экземпляр соответствующего класса и направляет его на приемник или передает его в зависимости от сообщения. Я не знаю, соответствует ли Java встроенная сериализация. Но даже если нет, существует множество библиотек, которые могут это сделать.
GameObjects и компоненты делают свое постоянное состояние доступным через свойства (С++ не имеет встроенной Serialization). У них есть интерфейс, похожий на PropertyBag на Java, с которым их состояние можно восстановить и восстановить.
Ссылки
- The Brain Dump: блог профессионального разработчика игр. Также авторы двигателя Nebula с открытым исходным кодом, игровой движок, используемый в коммерчески успешных играх. Большая часть представленного здесь проекта взята из слоя приложения туманности.
- Примечательная статья в этом блоге, в ней описывается прикладной уровень движка. Другой подход к тому, что я пытался описать выше.
- Длительное обсуждение о том, как выложить игровую архитектуру. В основном Ogre конкретный, но достаточно общий, чтобы быть полезным и для других.
- Еще один аргумент для проектов на основе компонентов, с полезными ссылками внизу.