Как избежать дублирования с использованием модели клиент-сервер

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

Однако есть определенные вещи, которые должны выполняться только клиентами (например, отправлять пользовательский ввод на сервер, обновлять положение камеры, воспроизводить звуки и т. д.) и некоторые вещи, которые должны выполняться только сервером (например, отправлять сообщения клиентам, удаление объектов и т. д.).

Как я могу поделиться одним и тем же кодом между моим клиентом и сервером, не нагромождая его «если клиент», «если сервер» и т. д.?

EDIT: Было высказано предположение, что я должен извлечь весь общий код в библиотеку и иметь два отдельных проекта - клиент и сервер - каждый из которых должен распространять общий код, чтобы включить пользовательскую функциональность , Но не приведет ли это к многому дублированию?

Чтобы взять пример: допустим, у меня есть класс Entity. Всякий раз, когда Entity перемещается, он должен воспроизводить звук, если он находится на клиенте, и отправлять TCP-сообщение, если оно находится на сервере. Поэтому я создаю еще два класса: ClientEntity и ServerEntity, которые расширяют Entity. Теперь внезапно мой класс Zombie, который ранее расширил Entity, необходимо дублировать по клиентским и серверным проектам, чтобы клиентская версия могла расширить ClientEntity, а версия сервера может расширить ServerEntity.

Если у меня много классов, которые расширяют Entity, это приводит к большому количеству дублирования. Что мне здесь не хватает? :)

5 голосов | спросил Dan 18 22014vEurope/Moscow11bEurope/MoscowTue, 18 Nov 2014 12:08:02 +0300 2014, 12:08:02

4 ответа


10

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

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

Похоже, вы используете жесткий код play_sound(), например:

(моя Java ржавая, пожалуйста, извините, что я пишу эти примеры в Python)

class Entity():
    def move(self):
        # update entity's position
        game_engine.play_sound("entity_moved.wav")

entity = Entity()
entity.move()  # will use the game engine's sound engine

Это заставляет вас всегда использовать метод движка play_sound. Вы могли бы подклассировать сущности, как вы сказали, но есть лучший способ!

Компонентный шаблон (инъекция зависимостей a.k.a.)

Решение состоит в параметризации звукового компонента в вашем классе Entity, a la Компонентный шаблон . Когда вы инициализируете новый Entity, вы передаете конструктору звуковой движок, который вы хотите использовать.

class Entity():
    def __init__(self, sound_engine):
        self.sound_engine = sound_engine

    def move(self):
        # update entity's position
        self.sound_engine.play_sound("entity_moved.wav")

entity = Entity(game_engine.sound_engine)
entity.move()  # will still use the game engine's sound engine

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

# On the server
class ServerSoundEngine():
    def play_sound(self, sound_file):
        notify_clients(sound_file)

entity = Entity(ServerSoundEngine())
entity.move()  # will notify clients to play the sound, instead of playing the sound itself

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

def initialize_game():
    if is_client:
        config["sound_engine"] = game_engine.sound_engine
    else:
        config["sound_engine"] = ServerSoundEngine()

# ...

entity = Entity(config["sound_engine"])

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

Эта идея может быть использована и для других ваших примеров: вместо ветвления на is_client /is_server, вы можете инициализировать игру с помощью RealCamera или FakeCamera. Инициализируйте настоящий класс камеры на клиентах или фиктивный класс с методами no-op, когда вы находитесь на сервере, и вуаля! Вы четко отделили то, что клиент делает от того, что делает сервер, достигли высокого повторного использования кода и не имеют ветвей кода после инициализации игры.

Как статья предлагает , вы «Найдите компонентный шаблон полезным во многих других областях вашей игры.

Чтобы лучше понять шаблон компонента, здесь Санди Мец на RailsConf 2015, дающий супер-крутой разговор на инъекции зависимостей в качестве альтернативы наследованию.

Шаблон наблюдателя

У вас также есть возможность сделать этот шаг дальше и полностью обойти проблему «настроить класс». Ваши звуковые события могут быть хорошим кандидатом для шаблона наблюдателя .

С помощью шаблона Observer ваш код Entity будет транслировать событие (например, ZOMBIE_MOVED), которые ваша игра может обнаруживать и обрабатывать в более поздней точке при обновлении фрейма. Это не только отделяет ваш Entity от любого конкретного звукового движка, но также от любого конкретного звукового API. Это фактически отделяет ваш Entity от полного воспроизведения звука - Entity логика движения больше не должна знать, что воспроизведение звука является побочным эффектом перемещения. Это упрощает ваш код движения и более четко определяет обязанности вашего кода иясно.

С помощью этого шаблона вам больше не нужно будет настраивать объекты с помощью sound_engine. Кроме того, при написании модульных тестов вам больше не придется писать класс класса mock - вы можете просто утверждать, что событие было выпущено. Все это происходит за счет наличия еще одной движущейся части в вашей игре и еще одного слоя абстракции.

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

ответил spiffytech 20 42014vEurope/Moscow11bEurope/MoscowThu, 20 Nov 2014 18:10:32 +0300 2014, 18:10:32
2

Самый чистый способ - создать «основной» проект, который включает общий игровой код.

Вы можете использовать либо интерфейсы, либо абстрактные классы, чтобы подготовить там свой игровой код, и использовать наследование [1] для расширения /реализации этих классов либо на стороне сервера, либо на стороне клиента.

Таким образом, вам не нужны какие-либо операторы if, чтобы различать код клиента и сервера.

Вы также должны убедиться, что не полагаетесь на сложные библиотеки в этих основных классах (т. е. там не должно быть никакого изображения -> thats для клиента, или db stuff -> thats server only). Если у вас есть классы игровых данных, которые как клиент, так и сервер могут /должны иметь совместно используемый аспект, сделать его абстрактным или использовать интерфейс.

[1] http://www.homeandlearn.co.uk/java/java_inheritance .html

ответил Niko 18 22014vEurope/Moscow11bEurope/MoscowTue, 18 Nov 2014 14:40:12 +0300 2014, 14:40:12
2

Извлеките любой общий код в библиотеку, которую вы поддерживаете отдельно.

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

ответил Philipp 18 22014vEurope/Moscow11bEurope/MoscowTue, 18 Nov 2014 15:03:42 +0300 2014, 15:03:42
0

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

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

Это хорошо работает для более 60 различных игр на Boardspace.net

ответил ddyer 24 12014vEurope/Moscow11bEurope/MoscowMon, 24 Nov 2014 22:49:58 +0300 2014, 22:49:58

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132