SidedProxy: все, что вы хотели узнать о клиент-серверной архитектуре, но боялись спросить
Почему мультиплеер - это сложно
Minecraft умеет в мультиплеер. Если бы это, в него бы, вероятно, не играли. И, если бы не мультиплеер, кода в Minecraft'е было бы порядком меньше.
Если говорить о сетевом коде, его задача - постоянно синхронизировать данные между игроками. Мир, погода, поведение мобов, траектория проджектайлов - у всех все должно быть одинаковым. Это не такая простая архитектурная задача: с одной стороны, нам нужно выполнять всю "общую" логику на стороне сервера, с другой - эта же логика должна быть в синглплеере, где сервера как такого нет. И все это должно быть на основе одного и того же кода, единой системой. Как же это сделать?
Разделяй и властвуй: как усложнить логику, но упростить жизнь
В Minecraft'е не существует синглплеера. Совсем. Это ложь, фикция и вообще они пошутили. Для того, чтобы одна логика обрабатывала и одиночный, и сетевой режимы, каждый раз, когда мы запускаем синглплеер, система в фоне запускает сервер, на котором мы играем одни.
Поэтому когда говорят о сервере и клиенте, речь не про расположение выполняемого кода (на ПК или каком-то, в т.ч. локальном, хостинге), а о том, какая логика выполняется: серверная (промежуточные расчеты, которые не нужны игроку), клиентская (графика, обработка клавиш, звуки) или общая, исполняемая синхронно и там, и там.
Minecraft'у все равно, расположен ли логический сервер на той же машине в другом потоке, или где-то во внешнем интернете: для клиента они практически идентичны, за исключением возможности ставить игру на паузу.
Как же с этим работать?
Когда мы пишем мод, он выполняется и на сервере, и на клиенте. В общем случае код исполняется и там, и там. Но есть ситуации, когда мы хотим выполнить что-то только на одной стороне, будь то, к примеру, генерация случайного ключа на сервере или обработка нажатия клавиши на клиенте. В подобных ситуациях необходимо контролировать, на какой стороне будет выполнятся код.
Forge позволяет проверять, где проходит исполнение, c помощью функций world.isRemote()
для мира и event.getSide()==Side.SERVER
для события, возвращающих true
на сервере на false
на клиенте.
Для того, чтобы код было легче поддерживать и отлаживать, принято разделять логику на отдельные файлы-proxy. ClientProxy и ServerProxy выполняют специфическую логику, CommonProxy же общую. Как и сам мод, proxy должны содержать три функции: preInit()
, init()
и postInit()
, которые будут выполняется на соответствующем этапе инициализации.
Решение "в лоб": пишем ветвление сами
Для того, чтобы лучше понять, как это работает, посмотрим на простейшую реализацию такой системы.
Любой прокси:
public class ...Proxy { public void preInit() { //код } public void init() { //код } public void postInit() { //код } }
ExampleMod.java:
@Mod(modid = "example", version = "1") public class ExampleMod { CommonProxy commonProxy = new CommonProxy(); ServerProxy serverProxy = new ServerProxy(); ClientProxy clientProxy = new clientProxy(); @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { commonProxy.preInit(); if(event.getSide()==Side.CLIENT) clientProxy.preInit(); else serverProxy.preInit(); } @Mod.EventHandler public void init(FMLInitializationEvent event) { commonProxy.init(); if(event.getSide()==Side.CLIENT) clientProxy.init(); else serverProxy.init(); } @Mod.EventHandler public void postInit(FMLPostInitializationEvent event) { commonProxy.postInit(); if(event.getSide()==Side.CLIENT) clientProxy.postInit(); else serverProxy.postInit(); } }
Общий код выполняется во всех случаях, зависящий от стороны - только на своей стороне.
Решение "в лоб" 2.0: добавляем наследование
Решение, приведенное выше, является неоптимальным. Для того, чтобы упростить, воспользуемся ООП: пусть ClientProxy и ServerProxy наследуются от CommonProxy, запуская при этом родительский код.
CommonProxy.java:
public class CommonProxy { public void preInit() { //общий код } public void init() { //общий код } public void postInit() { //общий код } }
Любая из сторон:
public class ...Proxy extends CommonProxy { public void preInit() { super.preInit(); //Специфический код } public void init() { super.init(); //Специфический код } public void postInit() { super.postInit(); //Специфический код } }
В основном моде будем хранить только один прокси, а какой - решать один раз на этапе преинициализации.
ExampleMod.java:
@Mod(modid = "example", version = "1") public class ExampleMod { CommonProxy proxy; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { if(event.getSide()==Side.CLIENT) proxy = new ClientProxy; else proxy = new serverProxy; proxy.preInit(); } @Mod.EventHandler public void init(FMLInitializationEvent event) { proxy.init(); } @Mod.EventHandler public void postInit(FMLPostInitializationEvent event) { proxy.postInit(); } }
Решение в одну строку: все, что вы хотели знать об аннотации @Mod, но боялись спросить
Наше второе решение имеет право на жизнь. Однако прокси сторон - настолько часто используемый шаблон, что разработчики Forge встроили в него готовое решение. Для того, чтобы им воспользоваться, нам не придется менять ничего в самих прокси из второго варианта, но нужно будет сократить код в основном файле.
ExampleMod.java
@Mod(modid = "example", version = "1") public class ExampleMod { @SidedProxy(clientSide = "com.example.examplemod.ClientProxy", serverSide = "com.example.examplemod.ServerProxy") static CommonProxy proxy; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { proxy.preInit(); } @Mod.EventHandler public void init(FMLInitializationEvent event) { proxy.init(); } @Mod.EventHandler public void postInit(FMLPostInitializationEvent event) { proxy.postInit(); } }
Аннотация @SidedProxy()
, принимающая в качестве параметров пакеты-стороны, сама запишет в переменную proxy
тот прокси, который будет соответствовать потоку исполнения и сама его инициализирует.
Код стал максимально коротким и информативным, а логика разделена на три составляющие. Для того, чтобы использовать все возможности разделения серверной и клиентской логики, необходимо также научиться слать сообщения между потоками, но это уже другая история...