MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square

в 8:11, , рубрики: c++, client, java, MMO, netty, open source, UE4, Unreal Engine, разработка игр

Всем привет! В предыдущей части мы разобрались с базовой архитектурой, сетью и обменом сообщениями. Нарастим теперь функционал. Сделаем возможность войти, зарегистрироваться получив при этом сессионный id, который можно в будущем использовать для управления клиентом в процессе игры. Далее мы добавим чат, по сути все работает по его принципу: получили сообщение — разослали подписантам. Сделаем возможность создавать игровые комнаты, где будем собирать игроков и отправлять в бой. Синхронизировать перемещение клиентов и напоследок проверять выстрел на проверочном сервере. Будет много кода, я продолжаю пошаговое описание, чтобы можно было быстро разобраться и воспроизвести для своих нужд. Для тех, кто не знаком с первой частью, но хочет вынести для себя что-то полезное здесь и сейчас, я добавил реализацию алгоритма генерации фрактальных ландшафтов Diamond Square, в начало. Happy coding!

Часть 1. Общая картина, сборка библиотек, подготовка клиента и сервера к обмену сообщениями
Часть 2. Наращивание игрового функционала + алгоритм Diamond Square

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 1

Алгоритм Diamond Square и Unreal Engine

Diamond square даёт один из самых реалистичных результатов. Ландшафты, получающиеся с его помощью, как правило, называют фрактальными. Различные реализации алгоритма используются в программах, таких как terragen.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 2

Описать алгоритм можно в пяти шагах:

  1. Инициализация угловых точек. Присваивание им значений высот выбором случайных чисел.
  2. Нахождение срединной точки, присваивание ей значения, на основе среднего от угловых, плюс случайное число.
  3. Нахождение срединной точек для ромбов, отмеченных черными точками (на этом шаге по одной точке каждого ромба выходят за пределы массива).
  4. Для каждого квадрата (на этом шаге их 4), повторяем шаг № 2.
  5. Повторяем шаг № 3 для каждого ромба. У ромбов, имеющих точки на краю массива, одна из точек выходит за пределы массива.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 3

Реализацию вы можете найти тут:

github.com/VadimDev/Unreal-Engine-diamond-square-algorithm

Пару слов о том, как я отрисовал его в движке. Один из вариантов был создать все, использовав DrawDebugLine, однако, это не работает в запакованной игре, и к тому же для каждой линии запускается таймер с её временем жизни, что создаёт дополнительную нагрузку. Чтобы нарисовать линии или создать Mesh в runtime, нужно создать свой UPrimitiveComponent, затем в нем класс производный от FPrimitiveSceneProxy, у которого мы переопределим функцию GetDynamicMeshElements (или DrawDynamicElements, если вызвать и отрисовать нужно один раз). В этой функции есть доступ к FPrimitiveDrawInterface, которая позволяет рисовать различные примитивы. Переопределяем FPrimitiveSceneProxy* CreateSceneProxy() в UPrimitiveComponent и возвращаем оттуда экземпляр вложенного класса производного от FPrimitiveSceneProxy. Чтобы использовать класс, необходимо создать BP актора и присвоить ему созданный компонент.

Алгоритм «diamond-square» для построения фрактальных ландшафтов

За дело! Регистрация и вход

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

github.com/VadimDev/Spiky-Project

Начнем с реализации интерфейса и функционала, формы регистрации и входа. Для начала создадим их вид в визуальном UMG редакторе.

Создадим папку Blueprints, в ней Widgets. За основу я всегда беру HorizontalBox/VerticalBox, в котором может быть ScaleBox с разными параметрами. Как показала практика, это лучший вариант автомасштабирования для разных экранов. Интерфейс имеет множество вложений и сам по себе довольно сложный. Для тестов полезно иметь временный виджет с корнем Canvas, на него можно добавить созданный виджет и растягивать, наблюдая за масштабированием.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 4

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

Теперь к логике, нам нужно привязать интерфейс к коду, создаём для каждого виджета класс наследованный от UserWidget класс.

Extend UserWidget for UMG Widgets
docs.unrealengine.com/latest/INT/Programming/Slate
docs.unrealengine.com/latest/INT/Programming/Tutorials/UMG

Откроем Spiky_Server.Build.cs и добавим новые модули необходимые для работы с UI:

PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" });

Создадим папку UI и разместим там заголовки и реализации заглушки:

Виджеты регистрации, входа, виджет настройки адреса сервера и виджет экрана ожидания

LoginWidgets

LoginWidgets.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include <string>
#include "LoginWidgets.generated.h"

class UButton;
class UTextBlock;
class UEditableTextBox;

UCLASS()
class SPIKY_CLIENT_API ULoginWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	bool bMailOk = false;
	bool bPassOk = false;

public:

	UButton* wSingUpButton = nullptr;
	UTextBlock* wInfoBlock = nullptr;

	UEditableTextBox* wMailTextBox = nullptr;
	UEditableTextBox* wPasswordTextBox = nullptr;

	UButton* wLoginButton = nullptr;
	UButton* wSettingsButton = nullptr;

	UFUNCTION()
	void SettingsButtonClicked();

	UFUNCTION()
	void SingUpButtonClicked();

	UFUNCTION()
	void LoginButtonClicked();

	UFUNCTION()
	void OnMailTextChanged(const FText & text);

	UFUNCTION()
	void OnPasswordTextChanged(const FText & text);

	FTimerHandle MessageTimerHandle;
	void HideErrorMessage();
	void ShowErrorMessage(FString msg);

	static std::string mail;
	static std::string password;
};

LoginWidgets.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "LoginWidgets.h"

std::string ULoginWidgets::mail = "";
std::string ULoginWidgets::password = "";

void ULoginWidgets::NativeConstruct()
{
	Super::NativeConstruct();
}

void ULoginWidgets::LoginButtonClicked()
{
}

void ULoginWidgets::SettingsButtonClicked()
{
}

void ULoginWidgets::SingUpButtonClicked()
{
}

void ULoginWidgets::HideErrorMessage()
{
}

void ULoginWidgets::ShowErrorMessage(FString msg)
{
}

void ULoginWidgets::OnMailTextChanged(const FText & text)
{
}

void ULoginWidgets::OnPasswordTextChanged(const FText & text)
{
}

RegWidgets

RegWidgets.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "RegWidgets.generated.h"

class UButton;
class UImage;
class UEditableTextBox;
class UTextBlock;
class UTexture2D;

UCLASS()
class SPIKY_CLIENT_API URegWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	URegWidgets(const FObjectInitializer& ObjectInitializer);

	virtual void NativeConstruct() override;

public:

	UButton* wReloadCaptchaButton = nullptr;
	UImage* wCaptchaImage = nullptr;

	UImage* wLoginImage = nullptr;
	UImage* wPassImage = nullptr;
	UImage* wMailImage = nullptr;
	UImage* wCaptchaCheckImage = nullptr;

	UTexture2D* accept_tex = nullptr;
	UTexture2D* denied_tex = nullptr;
	UTexture2D* empty_tex = nullptr;

	UEditableTextBox* wLoginTextBox = nullptr;
	UEditableTextBox* wPasswordTextBox = nullptr;
	UEditableTextBox* wMainTextBox = nullptr;
	UEditableTextBox* wCaptchaTextBox = nullptr;

	UTextBlock* wInfoBlock = nullptr;

	UButton* wShowTermsPrivacyButton = nullptr;

	UButton* wCloseButton = nullptr;

	UButton* wSingUpButton = nullptr;

	UFUNCTION()
	void SingUpButtonClicked();

	UFUNCTION()
	void CloseButtonClicked();

	UFUNCTION()
	void ShowTermPrivacyClicked();

	UFUNCTION()
	void ReloadCaptchaClicked();

	UFUNCTION()
	void OnLoginTextChanged(const FText & text);

	UFUNCTION()
	void OnPasswordTextChanged(const FText & text);

	UFUNCTION()
	void OnMailTextChanged(const FText & text);

	UFUNCTION()
	void OnCaptchaTextChanged(const FText & text);

	bool bLoginOk = false;
	bool bPassOk = false;
	bool bMailOk = false;
	bool bCaptchaOk = false;
};

RegWidgets.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "RegWidgets.h"

URegWidgets::URegWidgets(const FObjectInitializer & ObjectInitializer)
	: Super(ObjectInitializer)
{
}

void URegWidgets::NativeConstruct()
{
	Super::NativeConstruct();
}

void URegWidgets::CloseButtonClicked()
{
}

void URegWidgets::ShowTermPrivacyClicked()
{
}

void URegWidgets::ReloadCaptchaClicked()
{
}

void URegWidgets::OnLoginTextChanged(const FText & text)
{
}

void URegWidgets::OnPasswordTextChanged(const FText & text)
{
}

void URegWidgets::OnMailTextChanged(const FText & text)
{
}

void URegWidgets::OnCaptchaTextChanged(const FText & text)
{
}

void URegWidgets::SingUpButtonClicked()
{
}

SetServerWidgets

SetServerWidgets.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "SetServerWidgets.generated.h"

class UEditableTextBox;

UCLASS()
class SPIKY_CLIENT_API USetServerWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	UEditableTextBox* wAddressBox = nullptr;
	UEditableTextBox* wPortBox = nullptr;
	
public:
	
	void SetAddress();
};

SetServerWidgets.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SetServerWidgets.h"
#include "SocketObject.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"

void USetServerWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wAddressBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("AddressBox")));
	wPortBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("PortBox")));

	// default value
	uint32 OutIP;
	USocketObject::tcp_address->GetIp(OutIP);

	// возвращаем ip нормальный вид
	FString ip = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP);

	wAddressBox->SetText(FText::FromString(ip));
	wPortBox->SetText(FText::FromString(FString::FromInt(USocketObject::tcp_address->GetPort())));
}

void USetServerWidgets::SetAddress()
{
	uint32 OutIP;
	USocketObject::tcp_address->GetIp(OutIP);

	// возвращаем ip нормальный вид
	FString oldIP = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP);
	FString oldPort = FString::FromInt(USocketObject::tcp_address->GetPort());

	// забрать данные при закрытии
	FIPv4Address serverIP;
	FIPv4Address::Parse(wAddressBox->GetText().ToString(), serverIP);
	int32 serverPort = FCString::Atoi(*(wPortBox->GetText().ToString()));

	FString newIP = serverIP.ToString();
	FString newPort = FString::FromInt(serverPort);

	GLog->Log(newIP + " " + newPort);

	// если новый ввод отличается от старого
	if (!oldIP.Equals(*newIP, ESearchCase::IgnoreCase) || !oldPort.Equals(*newPort, ESearchCase::IgnoreCase))
	{
		GLog->Log("Address change");
		USocketObject::tcp_address->SetIp(serverIP.Value);
		USocketObject::tcp_address->SetPort(FCString::Atoi(*(wPortBox->GetText().ToString())));
		USocketObject::Reconnect();
	}
}

SSButtonWidgets

SSButtonWidgets.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "SSButtonWidgets.generated.h"

class UButton;

UCLASS()
class SPIKY_CLIENT_API USSButtonWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	UButton* wSettingsButton = nullptr;

	UFUNCTION()
	void SettingsButtonClicked();
};

SSButtonWidgets.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SSButtonWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"

void USSButtonWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wSettingsButton = Cast<UButton>(GetWidgetFromName(TEXT("SettingsButton")));
	wSettingsButton->OnClicked.AddDynamic(this, &USSButtonWidgets::SettingsButtonClicked);
}

void USSButtonWidgets::SettingsButtonClicked()
{
}

WSWidgets

WSWidgets.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Spiky_Client.h"

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "Runtime/UMG/Public/Components/Image.h"
#include "WSWidgets.generated.h"

UCLASS()
class SPIKY_CLIENT_API UWSWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	FTimerHandle MessageTimerHandle;

	bool once = true;

	UImage * wGear1 = nullptr;
	UImage * wGear2 = nullptr;

	FWidgetTransform transform1;
	FWidgetTransform transform2;
	void GearsAnim();
};

WSWidgets.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "WSWidgets.h"

#include "Runtime/Engine/Public/TimerManager.h"

void UWSWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	if (once)
	{
		once = false;
		GetWorld()->GetTimerManager().SetTimer(MessageTimerHandle, this, &UWSWidgets::GearsAnim, 0.01f, true);
	}

	wGear1 = Cast<UImage>(GetWidgetFromName(TEXT("Gear1")));
	wGear2 = Cast<UImage>(GetWidgetFromName(TEXT("Gear2")));
}

void UWSWidgets::GearsAnim()
{
	transform1.Angle += 1;
	wGear1->SetRenderTransform(transform1);

	transform2.Angle -= 1;
	wGear2->SetRenderTransform(transform2);
}

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

USetServerWidgets::NativeConstruct()
     wAddressBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("AddressBox")));
     wPortBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("PortBox")));

В виджете SetServerWidgets мы получаем статический адрес, возвращаем ему нормальный вид. И заполняем им поля wAddressBox и wPortBox:

USetServerWidgets::SetAddress()
     вызывается после нажатия на кнопку скрыть виджет, задача функции
     забрать данные при закрытии
     если новый ввод отличается от старого 
     установить новые значения статическим полям адреса и порта
     вызвать USocketObject::Reconnect()

Виджет SSButtonWidgets – единственная функция показывать и скрывать SetServerWidgets всегда находясь поверх всего остального.

Для размещения виджетов слоями нам нужно создать WidgetsContainer с единственным элементом UCanvasPanel:

WidgetsContainer

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "WidgetsContainer.generated.h"

class UCanvasPanel;

UCLASS()
class SPIKY_CLIENT_API UWidgetsContainer : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	UCanvasPanel * wCanvas = nullptr;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "WidgetsContainer.h"
#include "Runtime/UMG/Public/Components/CanvasPanel.h"
#include "CanvasPanelSlot.h"

void UWidgetsContainer::NativeConstruct()
{
	Super::NativeConstruct();

	wCanvas = Cast<UCanvasPanel>(GetWidgetFromName(TEXT("Canvas")));
}

Сейчас откроем Unreal Editor, откроем виджет WidgetContainer, в котором есть дефолтный кенвас, присвоим ему имя Canvas, чтобы мы могли найти его в коде (если уже не присвоено), присвоим созданным виджетам новых родителей, переходим из Designer в Graph, выбираем Edit Class Settings и меняем Parent Class на соответствующие имя C++ класса.

Начнем размещать виджеты, для этого воспользуемся созданным ранее DifferentMix. Добавим опережающие объявления, конструктор, набор временных ссылок на полученные экземпляры, функцию GetWorld(), через экземпляр DifferentMix так же сможем получить ссылку на текущий мир, меняющией от GameMode к GameMode, сами виджеты которые создаются на основе ссылок, и их слоты на Canvas:

DifferentMix

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "DifferentMix.generated.h"

class UWidgetsContainer;
class UCanvasPanelSlot;
class URegWidgets;
class ULoginWidgets;
class USSButtonWidgets;
class USetServerWidgets;
class UUserWidget;
class UWSWidgets;

/**
 * World singleton, stores references to widgets and rare functions
 */
UCLASS()
class SPIKY_CLIENT_API UDifferentMix : public UObject
{
	GENERATED_BODY()
	
	UDifferentMix(const FObjectInitializer& ObjectInitializer);

	UWidgetsContainer* tmpWidgetContainerRef;

	URegWidgets* tmpRegistrationRef;
	ULoginWidgets* tmpLoginScreenRef;
	USSButtonWidgets* tmpServerSettingsButtonRef;
	USetServerWidgets* tmpServerSettingsRef;
	UUserWidget* tmpTermsPrivacyRef;
	UWSWidgets*  tmpWaitingScreenRef;
	
public:

	virtual class UWorld* GetWorld() const override;

	void Init();

	UWidgetsContainer* wWidgetContainer;

	URegWidgets* wRegistration;
	ULoginWidgets* wLoginScreen;
	USSButtonWidgets* wServerSettingsButton;
	USetServerWidgets* wServerSettings;
	UUserWidget* wTermsPrivacy;
	UWSWidgets*  wWaitingScreen;

	UCanvasPanelSlot* registrationSlot;
	UCanvasPanelSlot* loginScreenSlot;
	UCanvasPanelSlot* serverSettingsButtonsSlot;
	UCanvasPanelSlot* serverSettingsSlot;
	UCanvasPanelSlot* TermsPrivacySlot;
	UCanvasPanelSlot* waitingScreenSlot;
};

Для создания каждого виджета нам нужен доступ к текущему игровому миру, мы будем инициализировать DifferentMix в GameMode и сохранять ссылку на мир в GameInstance. Добавим в SpikyGameInstance:

// .h
static UWorld* world;
void DifferentMixInit(UWorld* the_world);
static UDifferentMix * DifferentMix;

Создадим объект DifferentMix и добавим в root, это предотвратит его уничтожение сборщиком мусора, вызовем Init это создаст нам набор виджетов:

// .cpp
UWorld* UClientGameInstance::world = nullptr;
UDifferentMix * UClientGameInstance::DifferentMix = nullptr;

void USpikyGameInstance::DifferentMixInit(UWorld* the_world)
{
	GLog->Log("DifferentMixInit");

	world = the_world;

	DifferentMix = NewObject<UDifferentMix>(UDifferentMix::StaticClass());
	DifferentMix->AddToRoot();
	DifferentMix->Init();
}

Теперь нам нужна верная ссылка на мир, в SpikyGameInstance мы не можем её получить так как это независимый от текущего мира объект, но GameMode подойдёт идеально, добавим в ASpikyGameMode::BeginPlay() инициализацию DifferentMix:

USpikyGameInstance* gameInstance = Cast<USpikyGameInstance>(GetWorld()->GetGameInstance());
gameInstance->DifferentMixInit(GetWorld());

Создаём в UDifferentMix::Init() виджеты и размещаем их слотом на canvas:

wWidgetContainer = CreateWidget<UWidgetsContainer>(GetWorld(), tmpWidgetContainerRef->GetClass());
wWidgetContainer->AddToViewport();

wRegistration = CreateWidget<URegWidgets>(GetWorld(), tmpRegistrationRef->GetClass());
registrationSlot = Cast<UCanvasPanelSlot(wWidgetContainer->wCanvas->AddChild(wRegistration));
registrationSlot->SetZOrder(0);
registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
wRegistration->SetVisibility(ESlateVisibility::Hidden);

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

Любой новый виджет в проекте добавляется так:

  1. Создаётся UMG интерфейс и CPP родитель;
  2. В DifferentMix объявляется предварительное объявление: class URegWidgets;
  3. Ссылка на виджет URegWidgets* tmpRegistrationRef;
  4. Сам виджет URegWidgets* wRegistration;
  5. И слот Canvas: UCanvasPanelSlot* registrationSlot;
  6. После в конструкторе инициализируем ссылку:
    static ConstructorHelpers::FClassFinder<URegWidgets> RegistrationWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Reg_W.Reg_W_C'"));
    
    if (RegistrationWidgets.Class != NULL)
    {
    	tmpRegistrationRef = RegistrationWidgets.Class->GetDefaultObject<URegWidgets>();
    }
    

  7. Затем в UDifferentMix::Init() создаём виджет и размещаем его в слоте:
    wRegistration = CreateWidget<URegWidgets>(GetWorld(), tmpRegistrationRef->GetClass());
    registrationSlot = Cast<UCanvasPanelSlot(wWidgetContainer->wCanvas->AddChild(wRegistration));
    registrationSlot->SetZOrder(0);
    registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
    registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
    wRegistration->SetVisibility(ESlateVisibility::Hidden);
    

При запуске игры, нам нужно показать LoginScreen, для этого добавим две новые функции вызова в DifferentMix:

void HideAllWidgets();
void ShowLoginScreen();
void ShowMouse();

Текущее состояние DifferentMix

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "DifferentMix.generated.h"

class UWidgetsContainer;
class UCanvasPanelSlot;
class URegWidgets;
class ULoginWidgets;
class USSButtonWidgets;
class USetServerWidgets;
class UUserWidget;
class UWSWidgets;

/**
 * World singleton, stores references to widgets and rare functions
 */
UCLASS()
class SPIKY_CLIENT_API UDifferentMix : public UObject
{
	GENERATED_BODY()
	
	UDifferentMix(const FObjectInitializer& ObjectInitializer);

	UWidgetsContainer* tmpWidgetContainerRef;

	URegWidgets* tmpRegistrationRef;
	ULoginWidgets* tmpLoginScreenRef;
	USSButtonWidgets* tmpServerSettingsButtonRef;
	USetServerWidgets* tmpServerSettingsRef;
	UUserWidget* tmpTermsPrivacyRef;
	UWSWidgets*  tmpWaitingScreenRef;
	
public:

	virtual class UWorld* GetWorld() const override;

	void Init();

	UWidgetsContainer* wWidgetContainer;

	URegWidgets* wRegistration;
	ULoginWidgets* wLoginScreen;
	USSButtonWidgets* wServerSettingsButton;
	USetServerWidgets* wServerSettings;
	UUserWidget* wTermsPrivacy;
	UWSWidgets*  wWaitingScreen;

	UCanvasPanelSlot* registrationSlot;
	UCanvasPanelSlot* loginScreenSlot;
	UCanvasPanelSlot* serverSettingsButtonsSlot;
	UCanvasPanelSlot* serverSettingsSlot;
	UCanvasPanelSlot* TermsPrivacySlot;
	UCanvasPanelSlot* waitingScreenSlot;

	void HideAllWidgets();
	void ShowLoginScreen();
	void ShowMouse();
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "DifferentMix.h"
#include "SpikyGameInstance.h"
#include "WidgetsContainer.h"
#include "RegWidgets.h"
#include "LoginWidgets.h"
#include "SSButtonWidgets.h"
#include "SetServerWidgets.h"
#include "WSWidgets.h"
#include "Runtime/UMG/Public/Components/CanvasPanel.h"
#include "CanvasPanelSlot.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"

UDifferentMix::UDifferentMix(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	static ConstructorHelpers::FClassFinder<UWidgetsContainer> WidgetContainer(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/WidgetContainer.WidgetContainer_C'"));

	if (WidgetContainer.Class != NULL)
	{
		tmpWidgetContainerRef = WidgetContainer.Class->GetDefaultObject<UWidgetsContainer>();
	}

	static ConstructorHelpers::FClassFinder<URegWidgets> RegistrationWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Reg_W.Reg_W_C'"));

	if (RegistrationWidgets.Class != NULL)
	{
		tmpRegistrationRef = RegistrationWidgets.Class->GetDefaultObject<URegWidgets>();
	}

	static ConstructorHelpers::FClassFinder<ULoginWidgets> LoginWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Login_W.Login_W_C'"));

	if (LoginWidgets.Class != NULL)
	{
		tmpLoginScreenRef = LoginWidgets.Class->GetDefaultObject<ULoginWidgets>();
	}

	static ConstructorHelpers::FClassFinder<USSButtonWidgets> SetServerButtonWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/SSButton_W.SSButton_W_C'"));

	if (SetServerButtonWidgets.Class != NULL)
	{
		tmpServerSettingsButtonRef = SetServerButtonWidgets.Class->GetDefaultObject<USSButtonWidgets>();
	}

	static ConstructorHelpers::FClassFinder<USetServerWidgets> ServerSettingsWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/SetServer_W.SetServer_W_C'"));

	if (ServerSettingsWidgets.Class != NULL)
	{
		tmpServerSettingsRef = ServerSettingsWidgets.Class->GetDefaultObject<USetServerWidgets>();
	}

	static ConstructorHelpers::FClassFinder<UUserWidget> TermsPrivacyWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Terms_Privacy_W.Terms_Privacy_W_C'"));

	if (TermsPrivacyWidgets.Class != NULL)
	{
		tmpTermsPrivacyRef = TermsPrivacyWidgets.Class->GetDefaultObject<UUserWidget>();
	}

	static ConstructorHelpers::FClassFinder<UWSWidgets> WaitingScreenWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/WS_W.WS_W_C'"));

	if (WaitingScreenWidgets.Class != NULL)
	{
		tmpWaitingScreenRef = WaitingScreenWidgets.Class->GetDefaultObject<UWSWidgets>();
	}
}

class UWorld* UDifferentMix::GetWorld() const
{
	return USpikyGameInstance::world;
}

void UDifferentMix::Init()
{
	wWidgetContainer = CreateWidget<UWidgetsContainer>(GetWorld(), tmpWidgetContainerRef->GetClass());
	wWidgetContainer->AddToViewport();

	wRegistration = CreateWidget<URegWidgets>(GetWorld(), tmpRegistrationRef->GetClass());
	registrationSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wRegistration));
	registrationSlot->SetZOrder(0);
	registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wRegistration->SetVisibility(ESlateVisibility::Hidden);

	wLoginScreen = CreateWidget<ULoginWidgets>(GetWorld(), tmpLoginScreenRef->GetClass());
	loginScreenSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wLoginScreen));
	loginScreenSlot->SetZOrder(0);
	loginScreenSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	loginScreenSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wLoginScreen->SetVisibility(ESlateVisibility::Hidden);

	wServerSettingsButton = CreateWidget<USSButtonWidgets>(GetWorld(), tmpServerSettingsButtonRef->GetClass());
	serverSettingsButtonsSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wServerSettingsButton));
	serverSettingsButtonsSlot->SetZOrder(3);
	serverSettingsButtonsSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	serverSettingsButtonsSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wServerSettingsButton->SetVisibility(ESlateVisibility::Hidden);

	wServerSettings = CreateWidget<USetServerWidgets>(GetWorld(), tmpServerSettingsRef->GetClass());
	serverSettingsSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wServerSettings));
	serverSettingsSlot->SetZOrder(1);
	serverSettingsSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	serverSettingsSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wServerSettings->SetVisibility(ESlateVisibility::Hidden);

	wTermsPrivacy = CreateWidget<UUserWidget>(GetWorld(), tmpTermsPrivacyRef->GetClass());
	TermsPrivacySlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wTermsPrivacy));
	TermsPrivacySlot->SetZOrder(1);
	TermsPrivacySlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	TermsPrivacySlot->SetOffsets(FMargin(0, 0, 0, 0));
	wTermsPrivacy->SetVisibility(ESlateVisibility::Hidden);

	wWaitingScreen = CreateWidget<UWSWidgets>(GetWorld(), tmpWaitingScreenRef->GetClass());
	waitingScreenSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wWaitingScreen));
	waitingScreenSlot->SetZOrder(1000); // max
	waitingScreenSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	waitingScreenSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wWaitingScreen->SetVisibility(ESlateVisibility::Hidden);
}

void UDifferentMix::HideAllWidgets()
{
	for (size_t i = 0; i < wWidgetContainer->wCanvas->GetChildrenCount(); i++)
	{
		wWidgetContainer->wCanvas->GetChildAt(i)->SetVisibility(ESlateVisibility::Hidden);
	}
}

void UDifferentMix::ShowLoginScreen()
{
	HideAllWidgets();
	wLoginScreen->SetVisibility(ESlateVisibility::Visible);
	wServerSettingsButton->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
}

void UDifferentMix::ShowMouse()
{
	// show mouse
	APlayerController* MyController = GetWorld()->GetFirstPlayerController();
	MyController->bShowMouseCursor = true;
	MyController->bEnableClickEvents = true;
	MyController->bEnableMouseOverEvents = true;
}

Добавим их вызов в SpikyGameMode:

USpikyGameInstance::DifferentMix->ShowLoginScreen();
USpikyGameInstance::DifferentMix->ShowMouse();

Текущее состояние SpikyGameMode

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SpikyGameMode.h"
#include "SocketObject.h"
#include "Runtime/Engine/Classes/Engine/World.h"
#include "Protobufs/UtilityModels.pb.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"

void ASpikyGameMode::BeginPlay()
{
	Super::BeginPlay();

	GLog->Log("AClientGameMode::BeginPlay()");

	USpikyGameInstance* gameInstance = Cast<USpikyGameInstance>(GetWorld()->GetGameInstance());
	gameInstance->DifferentMixInit(GetWorld());

	EnableInput(GetWorld()->GetFirstPlayerController());
	//InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage);

	USpikyGameInstance::DifferentMix->ShowLoginScreen();
	USpikyGameInstance::DifferentMix->ShowMouse();
}

void ASpikyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	GLog->Log("AClientGameMode::EndPlay()");
}

void ASpikyGameMode::TestSendUPDMessage()
{
	GLog->Log("send ->>>");

	std::shared_ptr<Utility> utility(new Utility);
	utility->set_alive(true);

	USocketObject::SendByUDP(utility.get());
}

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

Экраны регистрации и входа

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 5

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 6

Добавим реакцию на нажатие кнопки настройки адреса сервера:

SSButtonWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SSButtonWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "SetServerWidgets.h"

void USSButtonWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wSettingsButton = Cast<UButton>(GetWidgetFromName(TEXT("SettingsButton")));
	wSettingsButton->OnClicked.AddDynamic(this, &USSButtonWidgets::SettingsButtonClicked);
}

void USSButtonWidgets::SettingsButtonClicked()
{
	if (USpikyGameInstance::DifferentMix->wServerSettings->GetVisibility() == ESlateVisibility::Hidden)
	{
		USpikyGameInstance::DifferentMix->wServerSettings->SetVisibility(ESlateVisibility::Visible);
	}
	else
	{
		USpikyGameInstance::DifferentMix->wServerSettings->SetAddress();
		USpikyGameInstance::DifferentMix->wServerSettings->SetVisibility(ESlateVisibility::Hidden);
	}
}

Начнем реализовывать регистрацию, для этого нам понадобится возможности OpenSSL. Эта часть очень важна, так как все данные в игре мы будем шифровать аналогичным образом. На этапе входа, регистрации мы получаем ключи шифрования. Это работает следующим образом: мы начинаем ввод, данные формы проверяются на допустимость символов и затем сразу отправляются на сервер для проверки доступности, сервер ищет в базе данных такой логин, и возвращает код допустимости или ошибки. При открытии формы, сервер присылает капчу, а сам сохраняет её значение в карте <время, значение> все капчи старше 60 секунд удаляются. Ввод капчи проверяется с набором текста. Проверки осуществляются специальным обработчиком InputChecking. Если все поля заполнены правильно, то мы отправляем логин, меил, капчу на сервер в незашифрованном виде. На сервере мы проверяем наличие обязательных полей, затем все данные еще раз, только после этого генерируем публичный ключ и отправляем его клиенту. В нашем проекте я использую алгоритм Диффи-Хеллмана для обмена ключами. Шифрование происходит с помощью алгоритма AES-128. Алгоритм Диффи-Хеллмана позволяет двум сторонам получить общий секретный ключ, используя незащищенный от прослушивания канал связи. Принцип работы можно посмотреть здесь:

Алгоритм Диффи — Хеллмана

Но если в двух словах:

Боб выбирает два публичных числа(p, g) – например 75, 2
Алиса и Боб выбирают два секретных числа(a, b) – например 3, 15

Alice — g^a mod p -> 2^3 mod 75 = 8 (A)
Bob — g^b mod p -> 2^15 mod 75 = 68 (B)

A и B являются локальными секретными ключами

Вычисляется общий секреный ключ:

Alice — B^a mod p, 68^3 mod 75 = 32 pk
Bob — A^b mod p, 8^15 mod 75 = 32 pk

AES я выбрал из-за его скорости, компактности и поддержкой на уровне процессора.

Теперь нам нужно добавить алгоритм Диффи-Хеллмана и шифровнание AES в проекты. Здесь не будет много подробностей, хотя код я снабдил комментариями, эта тема отдельных статей и весьма непростая. Стоит почитать официальную документацию:

wiki.openssl.org/index.php/Diffie_Hellman
wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption

Добавьте новый класс Crypto в Utils. С его помощью мы сможем вычислять ключи Diffie-Hellman, шифровать AES, получать хэш SHA256 и кодировать/декодировать в Base64:

Cryptography

Crypto.h

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#pragma warning(disable:4996)

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include <openssl/bn.h>
#include <string>
#include <google/protobuf/message.h>
#include "Crypto.generated.h"

struct keys
{
	char * p;
	char * g;
	char * pubKey;
	char * privKey;
};

UCLASS()
class SPIKY_CLIENT_API UCrypto : public UObject
{
	GENERATED_BODY()

public:
	// DiffieHellman
	static DH *get_dh(int size); // 512 or 1024

	static keys Generate_KeysSet_DH();
	static DH * client;
	static std::string Generate_SecretKey_DH(std::string str);

	// Base64
	static size_t CalcDecodeLength(const char* b64input);
	static size_t Base64Decode(char* b64message, unsigned char** buffer, size_t* length);
	static std::string Base64Encode(char *decoded_bytes, size_t decoded_length);

	// Sha256
	static std::string SHA256(const void *data, size_t data_len);

	// AES_ecb_128
	static int AES_ECB_Encrypt(unsigned char *source, int source_len, unsigned char *key, unsigned char *cipher);
	static int AES_ECB_Decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *plaintext);
	static std::string Encrypt(std::string source, std::string key);
	static std::string Decrypt(std::string cipher, std::string key);
	static std::string EncryptProto(google::protobuf::Message * message, std::string key);

private:

	static void handleErrors(void);
};

Crypto.cpp

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "Crypto.h"

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4267)

// Base64, AES
#include <string>
#include <assert.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <openssl/err.h>
// Sha256
#include <sstream>
#include <iomanip>
// DH
#include <openssl/crypto.h>
#include <openssl/dh.h>

#include <memory>
#include "Config.h"

using namespace std;

DH * UCrypto::get_dh(int size)
{
	static unsigned char dh512_p[] = {
		0xDA, 0x58, 0x3C, 0x16, 0xD9, 0x85, 0x22, 0x89, 
		0xD0, 0xE4, 0xAF, 0x75, 0x6F, 0x4C, 0xCA, 0x92, 
		0xDD, 0x4B, 0xE5, 0x33, 0xB8, 0x04, 0xFB, 0x0F,
		0xED, 0x94, 0xEF, 0x9C, 0x8A, 0x44, 0x03, 0xED, 
		0x57, 0x46, 0x50, 0xD3, 0x69, 0x99, 0xDB, 0x29, 
		0xD7, 0x76, 0x27, 0x6B, 0xA2, 0xD3, 0xD4, 0x12,
		0xE2, 0x18, 0xF4, 0xDD, 0x1E, 0x08, 0x4C, 0xF6, 
		0xD8, 0x00, 0x3E, 0x7C, 0x47, 0x74, 0xE8, 0x33
	};

	static unsigned char dh1024_p[] = {
		0xF4, 0x88, 0xFD, 0x58, 0x4E, 0x49, 0xDB, 0xCD, 
		0x20, 0xB4, 0x9D, 0xE4, 0x91, 0x07, 0x36, 0x6B, 
		0x33, 0x6C, 0x38, 0x0D, 0x45, 0x1D, 0x0F, 0x7C,
		0x88, 0xB3, 0x1C, 0x7C, 0x5B, 0x2D, 0x8E, 0xF6, 
		0xF3, 0xC9, 0x23, 0xC0, 0x43, 0xF0, 0xA5, 0x5B, 
		0x18, 0x8D, 0x8E, 0xBB, 0x55, 0x8C, 0xB8, 0x5D,
		0x38, 0xD3, 0x34, 0xFD, 0x7C, 0x17, 0x57, 0x43, 
		0xA3, 0x1D, 0x18, 0x6C, 0xDE, 0x33, 0x21, 0x2C, 
		0xB5, 0x2A, 0xFF, 0x3C, 0xE1, 0xB1, 0x29, 0x40,
		0x18, 0x11, 0x8D, 0x7C, 0x84, 0xA7, 0x0A, 0x72, 
		0xD6, 0x86, 0xC4, 0x03, 0x19, 0xC8, 0x07, 0x29, 
		0x7A, 0xCA, 0x95, 0x0C, 0xD9, 0x96, 0x9F, 0xAB,
		0xD0, 0x0A, 0x50, 0x9B,	0x02, 0x46, 0xD3, 0x08, 
		0x3D, 0x66, 0xA4, 0x5D, 0x41, 0x9F, 0x9C, 0x7C, 
		0xBD, 0x89, 0x4B, 0x22, 0x19, 0x26, 0xBA, 0xAB,
		0xA2, 0x5E, 0xC3, 0x55, 0xE9, 0x2F, 0x78, 0xC7
	};

	static unsigned char dh_g[] = {
		0x02,
	};

	DH *dh;

	if (size == 512)
	{
		if ((dh = DH_new()) == NULL) return(NULL);
		dh->p = BN_bin2bn(dh512_p, sizeof(dh512_p), NULL);
		dh->g = BN_bin2bn(dh_g, sizeof(dh_g), NULL);
	}
	else
	{
		if ((dh = DH_new()) == NULL) return(NULL);
		dh->p = BN_bin2bn(dh1024_p, sizeof(dh1024_p), NULL);
		dh->g = BN_bin2bn(dh_g, sizeof(dh_g), NULL);
	}

	if ((dh->p == NULL) || (dh->g == NULL))
	{
		DH_free(dh); return(NULL);
	}
	return(dh);
}

//char * UOpenSSLCrypto::private_key_dh = "";
DH * UCrypto::client = get_dh(512); // DH_new(); // <- use pregenegate P/G or generate manualy (cpu heavy task)

keys UCrypto::Generate_KeysSet_DH()
{
	//DH_generate_parameters_ex(client, 512, DH_GENERATOR_2, NULL); //  generate P/G manualy
	// if you generate P/G manualy you also must send P/G to server
	DH_generate_key(client);

	keys keys_set;

	keys_set.p = BN_bn2dec(client->p);
	keys_set.g = BN_bn2dec(client->g);
	keys_set.pubKey = BN_bn2dec(client->pub_key);
	keys_set.privKey = BN_bn2dec(client->priv_key);

	return keys_set;
}

string UCrypto::Generate_SecretKey_DH(string str)
{
	BIGNUM *pub_bob_key = BN_new();
	BN_dec2bn(&pub_bob_key, str.c_str());

	unsigned char * dh_secret = (unsigned char*)OPENSSL_malloc(sizeof(unsigned char) * (DH_size(client)));

	DH_compute_key(dh_secret, pub_bob_key, client);

	return Base64Encode((char*)dh_secret, sizeof(unsigned char) * (DH_size(client)));
}

size_t UCrypto::CalcDecodeLength(const char* b64input) { //Calculates the length of a decoded string
	size_t len = strlen(b64input),
		padding = 0;

	if (b64input[len - 1] == '=' && b64input[len - 2] == '=') //last two chars are =
		padding = 2;
	else if (b64input[len - 1] == '=') //last char is =
		padding = 1;

	return (len * 3) / 4 - padding;
}

size_t UCrypto::Base64Decode(char* b64message, unsigned char** buffer, size_t* length) { //Decodes a base64 encoded string
	BIO *bio, *b64;

	int decodeLen = CalcDecodeLength(b64message);
	*buffer = (unsigned char*)malloc(decodeLen + 1);
	(*buffer)[decodeLen] = '';

	bio = BIO_new_mem_buf(b64message, -1);
	b64 = BIO_new(BIO_f_base64());
	bio = BIO_push(b64, bio);

	BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); //Do not use newlines to flush buffer
	*length = BIO_read(bio, *buffer, strlen(b64message));
	assert(*length == decodeLen); //length should equal decodeLen, else something went horribly wrong
	BIO_free_all(bio);

	return (0); //success
}

string UCrypto::Base64Encode(char *decoded_bytes, size_t decoded_length)
{
	int x;
	BIO *bioMem, *b64;
	BUF_MEM *bufPtr;

	b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	bioMem = BIO_new(BIO_s_mem());
	b64 = BIO_push(b64, bioMem);

	BIO_write(b64, decoded_bytes, decoded_length);
	x = BIO_flush(b64);
	if (x < 1) {
		BIO_free_all(b64);
		return NULL;
	}

	BIO_get_mem_ptr(b64, &bufPtr);

	string buff(bufPtr->data, bufPtr->length);

	BIO_free_all(b64);

	return buff;
}

/*
// USAGE EXAMPLE
//Encode To Base64
char* base64EncodeOutput, *text = "Hello World";

char* inbase = OpenSSL_Base64::Base64Encode(text, strlen((char*)text));

cout << inbase << endl;

//Decode From Base64
unsigned char* base64DecodeOutput;
size_t test;
OpenSSL_Base64::Base64Decode(inbase, &base64DecodeOutput, &test);

cout << base64DecodeOutput << endl;
*/

string UCrypto::SHA256(const void * data, size_t data_len)
{
	EVP_MD_CTX mdctx;
	unsigned char md_value[EVP_MAX_MD_SIZE];
	unsigned int md_len;

	EVP_DigestInit(&mdctx, EVP_sha256());
	EVP_DigestUpdate(&mdctx, data, (size_t)data_len);
	EVP_DigestFinal_ex(&mdctx, md_value, &md_len);
	EVP_MD_CTX_cleanup(&mdctx);

	std::stringstream s;
	s.fill('0');

	for (size_t i = 0; i < md_len; ++i)
		s << std::setw(2) << std::hex << (unsigned short)md_value[i];

	return s.str();
}

int UCrypto::AES_ECB_Encrypt(unsigned char * plaintext, int plaintext_len, unsigned char * key, unsigned char * ciphertext)
{
	EVP_CIPHER_CTX *ctx;

	int len;

	int ciphertext_len;

	/* Create and initialise the context */
	ctx = EVP_CIPHER_CTX_new();

	if (!ctx) handleErrors();

	/* Initialise the encryption operation. IMPORTANT - ensure you use a key size appropriate for your cipher
	 * In this we are using 128 bit AES (i.e. a 128 bit key).
	 */
	if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL)) handleErrors();

	/* Provide the message to be encrypted, and obtain the encrypted output.
	* EVP_EncryptUpdate can be called multiple times if necessary
	*/
	if (1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) handleErrors();

	ciphertext_len = len;

	/* Finalise the encryption. Further ciphertext bytes may be written at this stage. */
	if (1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handleErrors();

	ciphertext_len += len;

	/* Clean up */
	EVP_CIPHER_CTX_free(ctx);

	return ciphertext_len;
}

int UCrypto::AES_ECB_Decrypt(unsigned char * ciphertext, int ciphertext_len, unsigned char * key, unsigned char * plaintext)
{
	EVP_CIPHER_CTX *ctx;

	int len;

	int plaintext_len;

	/* Create and initialise the context */
	ctx = EVP_CIPHER_CTX_new();

	if (!ctx) handleErrors();

	if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL))
		handleErrors();

	/* Provide the message to be decrypted, and obtain the plaintext output.
	 * EVP_DecryptUpdate can be called multiple times if necessary
	 */
	if (1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) handleErrors();

	plaintext_len = len;

	/* Finalise the decryption. Further plaintext bytes may be written at this stage. */
	if (1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) handleErrors();

	plaintext_len += len;

	/* Clean up */
	EVP_CIPHER_CTX_free(ctx);

	return plaintext_len;
}

std::string UCrypto::Encrypt(string source, string key)
{
	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto cipher = make_unique<unsigned char[]>(source.length() * 2 + 8);

		unsigned char * source_c = (unsigned char*)source.c_str();

		size_t cipherLen = AES_ECB_Encrypt(source_c, strlen((char*)source_c), key_c, cipher.get());

		string cipher_str((char*)cipher.get(), cipherLen);

		free(key_c);
		return cipher_str;
	}
	else
	{
		return source;
	}
}

std::string UCrypto::Decrypt(std::string cipher, std::string key)
{
	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto source = make_unique<unsigned char[]>(cipher.length() * 2);

		unsigned char * cipher_c = (unsigned char*)cipher.c_str();

		size_t decryptLen = AES_ECB_Decrypt(cipher_c, cipher.length(), key_c, source.get());

		string decrypt_str((char*)source.get(), decryptLen);

		free(key_c);
		return decrypt_str;
	}
	else
	{
		return cipher;
	}
}

std::string UCrypto::EncryptProto(google::protobuf::Message * message, std::string key)
{
	int size = message->ByteSize();
	auto proto_arr = make_unique<unsigned char[]>(size);
	message->SerializeToArray(proto_arr.get(), size);

	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto cipher = make_unique<unsigned char[]>(size * 2 + 8);

		unsigned char * source_c = (unsigned char*)proto_arr.get();

		size_t cipherLen = AES_ECB_Encrypt(source_c, size, key_c, cipher.get());

		string cipher_str((char*)cipher.get(), cipherLen);

		free(key_c);
		return cipher_str;
	}
	else
	{
		string cipher_str((char*)proto_arr.get(), size);
		return cipher_str;
	}
}

void UCrypto::handleErrors(void)
{
	ERR_print_errors_fp(stderr);
	abort();
}

Какие функции содержит Crypto?

DiffieHellman

static DH *get_dh(int size);

Инициализирует p и g в зависимости от желаемой длины ключа 512 или 1024, используются прегенерированные p/g, ручная генерация тяжелая задача (cpu heavy task), это не сказывается на надёжности.

static keys Generate_KeysSet_DH();

Создаёт и сохраняет набор ключей: p,g, private key, public key.

static DH * client;

Экземпляр DH.

static std::string Generate_SecretKey_DH(std::string str);

Создаёт общий секретный ключ на основе входящего открытого ключа, возвращает строку в Base64.

Base64

static size_t CalcDecodeLength(const char* b64input);

Вычисляет длину декодированной строки.

static size_t Base64Decode(char* b64message, unsigned char** buffer, size_t* length);
static std::string Base64Encode(char *decoded_bytes, size_t decoded_length);

Кодирование/декодирование Base64.

Sha256

static std::string SHA256(const void *data, size_t data_len);

Получить хеш.

AES_ecb_128

static int AES_ECB_Encrypt(unsigned char *source, int source_len, unsigned char *key, unsigned char *cipher);
static int AES_ECB_Decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *plaintext);
static std::string Encrypt(std::string source, std::string key);
static std::string Decrypt(std::string cipher, std::string key);
static std::string EncryptProto(google::protobuf::Message * message, std::string key);

Разные версии шифровальщика, для любых данных, длинна ключа 16 символов.

static void handleErrors(void);

Обработка ошибок.

При вызове шифровальщика идет проверка if (Config::bEnableCrypt) если шифрование отключено возвращаются незашифрованные байты вместо зашифрованных.

Теперь тоже самое на сервере, к счастью тут всё проще. Очень хорошо всё разжевано в
Java Cryptography Architecture, в конце есть подробный пример: Appendix D: Sample Programs Diffie-Hellman Key Exchange between 2 Parties.

Методы по функциям те же, но нам не нужна внешняя библиотека, всё есть в javax.crypto.*; java.security.*;

Создадим пакет com.spiky.server.utils а в нем класс Cryptography:

Cryptography.java

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.utils;

import com.spiky.server.ServerMain;

import javax.crypto.*;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;

public class Cryptography {
    private String secretKey;
    private String clientPublicKey;
    private String clientPrivateKey;
    private KeyAgreement clientKeyAgree;

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public String getClientPublicKey() {
        return clientPublicKey;
    }

    public void DiffieHellman_createKeys() {
        try {

            DHParameterSpec dhSkipParamSpec = new DHParameterSpec(P, G);

            // Alice creates her own DH key pair, using the DH parameters from above
            KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");

            aliceKpairGen.initialize(dhSkipParamSpec);

            KeyPair aliceKpair = aliceKpairGen.generateKeyPair();

            DHPublicKey dhPub = (DHPublicKey)aliceKpair.getPublic();
            clientPublicKey = String.valueOf(dhPub.getY());

            DHPrivateKey dhPr = (DHPrivateKey)aliceKpair.getPrivate();
            clientPrivateKey = String.valueOf(dhPr.getX());

            // Alice creates and initializes her DH KeyAgreement object
            clientKeyAgree = KeyAgreement.getInstance("DH");
            clientKeyAgree.init(aliceKpair.getPrivate());

        } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
        }
    }

    public String DiffieHellman_createSecretKey(String bobPublicKey) {
        try {
            DHPublicKeySpec dhPubKeySpecs = new DHPublicKeySpec(new BigInteger(bobPublicKey), P, G);
            KeyFactory kf = KeyFactory.getInstance("DH");
            DHPublicKey bobPubKey = (DHPublicKey) kf.generatePublic(dhPubKeySpecs);

            clientKeyAgree.doPhase(bobPubKey, true);

            byte[] aliceSecret = clientKeyAgree.generateSecret();
            byte[] encodedBytes = Base64.getEncoder().encode(aliceSecret);

            String source_key = new String(encodedBytes);
            return source_key.substring(0, 16);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) {
            e.printStackTrace();
        }
        return null;
    }

    public byte[] Crypt(byte[] source, String key) {
        if(ServerMain.bEnableCrypto) {
            try {
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
                cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
                return cipher.doFinal(source);
            } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException e) {
                e.printStackTrace();
            }
        } else {
            return source;
        }
        return null;
    }

    public byte[] Decrypt(byte[] cryptogram, String key) {

        //System.out.println("ServerMain.bEnableCrypto: " + ServerMain.bEnableCrypto);

        if(ServerMain.bEnableCrypto) {
            try {
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
                cipher.init(Cipher.DECRYPT_MODE, skeySpec);
                return cipher.doFinal(cryptogram);
            } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
                e.printStackTrace();
            }
        } else {
            return cryptogram;
        }
        return null;
    }

    // The 1024 bit Diffie-Hellman modulus values used by SKIP
    private static final byte dh1024_p[] = {
            (byte)0xF4, (byte)0x88, (byte)0xFD, (byte)0x58, (byte)0x4E, (byte)0x49, (byte)0xDB, (byte)0xCD,
            (byte)0x20, (byte)0xB4, (byte)0x9D, (byte)0xE4, (byte)0x91, (byte)0x07, (byte)0x36, (byte)0x6B,
            (byte)0x33, (byte)0x6C, (byte)0x38, (byte)0x0D, (byte)0x45, (byte)0x1D, (byte)0x0F, (byte)0x7C,
            (byte)0x88, (byte)0xB3, (byte)0x1C, (byte)0x7C, (byte)0x5B, (byte)0x2D, (byte)0x8E, (byte)0xF6,
            (byte)0xF3, (byte)0xC9, (byte)0x23, (byte)0xC0, (byte)0x43, (byte)0xF0, (byte)0xA5, (byte)0x5B,
            (byte)0x18, (byte)0x8D, (byte)0x8E, (byte)0xBB, (byte)0x55, (byte)0x8C, (byte)0xB8, (byte)0x5D,
            (byte)0x38, (byte)0xD3, (byte)0x34, (byte)0xFD, (byte)0x7C, (byte)0x17, (byte)0x57, (byte)0x43,
            (byte)0xA3, (byte)0x1D, (byte)0x18, (byte)0x6C, (byte)0xDE, (byte)0x33, (byte)0x21, (byte)0x2C,
            (byte)0xB5, (byte)0x2A, (byte)0xFF, (byte)0x3C, (byte)0xE1, (byte)0xB1, (byte)0x29, (byte)0x40,
            (byte)0x18, (byte)0x11, (byte)0x8D, (byte)0x7C, (byte)0x84, (byte)0xA7, (byte)0x0A, (byte)0x72,
            (byte)0xD6, (byte)0x86, (byte)0xC4, (byte)0x03, (byte)0x19, (byte)0xC8, (byte)0x07, (byte)0x29,
            (byte)0x7A, (byte)0xCA, (byte)0x95, (byte)0x0C, (byte)0xD9, (byte)0x96, (byte)0x9F, (byte)0xAB,
            (byte)0xD0, (byte)0x0A, (byte)0x50, (byte)0x9B, (byte)0x02, (byte)0x46, (byte)0xD3, (byte)0x08,
            (byte)0x3D, (byte)0x66, (byte)0xA4, (byte)0x5D, (byte)0x41, (byte)0x9F, (byte)0x9C, (byte)0x7C,
            (byte)0xBD, (byte)0x89, (byte)0x4B, (byte)0x22, (byte)0x19, (byte)0x26, (byte)0xBA, (byte)0xAB,
            (byte)0xA2, (byte)0x5E, (byte)0xC3, (byte)0x55, (byte)0xE9, (byte)0x2F, (byte)0x78, (byte)0xC7
    };

    private static final byte dh512_p[] = {
            (byte)0xDA, (byte)0x58, (byte)0x3C, (byte)0x16, (byte)0xD9, (byte)0x85, (byte)0x22, (byte)0x89,
            (byte)0xD0, (byte)0xE4, (byte)0xAF, (byte)0x75, (byte)0x6F, (byte)0x4C, (byte)0xCA, (byte)0x92,
            (byte)0xDD, (byte)0x4B, (byte)0xE5, (byte)0x33, (byte)0xB8, (byte)0x04, (byte)0xFB, (byte)0x0F,
            (byte)0xED, (byte)0x94, (byte)0xEF, (byte)0x9C, (byte)0x8A, (byte)0x44, (byte)0x03, (byte)0xED,
            (byte)0x57, (byte)0x46, (byte)0x50, (byte)0xD3, (byte)0x69, (byte)0x99, (byte)0xDB, (byte)0x29,
            (byte)0xD7, (byte)0x76, (byte)0x27, (byte)0x6B, (byte)0xA2, (byte)0xD3, (byte)0xD4, (byte)0x12,
            (byte)0xE2, (byte)0x18, (byte)0xF4, (byte)0xDD, (byte)0x1E, (byte)0x08, (byte)0x4C, (byte)0xF6,
            (byte)0xD8, (byte)0x00, (byte)0x3E, (byte)0x7C, (byte)0x47, (byte)0x74, (byte)0xE8, (byte)0x33
    };

    private static final BigInteger P = new BigInteger(1, dh512_p);

    private static final BigInteger G = BigInteger.valueOf(2);
}

Нужно добавить возможность включения отключения шифрования, для этого в файле конфигурации добавим enableCrypt = true и прочитаем его в ServerMain:

/* шифруются ли данные */
public static final boolean bEnableCrypto = Boolean.parseBoolean(configurationBundle.getString("enableCrypt"));

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

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
return cipher.doFinal(source);

Здесь мы так же используем прегенерированные значения p, g. Шифрование с обеих сторон готово!

Вернёмся к регистрации.

После начала набора логина я хочу, чтобы его длина соответствовала минимум 3 символам, так же в нем могут быть только английские буквы, цифры, подчеркивания, тире. Создадим в DifferentMix функцию StringCleaner, которая будет принимать строку и допустимые в ней символы:

void UDifferentMix::StringCleaner(std::string & source, const std::string & availableSymbols)
{
	source.erase(std::remove_if(source.begin(), source.end(),
		[&availableSymbols](const char ch)
	{
		if (availableSymbols.find(ch) != std::string::npos) return false;
		return true;
	}
	), source.end());
}

В конструкторе NativeConstruct добавим ссылки на виджеты и делигируем события такие как изменение текста:

wLoginTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("LoginTextBox")));
wLoginTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnLoginTextChanged);

Добавим ссылки на изображения в конструкторе URegWidgets::URegWidgets, которые будут выводиться рядом с формой и показывать правильность ввода.

static ConstructorHelpers::FObjectFinder<UTexture2D> accept_ref(TEXT("Texture2D'/Game/ProjectResources/Images/accept.accept'"));

accept_tex = accept_ref.Object;

Функция проверки ввода работает так:

void URegWidgets::OnLoginTextChanged(const FText & text)
	переводим в std::string
	очищаем DifferentMix->StringCleaner() и присваиваем полю
	если (str.length() < 3)
		SetBrushFromTexture(denied_tex)
		wInfoBlock->SetText(FText::FromString("Error : Too short login"));
		return;

	UMessageEncoder::Send(inputChecking)

Если все хорошо — отправляем на сервер для проверки доступности. В процессе регистрации и входа мы используем RegLogModels, в котором есть четыре модели:

  • InputChecking – задача отправить логин/меил/капчу и получить ответ от сервера, может содержать байты изображения капчи.
  • Login – задача отправить логин/хэш пароля и публичный ключ, принимает-отправляет состояние операций.
  • Registration – всё, что связанно с регистрацией: логин/меил/хэш/публичный ключ/капча и состояние операций.
  • InitialState – в случаем успеха, отправляем клиенту начальное состояние, это будет логин, id сессии и состояние списка игровых комнат, все данные необходимые в главном меню.

Добавим в URegWidgets::OnLoginTextChanged создание сообщения и отправку его по сети:

std::shared_ptr<InputChecking> inputChecking(new InputChecking);
inputChecking->set_mail(TCHAR_TO_UTF8(*text.ToString()));

UMessageEncoder::Send(inputChecking.get(), false, true);

Похожим образом работают и остальные поля. После нажатия на кнопку SingUp проверяем флаги допустимости каждого из полей (если какое-то поле заполнено неправильно, флаг сигнализирует об этом). И если всё верно, отправляем логин, меил и капчу:

URegWidgets::SingUpButtonClicked()
	if (bLoginOk && bPassOk && bMailOk && bCaptchaOk)

URegWidgets::CloseButtonClicked()
	USpikyGameInstance::DifferentMix->ShowLoginScreen();

Для показа лицензии используется виджет Terms_Privacy_W, родителя для него создавать не будем, тут нет логики. Добавим в URegWidgets возможность отображения:

void URegWidgets::ShowTermPrivacyClicked()
{
	USpikyGameInstance::DifferentMix->wTermsPrivacy->SetVisibility(ESlateVisibility::Visible);
}

Текущее состояние RegWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "RegWidgets.h"
#include "Protobufs/RegLogModels.pb.h"
#include "MessageEncoder.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
#include "Runtime/Engine/Classes/Engine/Texture2D.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/Image.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"

URegWidgets::URegWidgets(const FObjectInitializer & ObjectInitializer)
	: Super(ObjectInitializer)
{
	static ConstructorHelpers::FObjectFinder<UTexture2D> accept_ref(TEXT("Texture2D'/Game/ProjectResources/Images/accept.accept'"));

	accept_tex = accept_ref.Object;

	static ConstructorHelpers::FObjectFinder<UTexture2D> denied_ref(TEXT("Texture2D'/Game/ProjectResources/Images/denied.denied'"));

	denied_tex = denied_ref.Object;

	static ConstructorHelpers::FObjectFinder<UTexture2D> empty_ref(TEXT("Texture2D'/Game/ProjectResources/Images/empty.empty'"));

	empty_tex = empty_ref.Object;
}

void URegWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wReloadCaptchaButton = Cast<UButton>(GetWidgetFromName(TEXT("ReloadCaptchaButton")));
	wReloadCaptchaButton->OnClicked.AddDynamic(this, &URegWidgets::ReloadCaptchaClicked);

	wCaptchaImage = Cast<UImage>(GetWidgetFromName(TEXT("CaptchaImage")));

	wLoginTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("LoginTextBox")));
	wLoginTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnLoginTextChanged);

	wPasswordTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("PasswordTextBox")));
	wPasswordTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnPasswordTextChanged);

	wMainTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("MailTextBox")));
	wMainTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnMailTextChanged);

	wCaptchaTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("CaptchaTextBox")));
	wCaptchaTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnCaptchaTextChanged);

	wLoginImage = Cast<UImage>(GetWidgetFromName(TEXT("LoginImage")));
	wPassImage = Cast<UImage>(GetWidgetFromName(TEXT("PasswordImage")));
	wMailImage = Cast<UImage>(GetWidgetFromName(TEXT("MailImage")));
	wCaptchaCheckImage = Cast<UImage>(GetWidgetFromName(TEXT("CaptchaCheckImage")));
	wInfoBlock = Cast<UTextBlock>(GetWidgetFromName(TEXT("InfoBlock")));

	wShowTermsPrivacyButton = Cast<UButton>(GetWidgetFromName(TEXT("TermsPrivacy")));
	wShowTermsPrivacyButton->OnClicked.AddDynamic(this, &URegWidgets::ShowTermPrivacyClicked);

	wCloseButton = Cast<UButton>(GetWidgetFromName(TEXT("CloseButton")));
	wCloseButton->OnClicked.AddDynamic(this, &URegWidgets::CloseButtonClicked);

	wSingUpButton = Cast<UButton>(GetWidgetFromName(TEXT("SingUpButton")));
	wSingUpButton->OnClicked.AddDynamic(this, &URegWidgets::SingUpButtonClicked);
}

void URegWidgets::CloseButtonClicked()
{
	USpikyGameInstance::DifferentMix->ShowLoginScreen();
}

void URegWidgets::ShowTermPrivacyClicked()
{
	USpikyGameInstance::DifferentMix->wTermsPrivacy->SetVisibility(ESlateVisibility::Visible);
}

void URegWidgets::ReloadCaptchaClicked()
{
	std::shared_ptr<InputChecking> inputChecking(new InputChecking);
	inputChecking->set_getcaptcha(true);

	UMessageEncoder::Send(inputChecking.get(), false, true);

	wCaptchaTextBox->SetText(FText::FromString(""));
	bCaptchaOk = false;
}

void URegWidgets::OnLoginTextChanged(const FText & text)
{
	std::string str(TCHAR_TO_UTF8(*text.ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-");

	wLoginTextBox->SetText(FText::FromString(str.c_str()));

	if (str.length() < 3)
	{
		wLoginImage->SetBrushFromTexture(denied_tex);
		wInfoBlock->SetText(FText::FromString("Error : Too short login"));
		return;
	}

	wInfoBlock->SetText(FText::FromString(" "));

	wLoginImage->SetBrushFromTexture(empty_tex);

	std::shared_ptr<InputChecking> inputChecking(new InputChecking);
	inputChecking->set_login(TCHAR_TO_UTF8(*text.ToString()));

	UMessageEncoder::Send(inputChecking.get(), false, true);
}

void URegWidgets::OnPasswordTextChanged(const FText & text)
{
	std::string str(TCHAR_TO_UTF8(*text.ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");

	wPasswordTextBox->SetText(FText::FromString(str.c_str()));

	if (str.length() < 4)
	{
		bPassOk = false;
		wPassImage->SetBrushFromTexture(denied_tex);
		wInfoBlock->SetText(FText::FromString("Error : Too short password"));
		return;
	}

	wInfoBlock->SetText(FText::FromString(" "));
	wPassImage->SetBrushFromTexture(accept_tex);
	bPassOk = true;
}

void URegWidgets::OnMailTextChanged(const FText & text)
{
	std::string str(TCHAR_TO_UTF8(*text.ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@._-");

	wMainTextBox->SetText(FText::FromString(str.c_str()));

	if (str.length() < 4)
	{
		bMailOk = false;
		wMailImage->SetBrushFromTexture(denied_tex);
		wInfoBlock->SetText(FText::FromString("Error : Too short mail"));
		return;
	}

	wInfoBlock->SetText(FText::FromString(" "));
	wMailImage->SetBrushFromTexture(empty_tex);

	std::shared_ptr<InputChecking> inputChecking(new InputChecking);
	inputChecking->set_mail(TCHAR_TO_UTF8(*text.ToString()));

	UMessageEncoder::Send(inputChecking.get(), false, true);
}

void URegWidgets::OnCaptchaTextChanged(const FText & text)
{
	std::string captcha_str(TCHAR_TO_UTF8(*wCaptchaTextBox->GetText().ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(captcha_str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");

	wCaptchaTextBox->SetText(FText::FromString(captcha_str.c_str()));

	if (captcha_str.length() < 5)
	{
		bCaptchaOk = false;
		wCaptchaCheckImage->SetBrushFromTexture(denied_tex);
		wInfoBlock->SetText(FText::FromString("Error : Too short captcha"));
		return;
	}

	wInfoBlock->SetText(FText::FromString(" "));
	wCaptchaCheckImage->SetBrushFromTexture(empty_tex);

	std::shared_ptr<InputChecking> inputChecking(new InputChecking);
	inputChecking->set_captcha(captcha_str);
	UMessageEncoder::Send(inputChecking.get(), false, true);
}

void URegWidgets::SingUpButtonClicked()
{
	if (bLoginOk && bPassOk && bMailOk && bCaptchaOk)
	{
		USpikyGameInstance::DifferentMix->RunWaitingScreen();

		std::shared_ptr<Registration> registration(new Registration);
		registration->set_login(TCHAR_TO_UTF8(*wLoginTextBox->GetText().ToString()));
		registration->set_mail(TCHAR_TO_UTF8(*wMainTextBox->GetText().ToString()));
		registration->set_captcha(TCHAR_TO_UTF8(*wCaptchaTextBox->GetText().ToString()));
		UMessageEncoder::Send(registration.get(), false, true);
	}
	else
	{
		wInfoBlock->SetText(FText::FromString("Error : Enter valid login/pass/mail/captcha"));
	}
}

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 7

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

void UWSWidgets::GearsAnim()
{
	transform1.Angle += 1;
	wGear1->SetRenderTransform(transform1);

	transform2.Angle -= 1;
	wGear2->SetRenderTransform(transform2);
}

Добавим две функции в DifferentMix:

void RunWaitingScreen();
void StopWaitingScreen();
...
void UDifferentMix::RunWaitingScreen()
{
	wWaitingScreen->SetVisibility(ESlateVisibility::Visible);
}

void UDifferentMix::StopWaitingScreen()
{
	wWaitingScreen->SetVisibility(ESlateVisibility::Hidden);
}

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 8

Добавим в URegWidgets::SingUpButtonClicked запуск экрана ожидания, после нажатия кнопки зарегистрироваться:

void URegWidgets::SingUpButtonClicked()
	if (bLoginOk && bPassOk && bMailOk && bCaptchaOk)
		USpikyGameInstance::DifferentMix->RunWaitingScreen();

Откроем LoginWidgets и добавим функционала, во первых найдем виджеты в ULoginWidgets::NativeConstruct(), во вторых мы будем использовать всплывающие уведомления об ошибках, для этого реализуем таймер для всплытия:

void ULoginWidgets::HideErrorMessage()
{
	wInfoBlock->SetText(FText::FromString(" "));
}

void ULoginWidgets::ShowErrorMessage(FString msg)
{
	wInfoBlock->SetText(FText::FromString(*msg));

	GetWorld()->GetTimerManager().ClearTimer(MessageTimerHandle);
	GetWorld()->GetTimerManager().SetTimer(MessageTimerHandle, this, &ULoginWidgets::HideErrorMessage, 1.2f, false);
}

В ULoginWidgets::SingUpButtonClicked() мы проверяем есть ли соединение, и если есть, показываем форму регистрации, запрашиваем капчу или сообщаем об ошибке:

if (USocketObject::bIsConnection)
{
	USpikyGameInstance::DifferentMix->ShowRegistrationScreen();
	USpikyGameInstance::DifferentMix->wRegistration->ReloadCaptchaClicked();
}
else
{
	ShowErrorMessage("No connection");
}

Добавим UDifferentMix::ShowRegistrationScreen():

void UDifferentMix::ShowRegistrationScreen()
{
	HideAllWidgets();
	wRegistration->SetVisibility(ESlateVisibility::Visible);
}

Закончим с логином:

ULoginWidgets::LoginButtonClicked()
	if (bMailOk && bPassOk && USocketObject::bIsConnection)
		RunWaitingScreen(); // запускает экран ожидания
		keys_set = UCrypto::Generate_KeysSet_DH(); // создаёт публичный ключ
		std::shared_ptr<Login> login_proto(new Login); // отправляет на сервер
		login_proto->set_publickey(keys_set.pubKey);
		UMessageEncoder::Send(login_proto.get(), false, true);

Остальные функции не представляют особого интереса, полный код LoginWidgets:

LoginWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include <string>
#include "LoginWidgets.generated.h"

class UButton;
class UTextBlock;
class UEditableTextBox;

UCLASS()
class SPIKY_CLIENT_API ULoginWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	bool bMailOk = false;
	bool bPassOk = false;

public:

	UButton* wSingUpButton = nullptr;
	UTextBlock* wInfoBlock = nullptr;

	UEditableTextBox* wMailTextBox = nullptr;
	UEditableTextBox* wPasswordTextBox = nullptr;

	UButton* wLoginButton = nullptr;
	UButton* wSettingsButton = nullptr;

	UFUNCTION()
	void SettingsButtonClicked();

	UFUNCTION()
	void SingUpButtonClicked();

	UFUNCTION()
	void LoginButtonClicked();

	UFUNCTION()
	void OnMailTextChanged(const FText & text);

	UFUNCTION()
	void OnPasswordTextChanged(const FText & text);

	FTimerHandle MessageTimerHandle;
	void HideErrorMessage();
	void ShowErrorMessage(FString msg);

	static std::string mail;
	static std::string password;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "LoginWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"
#include "Runtime/Engine/Public/TimerManager.h"
#include "SocketObject.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "RegWidgets.h"
#include "Crypto.h"
#include "Protobufs/RegLogModels.pb.h"
#include "SetServerWidgets.h"
#include "MessageEncoder.h"

std::string ULoginWidgets::mail = "";
std::string ULoginWidgets::password = "";

void ULoginWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wSingUpButton = Cast<UButton>(GetWidgetFromName(TEXT("SingUpButton")));
	wSingUpButton->OnClicked.AddDynamic(this, &ULoginWidgets::SingUpButtonClicked);

	wLoginButton = Cast<UButton>(GetWidgetFromName(TEXT("LoginButton")));
	wLoginButton->OnClicked.AddDynamic(this, &ULoginWidgets::LoginButtonClicked);

	wInfoBlock = Cast<UTextBlock>(GetWidgetFromName(TEXT("InfoBlock")));

	wMailTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("MailBox")));
	wMailTextBox->OnTextChanged.AddDynamic(this, &ULoginWidgets::OnMailTextChanged);

	wPasswordTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("PasswordBox")));
	wPasswordTextBox->OnTextChanged.AddDynamic(this, &ULoginWidgets::OnPasswordTextChanged);
}

void ULoginWidgets::LoginButtonClicked()
{
	if (bMailOk && bPassOk && USocketObject::bIsConnection)
	{
		GLog->Log("ULoginWidgets::LoginButtonClicked()");

		USpikyGameInstance::DifferentMix->RunWaitingScreen();

		keys keys_set = UCrypto::Generate_KeysSet_DH();

		std::shared_ptr<Login> login_proto(new Login);
		login_proto->set_publickey(keys_set.pubKey);
		UMessageEncoder::Send(login_proto.get(), false, true);
	}
	else
	{
		if (!USocketObject::bIsConnection)
		{
			ShowErrorMessage("No connection");
		}
		else if (!bMailOk && !bPassOk)
		{
			ShowErrorMessage("Incorrect mail and password");
		}
		else if (!bMailOk)
		{
			ShowErrorMessage("Incorrect mail");
		}
		else if (!bPassOk)
		{
			ShowErrorMessage("Incorrect password");
		}
	}
}

void ULoginWidgets::SettingsButtonClicked()
{
	USpikyGameInstance::DifferentMix->wServerSettings->SetVisibility(ESlateVisibility::Visible);
}

void ULoginWidgets::SingUpButtonClicked()
{
	if (USocketObject::bIsConnection)
	{
		USpikyGameInstance::DifferentMix->ShowRegistrationScreen();
		USpikyGameInstance::DifferentMix->wRegistration->ReloadCaptchaClicked();
	}
	else
	{
		ShowErrorMessage("No connection");
	}
}

void ULoginWidgets::HideErrorMessage()
{
	wInfoBlock->SetText(FText::FromString(" "));
}

void ULoginWidgets::ShowErrorMessage(FString msg)
{
	wInfoBlock->SetText(FText::FromString(*msg));

	GetWorld()->GetTimerManager().ClearTimer(MessageTimerHandle);
	GetWorld()->GetTimerManager().SetTimer(MessageTimerHandle, this, &ULoginWidgets::HideErrorMessage, 1.2f, false);
}

void ULoginWidgets::OnMailTextChanged(const FText & text)
{
	std::string str(TCHAR_TO_UTF8(*text.ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@._-");

	wMailTextBox->SetText(FText::FromString(str.c_str()));

	if (str.length() < 4)
	{
		bMailOk = false;
		ShowErrorMessage("Too short mail");
		return;
	}

	HideErrorMessage();
	bMailOk = true;
	mail = str;
}

void ULoginWidgets::OnPasswordTextChanged(const FText & text)
{
	std::string str(TCHAR_TO_UTF8(*text.ToString()));

	USpikyGameInstance::DifferentMix->StringCleaner(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");

	wPasswordTextBox->SetText(FText::FromString(str.c_str()));

	if (str.length() < 4)
	{
		bPassOk = false;
		ShowErrorMessage("Too short password");
		return;
	}

	HideErrorMessage();
	bPassOk = true;
	password = str;
}

Регистрации не хватает капчи. Я использовал библиотеку Patchca для создание простых капч. Добавим её пакеты рядом, в проект нашего сервера. Теперь к логике, зашифрованные данные мы помещаем в CryptogramWrapper, а затем оборачиваем Wrapper, выглядит это так:

message CryptogramWrapper { 
	bytes registration  = 1;
}

message Wrapper {
	Registration registration = 3;
}

На сервере в DecryptHandler, сотрем отправку эха, и напишем все возможные в проекте входящие типы, что-то из них не зашифровано, а что то то мы расшифровываем, парсим и отправляем на обработчики (тут их пока нет):

@Override
protected void decode(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper, List<Object> list) throws Exception {
    init(ctx);
    /* некоторые сообщения приходят нешифрованными */
    if(wrapper.hasCryptogramWrapper())
    {
        if(wrapper.getCryptogramWrapper().hasField(registration_cw))
        {
            byte[] cryptogram = wrapper.getCryptogramWrapper().getRegistration().toByteArray();
            byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

            RegistrationLoginModels.Registration registration = RegistrationLoginModels.Registration.parseFrom(original);

        }
        else if (wrapper.getCryptogramWrapper().hasField(login_cw))
        {
            byte[] cryptogram = wrapper.getCryptogramWrapper().getLogin().toByteArray();
            byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

            RegistrationLoginModels.Login login = RegistrationLoginModels.Login.parseFrom(original);

        }
        else if(wrapper.getCryptogramWrapper().hasField(mainMenu_cw))
        {
            byte[] cryptogram = wrapper.getCryptogramWrapper().getMainMenu().toByteArray();
            byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

	    MainMenuModels.MainMenu mainMenu = MainMenuModels.MainMenu.parseFrom(original);

        }
        else if(wrapper.getCryptogramWrapper().hasField(room_cw))
        {
            byte[] cryptogram = wrapper.getCryptogramWrapper().getRoom().toByteArray();
            byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

            GameRoomModels.Room room = GameRoomModels.Room.parseFrom(original);

        }
        else if(wrapper.getCryptogramWrapper().hasField(gameModels_cw))
        {
            byte[] cryptogram = wrapper.getCryptogramWrapper().getGameModels().toByteArray();
            byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

            GameModels.GameData gameData = GameModels.GameData.parseFrom(original);

        }
    }
    else if(wrapper.hasInputChecking())
    {
    }
    else if(wrapper.hasRegistration())
    {
    }
    else if(wrapper.hasLogin())
    {
    }
}

Определяем тип входящего сообщения и наличие у него шифрования. Поиск полей осуществляется с помощью findFieldByName:

...
public static com.google.protobuf.Descriptors.FieldDescriptor registration_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("registration");
...
if(wrapper.getCryptogramWrapper().hasField(registration_cw)) 
{
}

Все дескрипторы мы храним в Utils в классе Descriptors. Обработаем запросы на проверку логина, меила и получение капчи, создадим пакет Logics и в нем пустой класс InputChecking.

Данные мы будем хранить в базе данных MySQL извлекая данные с помощью Hibernate. Добавим зависимости в Maven:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.38</version>
</dependency>

<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-core</artifactId>
	<version>[4.2.6,4.2.9]</version>
</dependency>

Добавим в Utils класс SessionUtil, помощник возвращающий сессию через которую мы делаем запросы к БД:

Query query = session.createQuery("SELECT login FROM UserModel WHERE login = :str ");

SessionUtil

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.utils;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class SessionUtil {
    private final SessionFactory factory;

    public SessionUtil() {
        Configuration configuration = new Configuration();
        configuration.configure();
        ServiceRegistryBuilder srBuilder = new ServiceRegistryBuilder();
        srBuilder.applySettings(configuration.getProperties());
        ServiceRegistry serviceRegistry = srBuilder.buildServiceRegistry();
        factory = configuration.buildSessionFactory(serviceRegistry);
    }

    public Session getSession() {
        return getInstance().factory.openSession();
    }

    private static SessionUtil getInstance() {
        return new SessionUtil();
    }
}

Нам нужна модель, на основе которой будет создаваться таблица в базе, добавим пакет dbmodels и в него класс UserModel:

UserModel

 /*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.dbmodels;

import javax.persistence.*;

@Entity
public class UserModel {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column
    String login;
    @Column
    String hash;
    @Column
    String mail;

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getHash() {
        return hash;
    }

    public void setHashPass(String hash) {
        this.hash = hash;
    }

    public String getMail() {
        return mail;
    }

    public void setMail(String mail) {
        this.mail = mail;
    }

    @Override
    public String toString() {
        return "login: "" + login + ""nmail: "" + mail + ""nhash: "" + hash + """;
    }
}

Теперь добавим файл конфигурации hibernate.cfg.xml в resources:

hibernate.cfg.xml

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>

    <session-factory>
        <!--  Database connection settings  -->
        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/game_db</property>

        <!-- SQL dialect -->
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>

        <!-- set up connection pool c3p0 for use -->
        <property name="c3p0.max_size">10</property>

        <!--  Echo all executed SQL to stdout  -->
        <property name="show_sql">true</property>

        <!-- update the database schema on startup  -->
        <property name="hibernate.hbm2ddl.auto">update</property>

        <mapping class="com.spiky.server.dbmodels.UserModel"></mapping>

    </session-factory>
</hibernate-configuration>

Для тестов я работал с MySQL Workbench где нужно создать БД перед её использованием.

Возвращаясь к InputChecking, для каждого канала создаётся своё соединение с БД. Необходимо иметь возможность получать сессию, экземпляр криптографии и другие данные в любой точке программы, но специфично для каждого канала, для этого в Netty есть AttributeKey.

netty.io/4.0/api/io/netty/util/AttributeKey.html
stackoverflow.com/questions/29596677/replacement-for-attributekey
stackoverflow.com/questions/25932352/using-channel-attributes-in-different-context-handlers

В ServerMain объявим:

public final static AttributeKey<String> SECRETKEY = AttributeKey.valueOf("secret_key");
public final static AttributeKey<String> CHANNEL_OWNER = AttributeKey.valueOf("channel_owner");
public final static AttributeKey<Session> HIBER_SESSION = AttributeKey.valueOf("hiber_session");
public final static AttributeKey<Transaction> HIBER_TRANSACTION = AttributeKey.valueOf("hiber_transaction");
public final static AttributeKey<Cryptography> CRYPTOGRAPHY = AttributeKey.valueOf("hiber_cryptography");

В DecryptHandler добавим инициализацию, метод Init() вызывается один раз, при подключении нового клиента:

private Session session = new SessionUtil().getSession();
private Transaction transaction  = session.beginTransaction();
private Cryptography cryptography = new Cryptography();

/* инициализируем один раз */
private boolean bInit = false;

/* сохраняем значения необходимые во всех обработчиках этого канала*/
private void init(ChannelHandlerContext ctx) {
    if(!bInit) {
        bInit = true;

        ctx.channel().attr(HIBER_SESSION).set(session);
        ctx.channel().attr(HIBER_TRANSACTION).set(transaction);
        ctx.channel().attr(CRYPTOGRAPHY).set(cryptography);
    }
}

Капчи где-то нужно хранить, добавим в ServerMain карту со временем и значением:

/* синхронизированные набор капч */
public static Map<Long,String> captchaBank = Collections.synchronizedMap(new HashMap<>());

Кроме того капча не должна жить дольше минуты, создадим очиститель:

/* удалять капчи которые старше 60 секунд */
private static void captchaCleaner() {
    long lifetime = 60000;
    new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(10000); // проверять устаревших наличие капч каждые 10 секунд
                synchronized (captchaBank) {
                    captchaBank.entrySet().removeIf(e-> System.currentTimeMillis() - e.getKey() > lifetime);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

/* И запустим captchaCleaner() */
public static void main(String[] args) {
    new Thread(ServerMain::run_tcp).start();
    //new Thread(ServerMain::run_udp).start();

    captchaCleaner();
}

Создание каптч я разместил в классе Registration так как больше нигде мы её не используем, добавим пустой класс в пакет логики. Добавим getCaptcha(). Тут мы создаём patchca настраиваем цвет, размер, получаем байты изображения и возвращаем обёртку с ними:

RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                    .setCaptchaData(ByteString.copyFrom(captchaBytes))
                    .build();

Registration

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.protomodels.RegistrationLoginModels;
import org.patchca.background.SingleColorBackgroundFactory;
import org.patchca.color.SingleColorFactory;
import org.patchca.filter.predefined.CurvesRippleFilterFactory;
import org.patchca.service.ConfigurableCaptchaService;
import org.patchca.utils.encoder.EncoderHelper;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import static com.spiky.server.ServerMain.captchaBank;

public class Registration {

    public MessageModels.Wrapper getCaptcha() {
        try {
            ConfigurableCaptchaService cs = new ConfigurableCaptchaService();

            cs.setBackgroundFactory(new SingleColorBackgroundFactory(new Color(0, 0, 0)));
            cs.setColorFactory(new SingleColorFactory(new Color(255, 255, 255)));
            cs.setFilterFactory(new CurvesRippleFilterFactory(cs.getColorFactory()));
            cs.setHeight(100);
            cs.setWidth(250);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            captchaBank.put(System.currentTimeMillis(), EncoderHelper.getChallangeAndWriteImage(cs, "png", bos));

            byte[] captchaBytes = bos.toByteArray();
            bos.close();

            RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                    .setCaptchaData(ByteString.copyFrom(captchaBytes))
                    .build();

            return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

В конструкторе класса InputChecking, определяем тип сообщения, что нужно проверить, после отправляем на функцию check() указав тип операции:

if(type.equals("login")) 
    Query query = session.createQuery("SELECT login FROM UserModel WHERE login = :str ");
    List users = query.setParameter("str", data).list();

Где например для логина мы делаем запрос к БД, и возвращаем клиенту информацию есть он там или нет:

RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
        .setLoginCheckStatus(false) // no valid
        .build();

InputChecking

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.protomodels.RegistrationLoginModels;
import io.netty.channel.ChannelHandlerContext;
import org.hibernate.Query;
import org.hibernate.Session;

import java.util.List;
import java.util.Map;

import static com.spiky.server.ServerMain.HIBER_SESSION;
import static com.spiky.server.ServerMain.captchaBank;
import static com.spiky.server.utils.Descriptors.*;

public class InputChecking {
    public InputChecking(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper) {

        Session session = ctx.channel().attr(HIBER_SESSION).get();

        if(wrapper.getInputChecking().hasField(getCaptcha_ich)) {
            ctx.writeAndFlush(new Registration().getCaptcha());
        } else if(wrapper.getInputChecking().hasField(login_ich)) {
            ctx.writeAndFlush(check(session, wrapper.getInputChecking().getLogin(), "login"));
        } else if(wrapper.getInputChecking().hasField(mail_ich)) {
            ctx.writeAndFlush(check(session, wrapper.getInputChecking().getMail(), "mail"));
        } else if(wrapper.getInputChecking().hasField(captcha_ich)) {
            ctx.writeAndFlush(check(session, wrapper.getInputChecking().getCaptcha(), "captcha"));
        }
    }

    private MessageModels.Wrapper check(Session session, String data, String type) {
        if(type.equals("login")) {
            Query query = session.createQuery("SELECT login FROM UserModel WHERE login = :str ");
            List users = query.setParameter("str", data).list();

            if(!users.isEmpty()) {
                RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                        .setLoginCheckStatus(false) // no valid
                        .build();

                return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
            } else {
                RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                        .setLoginCheckStatus(true) // valid
                        .build();
                return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
            }
        } else if (type.equals("mail")) {
            Query query = session.createQuery("SELECT mail FROM UserModel WHERE mail = :str ");
            List mails = query.setParameter("str", data).list();

            if(!mails.isEmpty()) {
                RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                        .setMailCheckStatus(false)
                        .build();
                return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
            } else {
                RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                        .setMailCheckStatus(true)
                        .build();
                return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
            }
        } else if (type.equals("captcha")) {
            boolean challengeFind = false;
            synchronized (captchaBank) {
                for (Map.Entry<Long, String> entry : captchaBank.entrySet())
                    if (entry.getValue().equals(data)) challengeFind = true;

                if (challengeFind) {
                    RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                            .setCaptchaCheckStatus(true)
                            .build();
                    return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
                } else {
                    RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                            .setCaptchaCheckStatus(false)
                            .build();
                    return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();
                }
            }
        }
        return null;
    }
}

Вернёмся к клиенту, нам нужна функция которая позволит создать из массива байт изображение. Добавим её в DifferentMix:

UTexture2D* CreateTexture(const std::string raw, bool alpha);

// .h
class UTexture2D;

UTexture2D* CreateTexture(const std::string raw, bool alpha);
...
// .cpp
#include "Runtime/ImageWrapper/Public/Interfaces/IImageWrapperModule.h"
#include "Runtime/Core/Public/Modules/ModuleManager.h"
#include "Runtime/Engine/Classes/Engine/Texture2D.h"

UTexture2D* UDifferentMix::CreateTexture(const std::string raw, bool alpha)
{
	int32 num = raw.length();
	const uint8_t * byte_array = reinterpret_cast<const uint8_t*>(raw.c_str());
		
	if (num != 0)
	{
		IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName(TEXT("ImageWrapper")));
		// Note: PNG format.  Other formats are supported
		IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);

		if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(byte_array, num))
		{
			const TArray<uint8>* UncompressedBGRA = nullptr;
			TArray<uint8> UncompressedRGBA;
			if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
			{
				// Create the UTexture for rendering
				UncompressedRGBA.AddZeroed(UncompressedBGRA->Num());
				for (int i = 0; UncompressedBGRA->Num() > i; i += 4) {
					UncompressedRGBA[i] = (*UncompressedBGRA)[i + 2];
					UncompressedRGBA[i + 1] = (*UncompressedBGRA)[i + 1];
					UncompressedRGBA[i + 2] = (*UncompressedBGRA)[i];
					UncompressedRGBA[i + 3] = (*UncompressedBGRA)[i + 3];
					if (alpha) {
						if ((UncompressedRGBA[i] + UncompressedRGBA[i + 1] + UncompressedRGBA[i + 2]) < 3) {
							UncompressedRGBA[i + 3] = 0;
						}
					}
				}

				UTexture2D* MyTexture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_R8G8B8A8);

				// Fill in the source data from the file
				uint8* TextureData = (uint8*)MyTexture->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
				FMemory::Memcpy(TextureData, UncompressedRGBA.GetData(), UncompressedRGBA.Num());
				MyTexture->PlatformData->Mips[0].BulkData.Unlock();

				// Update the rendering resource from data.
				MyTexture->UpdateResource();
				return MyTexture;
			}
		}
	}
	return nullptr;
}

Для её использования нужно подключить новый модуль в .Build.cs – ImageWrapper. Создаём обработчик InputChecking в Handlers/Logics:

InputChecking

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include "InputChecking.generated.h"

class InputChecking;

UCLASS()
class SPIKY_CLIENT_API UInputChecking : public UObject
{
	GENERATED_BODY()

public:

	void Handler(const InputChecking inputChecking);

};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "InputChecking.h"
#include "Descriptors.h"
#include "Runtime/Engine/Classes/Engine/Texture2D.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "RegWidgets.h"
#include "Runtime/UMG/Public/Components/Image.h"
#include "Protobufs/RegLogModels.pb.h"

void UInputChecking::Handler(const InputChecking inputChecking)
{
	if (inputChecking.GetReflection()->HasField(inputChecking, Descriptors::captchaDataField_ich))
	{
		UTexture2D * tex = USpikyGameInstance::DifferentMix->CreateTexture(inputChecking.captchadata(), true);
		USpikyGameInstance::DifferentMix->wRegistration->wCaptchaImage->SetBrushFromTexture(tex);
	}
	else if (inputChecking.GetReflection()->HasField(inputChecking, Descriptors::loginCheckStatus_ich))
	{
		if (inputChecking.logincheckstatus())
		{
			USpikyGameInstance::DifferentMix->wRegistration->wLoginImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->accept_tex);
			USpikyGameInstance::DifferentMix->wRegistration->bLoginOk = true;
		}
		else
		{
			USpikyGameInstance::DifferentMix->wRegistration->wLoginImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->denied_tex);
		}
	}
	else if (inputChecking.GetReflection()->HasField(inputChecking, Descriptors::mailCheckStatus_ich))
	{
		if (inputChecking.mailcheckstatus())
		{
			USpikyGameInstance::DifferentMix->wRegistration->wMailImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->accept_tex);
			USpikyGameInstance::DifferentMix->wRegistration->bMailOk = true;
		}
		else
		{
			USpikyGameInstance::DifferentMix->wRegistration->wMailImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->denied_tex);
		}
	}
	else if (inputChecking.GetReflection()->HasField(inputChecking, Descriptors::captchaCheckStatus_ich))
	{
		if (inputChecking.captchacheckstatus())
		{
			USpikyGameInstance::DifferentMix->wRegistration->wCaptchaCheckImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->accept_tex);
			USpikyGameInstance::DifferentMix->wRegistration->bCaptchaOk = true;
		}
		else
		{
			USpikyGameInstance::DifferentMix->wRegistration->bCaptchaOk = false;
			USpikyGameInstance::DifferentMix->wRegistration->wCaptchaCheckImage->SetBrushFromTexture(USpikyGameInstance::DifferentMix->wRegistration->denied_tex);
		}
	}		
}

Здесь мы так же как и на сервере определяем что в сообщении и принимаем решение в зависимости от этого:

if (inputChecking.GetReflection()->HasField(inputChecking, Descriptors::captchaDataField_ich))
{
	// обработать капчу
}

На сервере в DecryptHandler добавляем обработчик InputChecking:

...
else if(wrapper.hasInputChecking())
{
    new InputChecking(ctx, wrapper);
}
...

В EncryptHandler обрабатываем исходящие сообщения:

public class EncryptHandler extends MessageToMessageEncoder<MessageModels.Wrapper> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper, List<Object> list) throws Exception {
        /* если не зашифрованно, просто отправить */
        if(!wrapper.hasCryptogramWrapper())
        {
            ctx.writeAndFlush(wrapper);
        } // или зашифровать 
        else if(cryptogramWrapper.hasField(registration_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getRegistration().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setRegistration(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
    }
}

Добавим все возможные варианты исходящих сообщений в EncryptHandler, чтобы к этому не возвращаться:

EncryptHandler

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.handlers;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.utils.Cryptography;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;

import java.util.List;

import static com.spiky.server.ServerMain.CRYPTOGRAPHY;
import static com.spiky.server.utils.Descriptors.*;

public class EncryptHandler extends MessageToMessageEncoder<MessageModels.Wrapper> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper, List<Object> list) throws Exception {

        Cryptography cryptography = ctx.channel().attr(CRYPTOGRAPHY).get();
        MessageModels.CryptogramWrapper cryptogramWrapper = wrapper.getCryptogramWrapper();
        String secretKey = cryptography.getSecretKey();

        /* если не зашифрованно, просто отправить */
        if(!wrapper.hasCryptogramWrapper())
        {
            ctx.writeAndFlush(wrapper);
        }
        else if(cryptogramWrapper.hasField(registration_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getRegistration().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setRegistration(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
        else if (cryptogramWrapper.hasField(login_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getLogin().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setLogin(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
        else if (cryptogramWrapper.hasField(initialState_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getInitialState().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setInitialState(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
        else if(cryptogramWrapper.hasField(mainMenu_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getMainMenu().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setMainMenu(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
        else if(cryptogramWrapper.hasField(room_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getRoom().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setRoom(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
        else if(cryptogramWrapper.hasField(gameModels_cw))
        {
            byte[] cryptogram = cryptography.Crypt(cryptogramWrapper.getGameModels().toByteArray(), secretKey);
            cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder().clear()
                    .setGameModels(ByteString.copyFrom(cryptogram)).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
    }
}

Добавим обработчик в ServerInitializer перед декриптором:

pipeline.addLast(new EncryptHandler());

На клиенте так же добавим в MessageEncoder обработку всех видов сообщений:

MessageEncoder

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MessageEncoder.h"
#include "SocketObject.h"
#include "Protobufs/MessageModels.pb.h"
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/io/coded_stream.h>
#include "Crypto.h"
#include "SpikyGameInstance.h"
#include "Protobufs/MainMenuModels.pb.h"
#include "Protobufs/GameRoomModels.pb.h"
#include "Protobufs/GameModels.pb.h "

bool UMessageEncoder::Send(google::protobuf::Message * message, bool bCrypt, bool bTCP)
{
	Wrapper wrapper;
	std::shared_ptr<CryptogramWrapper> cw(new CryptogramWrapper);

	// шифрованное или не шифрованное 
	if (bCrypt)
	{
		if (message->GetTypeName() == "Registration")
		{
			Registration * mes = static_cast<Registration*>(message);
			std::string cipher = UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey);
			cw->set_registration(cipher);
			wrapper.set_allocated_cryptogramwrapper(cw.get());
		}
		else if (message->GetTypeName() == "Login")
		{
			Login * mes = static_cast<Login*>(message);
			std::string cipher = UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey);
			cw->set_login(cipher);
			wrapper.set_allocated_cryptogramwrapper(cw.get());
		}
		else if (message->GetTypeName() == "MainMenu")
		{
			MainMenu * mes = static_cast<MainMenu*>(message);
			std::string cipher = UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey);
			cw->set_mainmenu(cipher);
			wrapper.set_allocated_cryptogramwrapper(cw.get());
		}
		else if (message->GetTypeName() == "Room")
		{
			Room * mes = static_cast<Room*>(message);
			std::string cipher = UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey);
			cw->set_room(cipher);
			wrapper.set_allocated_cryptogramwrapper(cw.get());
		}
		else if (message->GetTypeName() == "GameData")
		{
			GameData * mes = static_cast<GameData*>(message);
			std::string cipher = UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey);
			cw->set_gamemodels(cipher);
			wrapper.set_allocated_cryptogramwrapper(cw.get());
		}
	}
	else
	{
		if (message->GetTypeName() == "Utility")
		{
			Utility * mes = static_cast<Utility*>(message);
			wrapper.set_allocated_utility(mes);
		}
		else if (message->GetTypeName() == "InputChecking")
		{
			InputChecking * mes = static_cast<InputChecking*>(message);
			wrapper.set_allocated_inputchecking(mes);
		}
		else if (message->GetTypeName() == "Registration")
		{
			Registration * mes = static_cast<Registration*>(message);
			wrapper.set_allocated_registration(mes);
		}
		else if (message->GetTypeName() == "Login")
		{
			Login * mes = static_cast<Login*>(message);
			wrapper.set_allocated_login(mes);
		}
	}

	size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes
	uint8_t * buffer = new uint8_t[size];

	google::protobuf::io::ArrayOutputStream arr(buffer, size);
	google::protobuf::io::CodedOutputStream output(&arr);

	// записать длину сообщения и сообщение в buffer
	output.WriteVarint32(wrapper.ByteSize());
	wrapper.SerializeToCodedStream(&output);

	// освободить память выделенную для utility
	if (wrapper.has_utility())
	{
		wrapper.release_utility();
	}
	else if (wrapper.has_inputchecking())
	{
		wrapper.release_inputchecking();
	}
	else if (wrapper.has_registration())
	{
		wrapper.release_registration();
	}
	else if (wrapper.has_login())
	{
		wrapper.release_login();
	}
	else if (wrapper.has_cryptogramwrapper())
	{
		wrapper.release_cryptogramwrapper();
	}

	int32 bytesSent = 0;
	bool sentState = false;

	if (bTCP)
	{
		//send by tcp
		sentState = USocketObject::tcp_socket->Send(buffer, output.ByteCount(), bytesSent);
	}
	else
	{
		//send by udp
		sentState = USocketObject::udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *USocketObject::udp_address);
	}

	delete[] buffer;
	return sentState;
}

В MessageDecoder добавим дешифровку всех возможные в проекте входящих типов:

MessageDecoder

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MessageDecoder.h"
#include "Protobufs/MessageModels.pb.h"
#include "Crypto.h"
#include "Descriptors.h"
#include "SpikyGameInstance.h"
#include "Protobufs/MainMenuModels.pb.h"
#include "Protobufs/GameRoomModels.pb.h"

void UMessageDecoder::SendProtoToDecoder(Wrapper * wrapper)
{
	if (wrapper->has_inputchecking())
	{

	}
	else if (wrapper->has_registration())
	{

	}
	else if (wrapper->has_login())
	{

	}
	else if (wrapper->has_cryptogramwrapper())
	{
		if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::registration_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().registration(), USpikyGameInstance::secretKey);

			Registration registration;
			registration.ParseFromArray(source.c_str(), source.length());

		}
		else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::login_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().login(), USpikyGameInstance::secretKey);

			Login login;
			login.ParseFromArray(source.c_str(), source.length());

		}
		else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::initialState_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().initialstate(), USpikyGameInstance::secretKey);

			InitialState is;
			is.ParseFromArray(source.c_str(), source.length());

		}
		else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::mainMenu_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().mainmenu(), USpikyGameInstance::secretKey);

			MainMenu mainMenu;
			mainMenu.ParseFromArray(source.c_str(), source.length());

		}
		else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::room_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().room(), USpikyGameInstance::secretKey);

			Room room;
			room.ParseFromArray(source.c_str(), source.length());

		}
		else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::gameModels_cw))
		{
			std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().gamemodels(), USpikyGameInstance::secretKey);

			GameData gameData;
			gameData.ParseFromArray(source.c_str(), source.length());

		}
	}
}

И отправим на обработку сообщения типа InputChecking:

if (wrapper->has_inputchecking())
{
	UInputChecking * inputChecking = NewObject<UInputChecking>(UInputChecking::StaticClass());
	inputChecking->Handler(wrapper->inputchecking());
}

Теперь можно собрать, и протестировать. Регистрация регистро нечувствительна, Vadim и VaDiM одно и тоже, пароль никогда не пересылается по сети, мы берем хэш и отправляем.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 9

Закончим с регистрацией и логином, добавим в папку Logics на клиенте новые классы Registration и Login:

Registration

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include "Registration.generated.h"

class Registration;

UCLASS()
class SPIKY_CLIENT_API URegistration : public UObject
{
	GENERATED_BODY()

public:

	void Handler(Registration registration);

};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "Registration.h"
#include "Protobufs/RegLogModels.pb.h"
#include "Descriptors.h"
#include "Crypto.h"
#include "MessageEncoder.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "RegWidgets.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"

void URegistration::Handler(Registration registration)
{
	if (registration.GetReflection()->HasField(registration, Descriptors::publicKey_reg))
	{
		// create public, private key
		keys keys_set = UCrypto::Generate_KeysSet_DH();

		std::shared_ptr<Registration> reg(new Registration);
		reg->set_publickey(keys_set.pubKey);
		UMessageEncoder::Send(reg.get(), false, true); // send public key to server

		USpikyGameInstance::secretKey = UCrypto::Generate_SecretKey_DH(registration.publickey());
	}
	else if (registration.GetReflection()->HasField(registration, Descriptors::stateCode_reg))
	{
		if (registration.statecode() == 0) // error while registration
		{
			USpikyGameInstance::DifferentMix->wRegistration->wInfoBlock->SetText(FText::FromString("Unknown error try to enter everything again"));
			USpikyGameInstance::DifferentMix->wRegistration->wLoginTextBox->SetText(FText::FromString(""));
			USpikyGameInstance::DifferentMix->wRegistration->wPasswordTextBox->SetText(FText::FromString(""));
			USpikyGameInstance::DifferentMix->wRegistration->wMainTextBox->SetText(FText::FromString(""));
			USpikyGameInstance::DifferentMix->wRegistration->wCaptchaTextBox->SetText(FText::FromString(""));
			USpikyGameInstance::DifferentMix->wRegistration->ReloadCaptchaClicked();
			USpikyGameInstance::DifferentMix->StopWaitingScreen();
		}
		else if (registration.statecode() == 1) // successful secret key generation
		{
			GLog->Log("Successful secret key generation");

			std::shared_ptr<Registration> reg(new Registration);
			reg->set_login(TCHAR_TO_UTF8(*USpikyGameInstance::DifferentMix->wRegistration->wLoginTextBox->GetText().ToString()));
			std::string hash_str = TCHAR_TO_UTF8(*USpikyGameInstance::DifferentMix->wRegistration->wPasswordTextBox->GetText().ToString());
			reg->set_hash(UCrypto::SHA256(&hash_str, hash_str.length()));
			reg->set_mail(TCHAR_TO_UTF8(*USpikyGameInstance::DifferentMix->wRegistration->wMainTextBox->GetText().ToString()));

			UMessageEncoder::Send(reg.get(), true, true);
		}
	}
}

Login

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include "Login.generated.h"

class Login;

UCLASS()
class SPIKY_CLIENT_API ULogin : public UObject
{
	GENERATED_BODY()

public:

	void Handler(Login login);

};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "Login.h"
#include "Protobufs/RegLogModels.pb.h"
#include "SpikyGameInstance.h"
#include "LoginWidgets.h"
#include "Descriptors.h"
#include "Crypto.h"
#include "MessageEncoder.h"
#include "DifferentMix.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"

void ULogin::Handler(Login login)
{
	if (login.GetReflection()->HasField(login, Descriptors::publicKey_log))
	{
		USpikyGameInstance::secretKey = UCrypto::Generate_SecretKey_DH(login.publickey());

		std::shared_ptr<Login> login_proto(new Login);
		login_proto->set_mail(ULoginWidgets::mail);
		login_proto->set_hash(UCrypto::SHA256(&ULoginWidgets::password, ULoginWidgets::password.length()));

		UMessageEncoder::Send(login_proto.get(), true, true);
	}
	if (login.GetReflection()->HasField(login, Descriptors::stateCode_log))
	{
		USpikyGameInstance::DifferentMix->StopWaitingScreen();
		USpikyGameInstance::DifferentMix->wLoginScreen->wInfoBlock->SetText(FText::FromString("Incorrect login or password"));

		GLog->Log(FString("Session ID: NULL"));
		GLog->Log(FString("Login: NULL"));
	}
}

В SpikyGameInstance добавьте три новых статических поля, которые понадобятся на протяжении всего жизненного цикла программы:

//.h
static std::string secretKey;
static std::string sessionId;
static std::string userLogin;

//.cpp
std::string USpikyGameInstance::secretKey = "";
std::string USpikyGameInstance::sessionId = "";
std::string USpikyGameInstance::userLogin = "";

А так же на сервере обновим Registration и добавим Login:

Registration

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.dbmodels.UserModel;
import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.protomodels.RegistrationLoginModels;
import com.spiky.server.utils.Cryptography;
import io.netty.channel.ChannelHandlerContext;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.patchca.background.SingleColorBackgroundFactory;
import org.patchca.color.SingleColorFactory;
import org.patchca.filter.predefined.CurvesRippleFilterFactory;
import org.patchca.service.ConfigurableCaptchaService;
import org.patchca.utils.encoder.EncoderHelper;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;

import static com.spiky.server.ServerMain.*;
import static com.spiky.server.utils.Descriptors.*;

public class Registration {

	public Registration() {}

    public Registration(ChannelHandlerContext ctx, RegistrationLoginModels.Registration registration) {
        if(registration.hasField(publicKey_reg))
        {
            Cryptography cryptography = ctx.channel().attr(CRYPTOGRAPHY).get();

            ctx.channel().attr(SECRETKEY).set(cryptography.DiffieHellman_createSecretKey(registration.getPublicKey()));
            cryptography.setSecretKey(ctx.channel().attr(SECRETKEY).get());

            System.out.println("secret key: " + ctx.channel().attr(SECRETKEY).get()); // todo

            registration = RegistrationLoginModels.Registration.newBuilder().clear().setStateCode(1).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setRegistration(registration).build());
        }
        else if(!registration.hasField(login_reg) || !registration.hasField(mail_reg) || !registration.hasField(captcha_reg))
        {   // проверить наличие обязательных полей
            registration = RegistrationLoginModels.Registration.newBuilder()
                    .clear()
                    .setStateCode(0) // не уточняем причину
                    .build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setRegistration(registration).build());
        }
        else
        {   // проверить всё еще раз
            ctx.writeAndFlush(checkAll(ctx, registration));
        }
    }

    private MessageModels.Wrapper checkAll(ChannelHandlerContext ctx, RegistrationLoginModels.Registration registration) {

        Session session = ctx.channel().attr(HIBER_SESSION).get();

        Query query = session.createQuery("SELECT login, mail FROM UserModel WHERE login = :login OR mail = :mail ");
        query.setParameter("login", registration.getLogin());
        query.setParameter("mail", registration.getMail());

        java.util.List<String[]> userdata = query.list();

        boolean challengeFind = false;
        synchronized (captchaBank) {
            for (Map.Entry<Long, String> entry : captchaBank.entrySet())
                if (entry.getValue().equals(registration.getCaptcha())) challengeFind = true;
        }

        if(!userdata.isEmpty() || !challengeFind) {
            if(!userdata.isEmpty())
                System.out.println("!userdata.isEmpty()");

            if(!challengeFind)
                System.out.println("!challengeFind");

            registration = RegistrationLoginModels.Registration.newBuilder().clear().setStateCode(0).build();
            return MessageModels.Wrapper.newBuilder().setRegistration(registration).build();
        } else {
            System.out.println("Registration initialization, send public key..");

            Cryptography cryptography = ctx.channel().attr(CRYPTOGRAPHY).get();

            cryptography.DiffieHellman_createKeys();
            registration = RegistrationLoginModels.Registration.newBuilder().clear().setPublicKey(cryptography.getClientPublicKey()).build();
            return MessageModels.Wrapper.newBuilder().setRegistration(registration).build();
        }
    }

    public MessageModels.Wrapper getCaptcha() {
        try {
            ConfigurableCaptchaService cs = new ConfigurableCaptchaService();

            cs.setBackgroundFactory(new SingleColorBackgroundFactory(new Color(0, 0, 0)));
            cs.setColorFactory(new SingleColorFactory(new Color(255, 255, 255)));
            cs.setFilterFactory(new CurvesRippleFilterFactory(cs.getColorFactory()));
            cs.setHeight(100);
            cs.setWidth(250);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            captchaBank.put(System.currentTimeMillis(), EncoderHelper.getChallangeAndWriteImage(cs, "png", bos));

            byte[] captchaBytes = bos.toByteArray();
            bos.close();

            RegistrationLoginModels.InputChecking inputChecking = RegistrationLoginModels.InputChecking.newBuilder()
                    .setCaptchaData(ByteString.copyFrom(captchaBytes))
                    .build();

            return MessageModels.Wrapper.newBuilder().setInputChecking(inputChecking).build();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void saveUser(ChannelHandlerContext ctx, RegistrationLoginModels.Registration registration) {
        UserModel user = new UserModel();
        user.setLogin(registration.getLogin());
        user.setHashPass(registration.getHash());
        user.setMail(registration.getMail());

        Session session = ctx.channel().attr(HIBER_SESSION).get();
        Transaction transaction = ctx.channel().attr(HIBER_TRANSACTION).get();

        session.save(user);
        transaction.commit();

        RegistrationLoginModels.InitialState.Builder initialState = RegistrationLoginModels.InitialState.newBuilder()
                .setSessionId(ctx.channel().id().asShortText())
                .setLogin(user.getLogin());

        ctx.channel().attr(CHANNEL_OWNER).set(user.getLogin());

        /*
        *//* подписаться на обновления списка комнат *//*
        roomListUpdateSubscribers.add(ctx.channel());

        *//* прислать текущее состояние списка комнат *//*
        for (Map.Entry<String,GameRoom> pair : gameRooms.entrySet()) {
            *//* прислать только те комнаты которые не в процессе игры *//*
            if(!pair.getValue().getGameState()) {
                GameRoomModels.CreateRoom room = GameRoomModels.CreateRoom.newBuilder()
                        .setRoomName(pair.getValue().getRoomName())
                        .setMapName(pair.getValue().getMapName())
                        .setCreator(pair.getValue().getCreator())
                        .setGameTime(pair.getValue().getGameTime() + " minutes")
                        .setMaxPlayers(pair.getValue().getMaxPlayers() + " players")
                        .build();
                initialState.addCreateRoom(room);
            }
        }
        */

        MessageModels.CryptogramWrapper cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder()
                .setInitialState(ByteString.copyFrom(initialState.build().toByteArray())).build();

        ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
    }
}

Login

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.dbmodels.UserModel;
import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.protomodels.RegistrationLoginModels;
import com.spiky.server.utils.Cryptography;
import io.netty.channel.ChannelHandlerContext;
import org.hibernate.Query;
import org.hibernate.Session;

import java.util.List;
import java.util.Map;

import static com.spiky.server.ServerMain.*;
import static com.spiky.server.utils.Descriptors.publicKey_log;

public class Login {

	public Login() {}

    public Login(ChannelHandlerContext ctx, RegistrationLoginModels.Login login) {
        if(login.hasField(publicKey_log))
        {
            Cryptography cryptography = ctx.channel().attr(CRYPTOGRAPHY).get();

            cryptography.DiffieHellman_createKeys();
            ctx.channel().attr(SECRETKEY).set(cryptography.DiffieHellman_createSecretKey(login.getPublicKey()));
            cryptography.setSecretKey(ctx.channel().attr(SECRETKEY).get());

            System.out.println("secret key: " + ctx.channel().attr(SECRETKEY).get()); // todo

            login = RegistrationLoginModels.Login.newBuilder().clear().setPublicKey(cryptography.getClientPublicKey()).build();
            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setLogin(login).build());
        }
    }

    public void hasUser(ChannelHandlerContext ctx, RegistrationLoginModels.Login login) {
        Session session = ctx.channel().attr(HIBER_SESSION).get();

        Query query = session.createSQLQuery("SELECT * FROM UserModel WHERE mail = :mail AND hash = :hash").addEntity(UserModel.class);
        query.setParameter("mail", login.getMail());
        query.setParameter("hash", login.getHash());

        List<UserModel> user = query.list();

        if(!user.isEmpty()) {

            ctx.channel().attr(CHANNEL_OWNER).set(user.get(0).getLogin());

            /* подписаться на обновления списка комнат */
            //roomListUpdateSubscribers.add(ctx.channel());

            RegistrationLoginModels.InitialState.Builder initialState = RegistrationLoginModels.InitialState.newBuilder()
                    .setSessionId(ctx.channel().id().asShortText())
                    .setLogin(user.get(0).getLogin());

            /*
            *//* прислать текущее состояние списка комнат *//*
            for (Map.Entry<String,GameRoom> pair : gameRooms.entrySet()) {
                *//* прислать только те комнаты которые не в процессе игры *//*
                if(!pair.getValue().getGameState()) {
                    GameRoomModels.CreateRoom room = GameRoomModels.CreateRoom.newBuilder()
                            .setRoomName(pair.getValue().getRoomName())
                            .setMapName(pair.getValue().getMapName())
                            .setCreator(pair.getValue().getCreator())
                            .setGameTime(pair.getValue().getGameTime() + " minutes")
                            .setMaxPlayers(pair.getValue().getMaxPlayers() + " players")
                            .build();
                    initialState.addCreateRoom(room);
                }
            }
            */

            MessageModels.CryptogramWrapper cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder()
                    .setInitialState(ByteString.copyFrom(initialState.build().toByteArray())).build();

            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        } else {
            RegistrationLoginModels.Login log = RegistrationLoginModels.Login.newBuilder().setStateCode(0).build();

            MessageModels.CryptogramWrapper cryptogramWrapper = MessageModels.CryptogramWrapper.newBuilder()
                    .setLogin(ByteString.copyFrom(log.toByteArray())).build();

            ctx.writeAndFlush(MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cryptogramWrapper).build());
        }
    }
}

Добавим в DecryptHandler обработчики сообщений регистрации и входа:

DecryptHandler/decode

...
if(wrapper.hasCryptogramWrapper())
{
    if(wrapper.getCryptogramWrapper().hasField(registration_cw))
    {
        byte[] cryptogram = wrapper.getCryptogramWrapper().getRegistration().toByteArray();
        byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

        RegistrationLoginModels.Registration registration = RegistrationLoginModels.Registration.parseFrom(original);
        new Registration().saveUser(ctx, registration);
    }
    else if (wrapper.getCryptogramWrapper().hasField(login_cw))
    {
        byte[] cryptogram = wrapper.getCryptogramWrapper().getLogin().toByteArray();
        byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

        RegistrationLoginModels.Login login = RegistrationLoginModels.Login.parseFrom(original);
        new Login().hasUser(ctx, login);
    }
}
else if(wrapper.hasInputChecking())
{
    new InputChecking(ctx, wrapper);
}
else if(wrapper.hasRegistration())
{
    new Registration(ctx, wrapper.getRegistration());
}
else if(wrapper.hasLogin())
{
    new Login(ctx, wrapper.getLogin());
}

На клиенте:

UMessageDecoder::SendProtoToDecoder

...
if (wrapper->has_inputchecking())
{
	UInputChecking * inputChecking = NewObject<UInputChecking>(UInputChecking::StaticClass());
	inputChecking->Handler(wrapper->inputchecking());
}
else if (wrapper->has_registration())
{
	URegistration * registration = NewObject<URegistration>(URegistration::StaticClass());
	registration->Handler(wrapper->registration());
}
else if (wrapper->has_login())
{
	ULogin * login = NewObject<ULogin>(ULogin::StaticClass());
	login->Handler(wrapper->login());
}
else if (wrapper->has_cryptogramwrapper())
{
	if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::registration_cw))
	{
		std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().registration(), USpikyGameInstance::secretKey);

		Registration registration;
		registration.ParseFromArray(source.c_str(), source.length());

		URegistration * reg = NewObject<URegistration>(URegistration::StaticClass());
		reg->Handler(registration);
	}
	else if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::login_cw))
	{
		std::string source = UCrypto::Decrypt(wrapper->cryptogramwrapper().login(), USpikyGameInstance::secretKey);

		Login login;
		login.ParseFromArray(source.c_str(), source.length());

		ULogin * log = NewObject<ULogin>(ULogin::StaticClass());
		log->Handler(login);
	}
}

Разберём что тут происходит.

Регистрация

После нажатия на кнопку открытие окна регистрации — запрашивается капча, вызывается ReloadCaptchaClicked(), в котором создаётся объект типа InputChecking с полем inputChecking->set_getcaptcha(true), объект отправляется серверу через UMessageEncoder::Send(message, bCrypt, bTCP), в нем определяется тип, что сообщения InputChecking не шифруются, оборачиваем в Wrapper и отправляем по TCP USocketObject::tcp_socket->Send(...).

На сервере сообщение декодируется протобаф декодером и поступает на обработку в DecryptHandler decode. Здесь определяется их тип, наличие шифрования, и оправляется на обработчик InputChecking(ctx, wrapper) с параметрами контекста, чтобы мы могли ответить конкретному каналу и обёртка с данными. Так как получение капчи нужно нам только для регистрации метод создающий изображение и возвращающий массив байт находится в обработчике Registration. Вызываем и отправляем из InputChecking Registration().getCaptcha(). Значение капчи сохраняется на сервере в течении минуты в статической синхронизированной карте Map<Long,String> captchaBank, у пользователя есть минута чтобы правильно заполнить форму, или запросить новую капчу. После запуска сервера запускается поток в ServerMain — captchaCleaner() занимающийся удалением капч.

На клиенте сообщение декодируется в FTCPSocketListeningTh::Run() и отправляется на декодер для определения дальнейшей ветви логики UMessageDecoder::SendProtoToDecoder. После определения типа сообщения данные отправляются на UInputChecking::Handler, тут проверяется наличие поля «байты капчи» HasField(inputChecking, Descriptors::captchaDataField_ich), если есть создаём текстуру и выводим её на виджет:

UTexture2D* tex= USpikyGameInstance::DifferentMix->CreateTexture(inputChecking.captchadata(),true);
USpikyGameInstance::DifferentMix->wRegistration->wCaptchaImage->SetBrushFromTexture(tex);

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

Когда все поля заполнены правильно и нажата SingUpButtonClicked(), запускаем экран ожидания USpikyGameInstance::DifferentMix->RunWaitingScreen(), создаём объект Registration с полями Login, Mail, Captcha не шифруя отправляем серверу. Сервер декодирует, и отправляет на обработчик Registration, где проверяется наличие обязательных полей, проверяются данные еще раз вызовом checkAll(). Тут используем HIBER_SESSION доступный во всех обработчиках и делаются запросы к БД. Получаем экземпляр CRYPTOGRAPHY и создаём публичный ключ DiffieHellman/createKeys() который отправляем клиенту.

Клиент определяет тип, отправляет на обработчик URegistration::Handler где создаёт публичный ключ и секретный ключ. Секретный ключ сохраняет в USpikyGameInstance::secretKey. Публичный ключ отправляется на сервер где в Registration проверяется наличие ключа if(registration.hasField(publicKey_reg)) и создаётся на его основе секретный ключ, сохраняем его в ctx.channel().attr(SECRETKEY). Сервер возвращает код успеха или ошибки, тогда операция прерывается, все поля на клиенте сбрасываются. Клиент создает протобаф объект Registration, заполняет его логином, меилом и хэшем пароля полученным с помощью UCrypto::SHA256. Отправляет на UMessageEncoder::Send где определяется тип сообщения и оно шифруется UCrypto::EncryptProto(mes, USpikyGameInstance::secretKey). Байты присваиваются объекту CryptogramWrapper, который в свою очередь присваивается Wrapper, далее сообщение отправляется на сервер. На сервере в Registration вызывается saveUser(...) где данные сохраняются в базу данных и возвращается начальное состояние клиенту.

Логин

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

Главное меню и создание игровых комнат

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

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 10

В главном меню у нас будет три виджета:

MainMenu_W – интерфейс главного меню;
ChatMain_W – чат в главном меню, вложеный в MainMenu;
CreateRoom_W – виджет для создания игровых комнат;

MainMenuWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "MainMenuWidgets.generated.h"

class UTextBlock;
class UButton;
class UScrollBox;
class URoomListUnit;

UCLASS()
class SPIKY_CLIENT_API UMainMenuWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	UButton* wOpenChatButton = nullptr;
	UTextBlock* wPlayerName = nullptr;
	UTextBlock* wInfoBlock = nullptr;

	UButton* wCreateRoomButton = nullptr;

	UFUNCTION()
	void OpenChatButtonClicked();

	UFUNCTION()
	void CreateRoomButtonClicked();

	FTimerHandle MessageTimerHandle;
	void HideErrorMessage();
	void ShowErrorMessage(FString msg);

	UScrollBox * wRoomsScrollBox = nullptr;

	bool bChatOpen = false;
	bool bCreateRoomOpen = false;

	void AddRoom(URoomListUnit* room);

};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MainMenuWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/ScrollBox.h"
#include "Runtime/Engine/Public/TimerManager.h"

void UMainMenuWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wOpenChatButton = Cast<UButton>(GetWidgetFromName(TEXT("OpenChatButton")));
	wOpenChatButton->OnClicked.AddDynamic(this, &UMainMenuWidgets::OpenChatButtonClicked);

	wCreateRoomButton = Cast<UButton>(GetWidgetFromName(TEXT("CreateRoomButton")));
	wCreateRoomButton->OnClicked.AddDynamic(this, &UMainMenuWidgets::CreateRoomButtonClicked);

	wPlayerName = Cast<UTextBlock>(GetWidgetFromName(TEXT("PlayerName")));

	wInfoBlock = Cast<UTextBlock>(GetWidgetFromName(TEXT("InfoBox")));

	wRoomsScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("RoomsScrollBox")));
}

void UMainMenuWidgets::OpenChatButtonClicked()
{
}

void UMainMenuWidgets::CreateRoomButtonClicked()
{
}

void UMainMenuWidgets::AddRoom(URoomListUnit* room)
{
}

void UMainMenuWidgets::HideErrorMessage()
{
	wInfoBlock->SetText(FText::FromString(" "));
}

void UMainMenuWidgets::ShowErrorMessage(FString msg)
{
	wInfoBlock->SetText(FText::FromString(*msg));

	GetWorld()->GetTimerManager().ClearTimer(MessageTimerHandle);
	GetWorld()->GetTimerManager().SetTimer(MessageTimerHandle, this, &UMainMenuWidgets::HideErrorMessage, 2.f, false);
}

MainMenuChatWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "MainMenuChatWidgets.generated.h"

class Chat;
class UButton;
class UScrollBox;
class UMultiLineEditableTextBox;
class URichText;

UCLASS()
class SPIKY_CLIENT_API UMainMenuChatWidgets : public UUserWidget
{
	GENERATED_BODY()

	virtual void NativeConstruct() override;

public:

	UButton* wEnterButton = nullptr;

	UFUNCTION()
	void EnterButtonClicked();

	UScrollBox * wChatScrollBox = nullptr;

	UMultiLineEditableTextBox* wChatTextBox = nullptr;

	void NewMessage(URichText* richText);

	void ScrollToEnd();
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MainMenuChatWidgets.h"
#include "Runtime/UMG/Public/Components/ScrollBox.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/MultiLineEditableTextBox.h"

void UMainMenuChatWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wEnterButton = Cast<UButton>(GetWidgetFromName(TEXT("EnterButton")));
	wEnterButton->OnClicked.AddDynamic(this, &UMainMenuChatWidgets::EnterButtonClicked);

	wChatScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("ChatScrollBox")));

	wChatTextBox = Cast<UMultiLineEditableTextBox>(GetWidgetFromName(TEXT("ChatTextBox")));
}

void UMainMenuChatWidgets::EnterButtonClicked()
{
}

void UMainMenuChatWidgets::NewMessage(URichText* richText)
{
}

void UMainMenuChatWidgets::ScrollToEnd()
{
}

CreateRoom

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "CreateRoomWidgets.generated.h"

class UEditableTextBox;
class UButton;
class UComboBoxString;

UCLASS()
class SPIKY_CLIENT_API UCreateRoomWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	UEditableTextBox* wRoomNameTextBox = nullptr;
	UComboBoxString* wMapComboBox = nullptr;
	UComboBoxString* wGameTimeComboBox = nullptr;
	UComboBoxString* wPlayersNumberComboBox = nullptr;
	UButton* wCreateRoomButton = nullptr;

	UFUNCTION()
	void CreateRoomButtonClicked();

	UFUNCTION()
	void OnRoomNameTextChanged(const FText & text);

	bool bRoomNameOk = false;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "CreateRoomWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"
#include "Runtime/UMG/Public/Components/ComboBoxString.h"

void UCreateRoomWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wCreateRoomButton = Cast<UButton>(GetWidgetFromName(TEXT("CreateRoomButton")));
	wCreateRoomButton->OnClicked.AddDynamic(this, &UCreateRoomWidgets::CreateRoomButtonClicked);

	wRoomNameTextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("RoomName")));
	wRoomNameTextBox->OnTextChanged.AddDynamic(this, &UCreateRoomWidgets::OnRoomNameTextChanged);

	wMapComboBox = Cast<UComboBoxString>(GetWidgetFromName(TEXT("MapComboBox")));
	wGameTimeComboBox = Cast<UComboBoxString>(GetWidgetFromName(TEXT("TimeComboBox")));
	wPlayersNumberComboBox = Cast<UComboBoxString>(GetWidgetFromName(TEXT("PNComboBox")));
}

void UCreateRoomWidgets::CreateRoomButtonClicked()
{
}

void UCreateRoomWidgets::OnRoomNameTextChanged(const FText & text)
{
}

Скомпилируем и присвоим в редакторе родителей каждому виджету. Добавим новые виджеты в DifferentMix:

Новые виджеты в DifferentMix

//.h
class UMainMenuWidgets;
class UMainMenuChatWidgets;
class UCreateRoomWidgets;

UMainMenuWidgets* tmpMainMenuRef;
UMainMenuChatWidgets* tmpMainMenuChatRef;
UCreateRoomWidgets* tmpCreateRoomRef;

UMainMenuWidgets* wMainMenuWidgets;
UMainMenuChatWidgets* wMainMenuChatWidgets;
UCreateRoomWidgets* wCreateRoomWidgets;

UCanvasPanelSlot* mainMenuSlot;
UCanvasPanelSlot* mainMenuChatSlot;
UCanvasPanelSlot* createRoomSlot;

void ShowMainMenuScreen();

//.cpp

#include "MainMenuWidgets.h"
#include "MainMenuChatWidgets.h"
#include "CreateRoomWidgets.h"

UDifferentMix::UDifferentMix(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	static ConstructorHelpers::FClassFinder<UMainMenuWidgets> MainMenuWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/MainMenu_W.MainMenu_W_C'"));

	if (MainMenuWidgets.Class != NULL)
	{
		tmpMainMenuRef = MainMenuWidgets.Class->GetDefaultObject<UMainMenuWidgets>();
	}

	static ConstructorHelpers::FClassFinder<UMainMenuChatWidgets> chatMainMenuWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/ChatMain_W.ChatMain_W_C'"));

	if (chatMainMenuWidgets.Class != NULL)
	{
		tmpMainMenuChatRef = chatMainMenuWidgets.Class->GetDefaultObject<UMainMenuChatWidgets>();
	}

	static ConstructorHelpers::FClassFinder<UCreateRoomWidgets> createRoomWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/CreateRoom_W.CreateRoom_W_C'"));

	if (createRoomWidgets.Class != NULL)
	{
		tmpCreateRoomRef = createRoomWidgets.Class->GetDefaultObject<UCreateRoomWidgets>();
	}
}

void UDifferentMix::Init()
{
	wMainMenuWidgets = CreateWidget<UMainMenuWidgets>(GetWorld(), tmpMainMenuRef->GetClass());
	mainMenuSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wMainMenuWidgets));
	mainMenuSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	mainMenuSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wMainMenuWidgets->SetVisibility(ESlateVisibility::Hidden);

	wMainMenuChatWidgets = CreateWidget<UMainMenuChatWidgets>(GetWorld(), tmpMainMenuChatRef->GetClass());
	mainMenuChatSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wMainMenuChatWidgets));
	mainMenuChatSlot->SetZOrder(10);
	mainMenuChatSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	mainMenuChatSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wMainMenuChatWidgets->SetVisibility(ESlateVisibility::Hidden);

	wCreateRoomWidgets = CreateWidget<UCreateRoomWidgets>(GetWorld(), tmpCreateRoomRef->GetClass());
	createRoomSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wCreateRoomWidgets));
	createRoomSlot->SetZOrder(1);
	createRoomSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	createRoomSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
}

void UDifferentMix::ShowMainMenuScreen()
{
	HideAllWidgets();
	wMainMenuWidgets->SetVisibility(ESlateVisibility::Visible);
}

Сделаем так, чтобы после регистрации и логина открывалось главное меню и применялось начальное состояние. Это может быть все что угодно, но у нас пока это логин пользователя и его сессионный id. Добавим в клиенте в Logics класс InitialState, идентификатор сессии мы никак не используем, но в будущем он наверняка понадобится, например, чтобы отключить игрока, сохранить данные боя, если соединение прервётся. Мы сохраняем логин и id сессии в глобальном пространстве, останавливаем экран загрузки устанавливаем имя игрока и отображаем главное меню.

Раскомментируем строки в UMessageDecoder для обработки сообщений этого типа:

UInitialState * initialState = NewObject<UInitialState>(UInitialState::StaticClass());
initialState->Handler(is);

Теперь займёмся чатом, это один из ключевых моментов, по сути весь обмен сообщениями клиент-сервер-клиенты работает именно так. Начнем с того, что после нажатия на кнопку открыть чат, игрок подписывается на обновление рассылки, закрывает — чат очищается, игрок отписывается, больше ему не приходят обновления. Сервер хранит только 100 последних сообщений в CircularArrayList расширенный ArrayList, после открытия чата клиенту присылаются все сообщения.

Когда мы нажимаем UMainMenuWidgets::OpenChatButtonClicked(), мы отправляем серверу сигнал что хотим подписаться-отписаться на обновления чата:

UMainMenuWidgets::OpenChatButtonClicked()

#include "Protobufs/MainMenuModels.pb.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "MainMenuChatWidgets.h"
#include "MessageEncoder.h"

void UMainMenuWidgets::OpenChatButtonClicked()
{
	if (!bChatOpen)
	{
		bChatOpen = true;
		USpikyGameInstance::DifferentMix->wMainMenuChatWidgets->SetVisibility(ESlateVisibility::SelfHitTestInvisible);

		std::shared_ptr<Chat> chat(new Chat);
		chat->set_subscribe(true);

		std::shared_ptr<MainMenu> mainMenu(new MainMenu);
		mainMenu->set_allocated_chat(chat.get());

		UMessageEncoder::Send(mainMenu.get(), true, true);

		mainMenu->release_chat();
	}
	else
	{
		bChatOpen = false;
		USpikyGameInstance::DifferentMix->wMainMenuChatWidgets->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wMainMenuChatWidgets->wChatScrollBox->ClearChildren();

		std::shared_ptr<Chat> chat(new Chat);
		chat->set_subscribe(false);

		std::shared_ptr<MainMenu> mainMenu(new MainMenu);
		mainMenu->set_allocated_chat(chat.get());

		UMessageEncoder::Send(mainMenu.get(), true, true);

		mainMenu->release_chat();
	}
}

Так же нам нужен новый обработчик логики, добавим класс MainMenu в клиент:

MainMenu

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include "MainMenu.generated.h"

class MainMenu;

UCLASS()
class SPIKY_CLIENT_API UMainMenu : public UObject
{
	GENERATED_BODY()

public:

	void Handler(MainMenu mainMenu);
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MainMenu.h"
#include "Protobufs/MainMenuModels.pb.h"
#include "Descriptors.h"

void UMainMenu::Handler(MainMenu mainMenu)
{
}

Реагируем на сервере на запросы касающиеся главного меню, создаём MainMenu в логике, добавляем новый тип в DecryptHandler:

else if(wrapper.getCryptogramWrapper().hasField(mainMenu_cw))
{
    byte[] cryptogram = wrapper.getCryptogramWrapper().getMainMenu().toByteArray();
    byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

    MainMenuModels.MainMenu mainMenu = MainMenuModels.MainMenu.parseFrom(original);
    new MainMenu(ctx, mainMenu);
}

Для чата нам понадобится отдельный класс MainMenuChat, создадим его в логике:

MainMenuChat

public class MainMenu {
    public MainMenu(ChannelHandlerContext ctx, MainMenuModels.MainMenu mainMenu) {
        if(mainMenu.hasField(chat_mm))
        {
            new MainMenuChat(ctx, mainMenu.getChat());
        }
    }
}

public class MainMenuChat {
    public MainMenuChat(ChannelHandlerContext ctx, MainMenuModels.Chat chat) {

    }
}

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

CircularArrayList

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.utils;

import java.util.ArrayList;

public class CircularArrayList<E> extends ArrayList<E> {
    private ArrayList<E> list;
    private int maxSize;

    public CircularArrayList(int size) {
        list = new ArrayList<E> (size);
        maxSize = size;
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean add (E objectToAdd) {
        if (list.size () > maxSize) {
            list.remove(0);
            list.add(objectToAdd);
        } else {
            list.add(objectToAdd);
        }
        return true;
    }

    @Override
    public E get(int i) {
        return list.get(i);
    }

    @Override
    public String toString() {
        String str = "";
        for (E element : list) str+= "[" + element + "]";
        return str;
    }
}

Мы расширяем ArrayList, при добавлении нового элемента проверяем не превышен ли размер если да удалить первый элемент и добавить новый в конец. Добавим в MainMenuChat:

/* список подписчиков главного чата */
private static ChannelGroup recipients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

/* список из 100 сообщений */
private static List<MainMenuModels.Chat> syncMessageList = Collections.synchronizedList(new CircularArrayList<MainMenuModels.Chat>(100));

Recipients – еще одна удобная возможность Netty, с его помощью мы храним список каналов, подписчиков, неактивные каналы сами удаляются из него. MainMenuChat работает следующим образом:

if(chat.hasField(subscribe_chat))
	if(chat.getSubscribe())
		/* подписать */ 
		recipients.add(ctx.channel());
		создать список последних 100 сообщений 
		отправить
	или отписать
		recipients.remove(ctx.channel());
else пришло сообщение
	приходит только текст
	присвоить сообщению время и имя автора
	поместить в список сообщений
	/* отправили всем подписантам */
        recipients.writeAndFlush(wrapper);

Добавим новый обработчик в UMainMenu::Handler:

 if (mainMenu.GetReflection()->HasField(mainMenu, Descriptors::chat_mm)) {
	UChats * chats = NewObject<UChats>(UChats::StaticClass());
	chats->Handler(mainMenu.chat(), "mainmenu");
}

С чатом всё, можно протестировать, когда игрок отправляет сообщение они появляется у него только после того как пройдёт сервер, вместе со всеми.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 11

Теперь перейдём к созданию тренировочных комнат. Создадим на сервере в логике два класса RoomManager – создаёт/удаляет/обновляет комнаты, отписывает/подписывает, чистит данные если пользователь например потерял связь или вышел. GameRoom – хранит настройки одной конкретной комнаты, имя, владельца, игроков по командам. На клиенте в CreateRoomWidgets проверяем допустимость имени и отправляем запрос с выбранными настройками на сервер, можно задать имя игры, карту, количество игроков и продолжительность игры.

Здесь мы используем сообщение типа CreateRoom, которое оборачиваем в Room, затем шифруем, и оборачиваем в Wrapper.

Добавим действия к UMainMenuWidgets::CreateRoomButtonClicked():

#include "CreateRoomWidgets.h"
...
void UMainMenuWidgets::CreateRoomButtonClicked()
{
	if (!bCreateRoomOpen)
	{
		bCreateRoomOpen = true;
		USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
	}
	else
	{
		bCreateRoomOpen = false;
		USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
	}
}

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 12

На сервере в DecryptHandler/decode, передаём контекст и входящие данные на RoomManager:

...
else if(wrapper.getCryptogramWrapper().hasField(room_cw))
{
    byte[] cryptogram = wrapper.getCryptogramWrapper().getRoom().toByteArray();
    byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

    GameRoomModels.Room room = GameRoomModels.Room.parseFrom(original);
    new RoomManager(ctx, room);
}
...

На клиенте логику комнат будем обрабатывать в классе GameRoom, добавим его в Logics:

GameRoom

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include <string>
#include "GameRoom.generated.h"

class Room;
class CreateRoom;
class URoomListUnit;
class RoomsListUpdate;
class SubscribeRoom;
class RoomUpdate;

UCLASS()
class SPIKY_CLIENT_API UGameRoom : public UObject
{
	GENERATED_BODY()

public:

	void Handler(Room room);

	static URoomListUnit* NewRoom(CreateRoom room);

	static std::string roomCreator;
	static std::string roomName;

	void DeleteRoom(RoomsListUpdate room);
	void DeleteRoom(std::string name);
	void Subscribe(SubscribeRoom subscribe);
	void UpdateRoom(RoomUpdate update);
	void StartGame();
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "GameRoom.h"
#include "Descriptors.h"
#include "Protobufs/GameRoomModels.pb.h"

std::string UGameRoom::roomCreator = "";
std::string UGameRoom::roomName = "";

void UGameRoom::Handler(Room room)
{
	if (room.has_createroom())
	{
	}
	else if (room.has_roomslistupdate())
	{
	}
	else if (room.has_subscriberoom())
	{
	}
	else if (room.has_roomupdate())
	{
	}
}

void UGameRoom::Subscribe(SubscribeRoom subscribe) 
{
}

void UGameRoom::UpdateRoom(RoomUpdate update)
{
}

URoomListUnit* UGameRoom::NewRoom(CreateRoom room)
{
	return nullptr;
}

void UGameRoom::DeleteRoom(RoomsListUpdate update)
{
}

void UGameRoom::DeleteRoom(std::string name)
{
}

void UGameRoom::StartGame()
{
}

Итак, мы отправили сообщение о создании новой комнаты с клиента и оно попало в RoomManager, где мы определили что хочет клиент if(room.hasCreateRoom()) и отправили на обработку в createRoom(ctx, room.getCreateRoom());

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

public final static AttributeKey<String> ROOM_OWNER = AttributeKey.valueOf("room_owner");
/* набор позьзователей получающих обновления списка комнат в главном меню */
public static ChannelGroup roomListUpdateSubscribers = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/* набор всех игровых комнат */
public static Map<String,GameRoom> gameRooms = Collections.synchronizedMap(new HashMap<>());

Откроем GameRoom, экземпляр этого класса хранит имя создателя, имя карты, имя комнаты, время игры, количество игроков, id сессии создателя, состояние игры – то есть игра запущена или нет, если да, то комната должна пропасть из общедоступного списка и не добавлять больше игроков. Списки команд и чат:

GameRoom

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MainMenuModels;
import com.spiky.server.utils.CircularArrayList;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.*;

import static com.spiky.server.ServerMain.CHANNEL_OWNER;

public class GameRoom {
    private String creator;
    private String mapName;
    private String roomName;
    private int gameTime;
    private int maxPlayers;
    private String creatorSessionId;
    private boolean gameState = false;

    Map<String,Channel> players = Collections.synchronizedMap(new HashMap<>());
    Map<String,Channel> team1 = Collections.synchronizedMap(new HashMap<>());
    Map<String,Channel> team2 = Collections.synchronizedMap(new HashMap<>());
    Map<String,Channel> undistributed = Collections.synchronizedMap(new HashMap<>());

    //Map<String,PlayerState> playersState = Collections.synchronizedMap(new HashMap<>());

    /* список подписчиков на обновления комнаты */
    ChannelGroup recipients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /* список из 100 сообщений чата*/
    List<MainMenuModels.Chat> syncMessageList = Collections.synchronizedList(new CircularArrayList<MainMenuModels.Chat>(100));

    GameRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom roomDescribe) {
        creator = ctx.channel().attr(CHANNEL_OWNER).get();
        /* если игрок вышел или потерял соединение, комната через несколько секунд удаляется по его id сессии */
        creatorSessionId = ctx.channel().id().asShortText();
        roomName = roomDescribe.getRoomName();
        mapName = roomDescribe.getMapName();

        if(Objects.equals(roomDescribe.getGameTime(), "5 minutes")) {
            gameTime = 5;
        } else if(Objects.equals(roomDescribe.getGameTime(), "10 minutes")) {
            gameTime = 10;
        } else if(Objects.equals(roomDescribe.getGameTime(), "15 minutes")) {
            gameTime = 15;
        } else if(Objects.equals(roomDescribe.getGameTime(), "20 minutes")) {
            gameTime = 20;
        }

        maxPlayers = Integer.parseInt(roomDescribe.getMaxPlayers());
        addPlayer(ctx, creator);
    }

    void addPlayer(ChannelHandlerContext ctx, String player) {
        if(players.size() < maxPlayers) {
            /* добавить в общий список игроков */
            players.put(player, ctx.channel());
            /* добавить в список нераспределённых */
            undistributed.put(player, ctx.channel());
            /* подписать на события */
            recipients.add(ctx.channel());
        }
    }

    public String getCreator() {
        return creator;
    }

    public void setCreator(String creator) {
        this.creator = creator;
    }

    public String getMapName() {
        return mapName;
    }

    public void setMapName(String mapName) {
        this.mapName = mapName;
    }

    public String getRoomName() {
        return roomName;
    }

    public void setRoomName(String roomName) {
        this.roomName = roomName;
    }

    public int getGameTime() {
        return gameTime;
    }

    public void setGameTime(int gameTime) {
        this.gameTime = gameTime;
    }

    public int getMaxPlayers() {
        return maxPlayers;
    }

    public void setMaxPlayers(int maxPlayers) {
        this.maxPlayers = maxPlayers;
    }

    public String getCreatorSessionId() {
        return creatorSessionId;
    }

    public void setCreatorSessionId(String creatorSessionId) {
        this.creatorSessionId = creatorSessionId;
    }

    public boolean getGameState() {
        return gameState;
    }

    public void setGameState(boolean gameState) {
        this.gameState = gameState;
    }
}

RoomManager

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MessageModels;
import io.netty.channel.ChannelHandlerContext;

import static com.spiky.server.ServerMain.*;

public class RoomManager {
    public RoomManager(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        if(room.hasCreateRoom())
        {
            createRoom(ctx, room.getCreateRoom());
        }
    }

    private void createRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom createRoom) {
        /* создать новую комнату на основе этих данных */
        GameRoom gameRoom = new GameRoom(ctx, createRoom);
        /* проверить наличие комнаты с таким владельцем */
        if(!gameRooms.containsKey(gameRoom.getCreator())) {
            gameRooms.put(gameRoom.getCreator(), gameRoom);
            ctx.channel().attr(ROOM_OWNER).set(ctx.channel().attr(CHANNEL_OWNER).get());
            createRoom = createRoom.toBuilder().setRoomName(gameRoom.getRoomName()).setCreator(gameRoom.getCreator()).build();
            /* вернуть клиенту данные комнаты */
            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setCreateRoom(createRoom).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
            /* обновить список комнат у всех */
            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }
}

createRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom createRoom)
Создаёт новую комнату на основе входящих данных. Проверяет наличие комнаты с таким владельцем, если игрок уже создал одну комнату он не может создать вторую одновременно. Вернуть клиенту данные комнаты и обновить список комнат у всех (тип ответа CreateRoom).

На клиенте сообщение попадает в UGameRoom::Handler:

if (room.has_createroom())
{
	USpikyGameInstance::DifferentMix->wMainMenuWidgets->AddRoom(NewRoom(room.createroom()));

	// если это создатель то открыть комнату, если нет просто обновить список в главном меню
	if (room.createroom().creator() == USpikyGameInstance::userLogin)
	{
		roomCreator = room.createroom().creator();
		roomName = room.createroom().roomname();
		USpikyGameInstance::DifferentMix->ShowGameRoom();
	}
}

Добавим возможность обновлять список комнат в главном меню. Каждый элемент списка с комнатами это кнопка с текстом. Для того чтобы привязать реакцию на нажатие для каждой кнопки мы расширим класс UButton и делегируем нажатие на одну функцию, которая будет отправлять на сервер желание подписаться и имя целевой комнаты:

std::shared_ptr<SubscribeRoom> subscribe(new SubscribeRoom);
subscribe->set_subscribe(true);
subscribe->set_roomname(TCHAR_TO_UTF8(*textBlock->GetText().ToString()));

Добавим класс RoomListUnit в UI:

RoomListUnit

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"

#include "Runtime/UMG/Public/Components/Button.h"
#include "RoomListUnit.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FClickDelegate);

UCLASS()
class SPIKY_CLIENT_API URoomListUnit: public UButton
{
	GENERATED_BODY()

public:

	URoomListUnit();

	UPROPERTY()
	FClickDelegate click;

	UFUNCTION()
	void OnClick();
};


// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "RoomListUnit.h"
#include "Protobufs/GameRoomModels.pb.h"
#include "Runtime/UMG/Public/Components/HorizontalBox.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "MessageEncoder.h"

URoomListUnit::URoomListUnit()
{
	OnClicked.AddDynamic(this, &URoomListUnit::OnClick);
}

void URoomListUnit::OnClick()
{
	std::shared_ptr<Room> room(new Room);

	UHorizontalBox* horBox = Cast<UHorizontalBox>(GetChildAt(0));
	UTextBlock* textBlock = Cast<UTextBlock>(horBox->GetChildAt(0));

	std::shared_ptr<SubscribeRoom> subscribe(new SubscribeRoom);
	subscribe->set_subscribe(true);
	subscribe->set_roomname(TCHAR_TO_UTF8(*textBlock->GetText().ToString()));

	room->set_allocated_subscriberoom(subscribe.get());

	UMessageEncoder::Send(room.get(), true, true);

	room->release_subscriberoom();
}

Обновим UMainMenuWidgets::AddRoom(URoomListUnit* room):

#include "Runtime/UMG/Public/Components/ScrollBoxSlot.h"
#include "RoomListUnit.h"

void UMainMenuWidgets::AddRoom(URoomListUnit* room) {
	UScrollBoxSlot* gameRoomSlot = Cast<UScrollBoxSlot>(wRoomsScrollBox->AddChild(room));
	gameRoomSlot->SetHorizontalAlignment(HAlign_Fill);
}

URoomListUnit* UGameRoom::NewRoom(CreateRoom room)
	Создаём представление кнопки, выводим параметры комнаты в текстовое поле 
	возвращаем кнопку для вывода её в скрол боксе

UGameRoom::NewRoom

#include "Runtime/UMG/Public/Components/HorizontalBox.h"
#include "Runtime/UMG/Public/Components/HorizontalBoxSlot.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/ButtonSlot.h"
#include "Runtime/UMG/Public/Components/ScrollBox.h"
#include "RoomListUnit.h"

URoomListUnit* UGameRoom::NewRoom(CreateRoom room)
{
	URoomListUnit* button = NewObject<URoomListUnit>(URoomListUnit::StaticClass());
	UHorizontalBox* horBox = NewObject<UHorizontalBox>(UHorizontalBox::StaticClass());

	UTextBlock* wRoomName = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wMapName = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wCreator = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wGameTime = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wMaxPlayers = NewObject<UTextBlock>(UTextBlock::StaticClass());

	wRoomName->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.roomname().c_str()))));
	wMapName->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.mapname().c_str()))));
	wCreator->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.creator().c_str()))));
	wGameTime->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.gametime().c_str()))));
	wMaxPlayers->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.maxplayers().c_str()))));

	UHorizontalBoxSlot* roomNameSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wRoomName));
	UHorizontalBoxSlot* mapNameSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wMapName));
	UHorizontalBoxSlot* creatorSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wCreator));
	UHorizontalBoxSlot* gameTimeSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wGameTime));
	UHorizontalBoxSlot* maxPlayersSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wMaxPlayers));

	roomNameSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	roomNameSlot->SetHorizontalAlignment(HAlign_Center);

	mapNameSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	mapNameSlot->SetHorizontalAlignment(HAlign_Center);

	creatorSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	creatorSlot->SetHorizontalAlignment(HAlign_Center);

	gameTimeSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	gameTimeSlot->SetHorizontalAlignment(HAlign_Center);

	maxPlayersSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	maxPlayersSlot->SetHorizontalAlignment(HAlign_Center);

	UButtonSlot* buttonSlot = Cast<UButtonSlot>(button->AddChild(horBox));
	buttonSlot->SetHorizontalAlignment(HAlign_Fill);
	buttonSlot->SetVerticalAlignment(VAlign_Fill);

	return button;
}

Список комнат обновляется на подобии чата, на основе подписок, добавим в начальное состояние список уже существующих комнат, после входа игрок подписывается на обновления.
Откроем Login на сервере и добавим игрока в список рассылки:

if(!user.isEmpty()) 
    ctx.channel().attr(CHANNEL_OWNER).set(user.get(0).getLogin());
    /* подписаться на обновления списка комнат */
    roomListUpdateSubscribers.add(ctx.channel());

Добавим отправку списка комнат на данный момент в начальном состоянии:

/* прислать текущее состояние списка комнат */
for (Map.Entry<String,GameRoom> pair : gameRooms.entrySet()) {
    /* прислать только те комнаты которые не в процессе игры */
    if(!pair.getValue().getGameState()) {
        GameRoomModels.CreateRoom room = GameRoomModels.CreateRoom.newBuilder()
                .setRoomName(pair.getValue().getRoomName())
                .setMapName(pair.getValue().getMapName())
                .setCreator(pair.getValue().getCreator())
                .setGameTime(pair.getValue().getGameTime() + " minutes")
                .setMaxPlayers(pair.getValue().getMaxPlayers() + " players")
                .build();
        initialState.addCreateRoom(room); }
}

Тоже самое в Registration:

public void saveUser(ChannelHandlerContext ctx, RegistrationLoginModels.Registration registration) {
…
ctx.channel().attr(CHANNEL_OWNER).set(user.getLogin());

/* подписаться на обновления списка комнат */
roomListUpdateSubscribers.add(ctx.channel());

/* прислать текущее состояние списка комнат */
for (Map.Entry<String,GameRoom> pair : gameRooms.entrySet()) {
…
}

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

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 13

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 14

В зависимости от того нажата кнопка или нет, меняется её цвет, для этого подгрузим ресурсы изображений в DiffrentMix и добавим функцию отображающую комнату:

Изменения

//.h

void ShowGameRoom();

UTexture2D* EmptyImage = nullptr;
UTexture2D* GreenImage = nullptr;

//.cpp
// в конструкторе

	static ConstructorHelpers::FObjectFinder<UTexture2D> EmptyImageRef(TEXT("Texture2D'/Game/ProjectResources/Images/empty.empty'"));
	EmptyImage = EmptyImageRef.Object;

	static ConstructorHelpers::FObjectFinder<UTexture2D> GreenImageRef(TEXT("Texture2D'/Game/ProjectResources/Images/G.G'"));
	GreenImage = GreenImageRef.Object;

void UDifferentMix::ShowGameRoom()
{
	HideAllWidgets();
	// закрыть чат, если открыт
	wMainMenuChatWidgets->SetVisibility(ESlateVisibility::Hidden);
	wMainMenuWidgets->bChatOpen = false;

	wGameRoomWidgets->SetVisibility(ESlateVisibility::Visible);
	wGameRoomWidgets->wToFirstTeamButton->SetVisibility(ESlateVisibility::Visible);
	wGameRoomWidgets->wToSecondTeamButton->SetVisibility(ESlateVisibility::Visible);
	wGameRoomWidgets->wToUndistributedTeamButton->SetVisibility(ESlateVisibility::Visible);
	wGameRoomWidgets->wStartButton->SetVisibility(ESlateVisibility::Visible);

	wGameRoomWidgets->AddPlayer(USpikyGameInstance::userLogin.c_str(), "undistributed");
}

Добавим исходники нового виджета, для начала элемент списка, кастомная кнопка UGameRoomUserUnit. Тут мы привязываем событие нажатия на кнопку, меняем её стиль и добавляем игрока в список выделенных, с помощью которого потом будем перемещать по спискам:

GameRoomUserUnit

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Components/Button.h"
#include <vector>
#include "GameRoomUserUnit.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FClickDelegateGameRoom);

UCLASS()
class SPIKY_CLIENT_API UGameRoomUserUnit: public UButton
{
	GENERATED_BODY()

public:

	UGameRoomUserUnit();

	UPROPERTY()
	FClickDelegateGameRoom click;

	UFUNCTION()
	void OnClick();

	bool bSelect = false;

	static std::vector<std::pair<FString, FString>> select_players;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "GameRoomUserUnit.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include <algorithm>
#include "GameRoom.h"

std::vector<std::pair<FString, FString>> UGameRoomUserUnit::select_players;

UGameRoomUserUnit::UGameRoomUserUnit()
{
	OnClicked.AddDynamic(this, &UGameRoomUserUnit::OnClick);
}

void UGameRoomUserUnit::OnClick()
{
	// не создатель не может выбирать элементы списка
	if (UGameRoom::roomCreator != USpikyGameInstance::userLogin) return;

	UTextBlock* textBlock = Cast<UTextBlock>(GetChildAt(0));

	FSlateBrush greenBackgroundImage;
	greenBackgroundImage.SetResourceObject((UObject*)(USpikyGameInstance::DifferentMix->GreenImage));

	FSlateBrush emptyBackgroundImage;
	emptyBackgroundImage.SetResourceObject((UObject*)(USpikyGameInstance::DifferentMix->EmptyImage));

	FString player_name = textBlock->GetText().ToString();;
	FString team_name = GetParent()->GetName();

	std::pair<FString, FString> item = std::make_pair(player_name, team_name);

	if (!bSelect)
	{
		bSelect = true;

		WidgetStyle.Pressed = greenBackgroundImage;
		WidgetStyle.Hovered = greenBackgroundImage;
		WidgetStyle.Normal  = greenBackgroundImage;

		select_players.push_back(item);
	}
	else
	{
		bSelect = false;

		WidgetStyle.Pressed = emptyBackgroundImage;
		WidgetStyle.Hovered = emptyBackgroundImage;
		WidgetStyle.Normal  = emptyBackgroundImage;

		auto it = std::find(select_players.begin(), select_players.end(), item);

		if (it == select_players.end()) 
		{ // not in vector 
		}
		else
		{
			auto index = std::distance(select_players.begin(), it);
			select_players.erase(select_players.begin() + index);
		}
	}

	GLog->Log(FString::FromInt(select_players.size()));
}

GameRoomWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "GameRoomWidgets.generated.h"

class UScrollBox;
class UButton;
class UMultiLineEditableTextBox;
class URichText;

UCLASS()
class SPIKY_CLIENT_API UGameRoomWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	UScrollBox* wUndistributedTeamScrollBox = nullptr;
	UScrollBox* wFirstTeamScrollBox = nullptr;
	UScrollBox* wSecondTeamScrollBox = nullptr;

	UButton* wToFirstTeamButton = nullptr;
	UButton* wToSecondTeamButton = nullptr;
	UButton* wToUndistributedTeamButton = nullptr;

	UButton* wStartButton = nullptr;
	UButton* wGameInfoButton = nullptr;
	UButton* wCloseButton = nullptr;
	UButton* wEnterButton = nullptr;

	UScrollBox * wChatScrollBox = nullptr;
	UMultiLineEditableTextBox* wChatTextBox = nullptr;

	UFUNCTION()
	void StartButtonClicked();

	UFUNCTION()
	void GameInfoButtonClicked();

	UFUNCTION()
	void ToFirstTeamClicked();

	UFUNCTION()
	void ToSecondTeamClicked();

	UFUNCTION()
	void ToUndistributedClicked();

	UFUNCTION()
	void CloseButtonClicked();

	UFUNCTION()
	void EnterButtonClicked();

	void NewMessage(URichText* richText);

	void ScrollToEnd();

	void AddPlayer(FString player, FString team);
	void RemovePlayer(FString player, FString team);
	void RemoveAddPlayer(FString player, FString sourceTeam, FString targetTeam);
	void TeamSelector(FString team);
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "GameRoomWidgets.h"
#include "Runtime/UMG/Public/Components/ScrollBox.h"
#include "Runtime/UMG/Public/Components/ScrollBoxSlot.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/MultiLineEditableTextBox.h"
#include "GameRoomUserUnit.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "Protobufs/GameRoomModels.pb.h"
#include "GameRoom.h"
#include "MessageEncoder.h"
#include "MainMenuWidgets.h"
#include "CreateRoomWidgets.h"
#include "RichText.h"
#include "Runtime/Engine/Public/TimerManager.h"

void UGameRoomWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wFirstTeamScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("team1")));
	wSecondTeamScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("team2")));
	wUndistributedTeamScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("undistributed")));

	wToFirstTeamButton = Cast<UButton>(GetWidgetFromName(TEXT("ToFTButton")));
	wToFirstTeamButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::ToFirstTeamClicked);

	wToSecondTeamButton = Cast<UButton>(GetWidgetFromName(TEXT("ToSTButton")));
	wToSecondTeamButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::ToSecondTeamClicked);

	wToUndistributedTeamButton = Cast<UButton>(GetWidgetFromName(TEXT("ToUndistributedButton")));
	wToUndistributedTeamButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::ToUndistributedClicked);

	wCloseButton = Cast<UButton>(GetWidgetFromName(TEXT("CloseButton")));
	wCloseButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::CloseButtonClicked);

	wEnterButton = Cast<UButton>(GetWidgetFromName(TEXT("EnterButton")));
	wEnterButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::EnterButtonClicked);

	wChatScrollBox = Cast<UScrollBox>(GetWidgetFromName(TEXT("ChatScrollBox")));
	wChatTextBox = Cast<UMultiLineEditableTextBox>(GetWidgetFromName(TEXT("ChatTextBox")));

	wStartButton = Cast<UButton>(GetWidgetFromName(TEXT("StartButton")));
	wStartButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::StartButtonClicked);

	wGameInfoButton = Cast<UButton>(GetWidgetFromName(TEXT("GameInfoButton")));
	wGameInfoButton->OnClicked.AddDynamic(this, &UGameRoomWidgets::GameInfoButtonClicked);
}

void UGameRoomWidgets::AddPlayer(FString player, FString team)
{
	UGameRoomUserUnit* button = NewObject<UGameRoomUserUnit>(this, UGameRoomUserUnit::StaticClass());
	UTextBlock* textBlock = NewObject<UTextBlock>(this, UTextBlock::StaticClass());

	textBlock->SetText(FText::FromString(player));
	button->AddChild(textBlock);

	FSlateBrush emptyBackgroundImage;
	emptyBackgroundImage.SetResourceObject((UObject*)(USpikyGameInstance::DifferentMix->EmptyImage));
	button->WidgetStyle.Normal = emptyBackgroundImage;
	button->WidgetStyle.Hovered = emptyBackgroundImage;
	button->WidgetStyle.Pressed = emptyBackgroundImage;

	UScrollBoxSlot* buttonSlot = nullptr;

	if (team == "team1")
	{
		buttonSlot = Cast<UScrollBoxSlot>(wFirstTeamScrollBox->AddChild(button));
	}
	else if (team == "team2")
	{
		buttonSlot = Cast<UScrollBoxSlot>(wSecondTeamScrollBox->AddChild(button));
	}
	else if (team == "undistributed")
	{
		buttonSlot = Cast<UScrollBoxSlot>(wUndistributedTeamScrollBox->AddChild(button));
	}

	buttonSlot->SetHorizontalAlignment(HAlign_Fill);
}

void UGameRoomWidgets::RemovePlayer(FString player, FString team)
{
	UScrollBox* targetScrollBox = nullptr;

	if (team == "team1")
	{
		targetScrollBox = wFirstTeamScrollBox;
	}
	else if (team == "team2")
	{
		targetScrollBox = wSecondTeamScrollBox;
	}
	else if (team == "undistributed")
	{
		targetScrollBox = wUndistributedTeamScrollBox;
	}

	for (size_t i = 0; i < targetScrollBox->GetChildrenCount(); i++)
	{
		UGameRoomUserUnit* button = Cast<UGameRoomUserUnit>(targetScrollBox->GetChildAt(i));
		UTextBlock* textBlock = Cast<UTextBlock>(button->GetChildAt(0));
		if (textBlock->GetText().ToString() == player) targetScrollBox->RemoveChildAt(i);
	}

	// удалить из списка выделенных игроков, если есть
	auto it = UGameRoomUserUnit::select_players.begin();
	// пройтись по выделенным игрокам в списке
	while (it != UGameRoomUserUnit::select_players.end())
	{
		if (it->first == player)
		{
			it = UGameRoomUserUnit::select_players.erase(it);
		}
		else
		{
			++it;
		}
		GLog->Log(FString::FromInt(UGameRoomUserUnit::select_players.size()));
	}
}

void UGameRoomWidgets::RemoveAddPlayer(FString player, FString sourceTeam, FString targetTeam)
{
	// сняли выделение с элементов, если они в целевом списке
	UScrollBox * targetScrollBox = nullptr;

	if (targetTeam == "team1") targetScrollBox = wFirstTeamScrollBox;
	if (targetTeam == "team2") targetScrollBox = wSecondTeamScrollBox;
	if (targetTeam == "undistributed") targetScrollBox = wUndistributedTeamScrollBox;

	for (size_t i = 0; i < targetScrollBox->GetChildrenCount(); i++)
	{
		UGameRoomUserUnit * entity = Cast<UGameRoomUserUnit>(targetScrollBox->GetChildAt(i));
		FString str = Cast<UTextBlock>(entity->GetChildAt(0))->GetText().ToString();

		for (auto const& e : UGameRoomUserUnit::select_players)
			if (str == e.first) entity->OnClick();
	}

	if (targetTeam == "team1" && sourceTeam != targetTeam)
	{
		RemovePlayer(player, sourceTeam);
		AddPlayer(player, targetTeam);
	}

	if (targetTeam == "team2" && sourceTeam != targetTeam)
	{
		RemovePlayer(player, sourceTeam);
		AddPlayer(player, targetTeam);
	}

	if (targetTeam == "undistributed" && sourceTeam != targetTeam)
	{
		RemovePlayer(player, sourceTeam);
		AddPlayer(player, targetTeam);
	}

	// удалить из списка выделенных игроков, если есть
	auto it = UGameRoomUserUnit::select_players.begin();

	while (it != UGameRoomUserUnit::select_players.end())
	{
		if (it->first == player)
		{
			it = UGameRoomUserUnit::select_players.erase(it);
		}
		else
		{
			++it;
		}
		GLog->Log(FString::FromInt(UGameRoomUserUnit::select_players.size()));
	}
}

void UGameRoomWidgets::TeamSelector(FString team)
{
	std::string targetTeam = TCHAR_TO_UTF8(*team);
	// отправить серверу список выбранных игроков
	std::shared_ptr<RoomDescribe> roomDescribe(new RoomDescribe);

	for (auto s : UGameRoomUserUnit::select_players)
	{
		if (s.second == "undistributed")
		{
			TeamPlayer* teamPlayer = roomDescribe->add_undistributed();
			teamPlayer->set_player_name(TCHAR_TO_UTF8(*s.first));
		}
		else if (s.second == "team1")
		{
			TeamPlayer* teamPlayer = roomDescribe->add_team1();
			teamPlayer->set_player_name(TCHAR_TO_UTF8(*s.first));
		}
		else if (s.second == "team2")
		{
			TeamPlayer* teamPlayer = roomDescribe->add_team2();
			teamPlayer->set_player_name(TCHAR_TO_UTF8(*s.first));
		}
	}

	std::shared_ptr<RoomUpdate> roomUpdate(new RoomUpdate);
	roomUpdate->set_allocated_roomdescribe(roomDescribe.get());
	roomUpdate->set_targetteam(targetTeam);
	roomUpdate->set_roomname(UGameRoom::roomName);

	std::shared_ptr<Room> room(new Room);
	room->set_allocated_roomupdate(roomUpdate.get());

	UMessageEncoder::Send(room.get(), true, true);

	room->release_roomupdate();
	roomUpdate->release_roomdescribe();
}

void UGameRoomWidgets::StartButtonClicked()
{
	std::shared_ptr<Room> room(new Room);
	room->set_startgame(true);
	room->set_roomname(UGameRoom::roomName);
	UMessageEncoder::Send(room.get(), true, true);
}

void UGameRoomWidgets::GameInfoButtonClicked()
{
	// TODO
}

void UGameRoomWidgets::ToFirstTeamClicked()
{
	TeamSelector("team1");
}

void UGameRoomWidgets::ToUndistributedClicked()
{
	TeamSelector("undistributed");
}

void UGameRoomWidgets::ToSecondTeamClicked()
{
	TeamSelector("team2");
}

void UGameRoomWidgets::CloseButtonClicked()
{
	if (UGameRoom::roomCreator != USpikyGameInstance::userLogin)
	{
		// сбросить состояние виджета для последующего использования
		wFirstTeamScrollBox->ClearChildren();
		wSecondTeamScrollBox->ClearChildren();
		wUndistributedTeamScrollBox->ClearChildren();
		wChatScrollBox->ClearChildren();
		// вернуть игрока в главное меню
		USpikyGameInstance::DifferentMix->ShowMainMenuScreen();
		// удалиться из комнаты
		std::shared_ptr<SubscribeRoom> unsubscribe(new SubscribeRoom);
		unsubscribe->set_subscribe(false);
		unsubscribe->set_roomname(UGameRoom::roomName);

		std::shared_ptr<Room> room(new Room);
		room->set_allocated_subscriberoom(unsubscribe.get());

		UMessageEncoder::Send(room.get(), true, true);
		room->release_subscriberoom();
	}
	else
	{
		std::shared_ptr<RoomsListUpdate> update(new RoomsListUpdate);
		update->set_deleteroom(true);

		std::shared_ptr<Room> room(new Room);
		room->set_allocated_roomslistupdate(update.get());

		UMessageEncoder::Send(room.get(), true, true);
		room->release_roomslistupdate();
	}
}

void UGameRoomWidgets::EnterButtonClicked()
{
	if (wChatTextBox->GetText().ToString() == "") return;

	std::shared_ptr<Room> room(new Room);
	std::shared_ptr<RoomUpdate> roomUpdate(new RoomUpdate);
	std::shared_ptr<RoomDescribe> roomDescribe(new RoomDescribe);
	std::shared_ptr<Chat> chat(new Chat);

	chat->set_text(TCHAR_TO_UTF8(*wChatTextBox->GetText().ToString()));
	wChatTextBox->SetText(FText::FromString(""));

	roomDescribe->set_allocated_chat(chat.get());
	roomUpdate->set_allocated_roomdescribe(roomDescribe.get());
	roomUpdate->set_roomname(UGameRoom::roomName);
	room->set_allocated_roomupdate(roomUpdate.get());

	UMessageEncoder::Send(room.get(), true, true);

	room->release_roomupdate();
	roomUpdate->release_roomdescribe();
	roomDescribe->release_chat();
}

void UGameRoomWidgets::NewMessage(URichText* richText)
{
	UScrollBoxSlot* richTextSlot = Cast<UScrollBoxSlot>(wChatScrollBox->AddChild(richText));
	richTextSlot->SetPadding(FMargin(10, 5, 10, 5));
	wChatScrollBox->ScrollToEnd();

	FTimerHandle timerHandle;
	GetWorld()->GetTimerManager().SetTimer(timerHandle, this, &UGameRoomWidgets::ScrollToEnd, .01f, false);
}

void UGameRoomWidgets::ScrollToEnd()
{
	wChatScrollBox->ScrollToEnd();
}

Виджета который мог бы выполнять перемещение списков в Unreal нет, придётся создать свой, вот описание каждой функции:

void UGameRoomWidgets::NativeConstruct()
	привязываем графические элементы к коду
	
void UGameRoomWidgets::AddPlayer(FString player, FString team)
	добавляем игрока в один из списков
		создаём кнопку с его именем
		
void UGameRoomWidgets::RemovePlayer(FString player, FString team)
	удаляем игрока из списка

void UGameRoomWidgets::RemoveAddPlayer(FString player, FString sourceTeam, FString targetTeam)
	удаляем игрока из списка и добавляем тут же в другой
	
void UGameRoomWidgets::TeamSelector(FString team)
	вызывается после нажатия на одну из кнопок распределения
	создаём RoomDescribe, в котором отправляем серверу список выбранных игроков
	
void UGameRoomWidgets::StartButtonClicked()
	доступна только владельцу комнаты
	отправляем всем находящимся в комнате сигнал о начале игры
		нераспределённые игроки возвращаются в главное меню
		
void UGameRoomWidgets::CloseButtonClicked()
	если не владелец комнаты
		сбросить состояние виджета для последующего использования
		вернуть игрока в главное меню
		отписаться из комнаты
	если владелец 
		отправить сигнал на сервер б удалении комнаты
		
void UGameRoomWidgets::EnterButtonClicked()
	отправка сообщений в чат
	
void UGameRoomWidgets::NewMessage(URichText* richText)
	добавление новых сообщений в чат
	
void UGameRoomWidgets::ScrollToEnd()
	решение бага с прокруткой вниз

Теперь добавим новый виджет в DifferentMix:

GameRoomWidgets в DifferentMix

.h

class UGameRoomWidgets;		

UGameRoomWidgets* tmpGameRoomRef;

UGameRoomWidgets* wGameRoomWidgets;

.cpp

#include "GameRoomWidgets.h"
...
static ConstructorHelpers::FClassFinder<UGameRoomWidgets> gameRoomWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/GameRoom_W.GameRoom_W_C'"));

if (gameRoomWidgets.Class != NULL)
{
	tmpGameRoomRef = gameRoomWidgets.Class->GetDefaultObject<UGameRoomWidgets>();
}
...
wGameRoomWidgets = CreateWidget<UGameRoomWidgets>(GetWorld(), tmpGameRoomRef->GetClass());
gameRoomSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wGameRoomWidgets));
gameRoomSlot->SetZOrder(1);
gameRoomSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
gameRoomSlot->SetOffsets(FMargin(0, 0, 0, 0));
wGameRoomWidgets->SetVisibility(ESlateVisibility::Hidden);

Назначим виджету GameRoom родителя GameRoomWidgets в редакторе. В UGameRoom::Handler() раскомментируем USpikyGameInstance::DifferentMix->ShowGameRoom(); Игрок после создания комнаты попадает в неё и находится в списке нераспределённых. Теперь сделаем так чтобы владелец мог удалить комнату, выйдя из неё. После нажатия на UGameRoomWidgets::CloseButtonClicked() отправляет серверу:

std::shared_ptr<RoomsListUpdate> update(new RoomsListUpdate);
update->set_deleteroom(true);

На сервере в RoomManager добавляем:

else if(room.hasRoomsListUpdate()) {
    if(room.getRoomsListUpdate().hasField(deleteRoom_room)) {
        deleteRoom(ctx);
    }
}

Функция deleteRoom(ChannelHandlerContext ctx), проверяет если владелец канала владеет комнатой, и если она не в состоянии игры, тогда удаляет комнату из списка и сообщает об этом подписчикам. Нам понадобится способ удалять игроков из комнаты если потеряли связь, добавим в RoomManager:

public static void clearRoomData(ChannelHandlerContext ctx)
	проверяем владеет ли игрок какой-то комнатой
		удалить комнату, если игра не запущенна
		сообщить подписантам об этом
	за игроком нет созданных комнат, тогда проверить игрока в списках существующих комнат 
	и удалить если он там есть и только в том случае если игра не запущенна 

private static void roomUnsubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room)
	находит комнату и удаляется из неё если она не в бою, сообщает об этом всем

RoomManager

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */
package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MessageModels;
import io.netty.channel.ChannelHandlerContext;

import java.util.Objects;

import static com.spiky.server.ServerMain.*;
import static com.spiky.server.utils.Descriptors.deleteRoom_room;

public class RoomManager {
    public RoomManager(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        if(room.hasCreateRoom())
        {
            createRoom(ctx, room.getCreateRoom());
        }
        else if(room.hasRoomsListUpdate()) {
            if(room.getRoomsListUpdate().hasField(deleteRoom_room)) {
                deleteRoom(ctx);
            }
        }
    }

    private void createRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom createRoom) {
        /* создать новую комнату на основе этих данных */
        GameRoom gameRoom = new GameRoom(ctx, createRoom);
        /* проверить наличие комнаты с таким владельцем */
        if(!gameRooms.containsKey(gameRoom.getCreator())) {
            gameRooms.put(gameRoom.getCreator(), gameRoom);
            ctx.channel().attr(ROOM_OWNER).set(ctx.channel().attr(CHANNEL_OWNER).get());
            createRoom = createRoom.toBuilder().setRoomName(gameRoom.getRoomName()).setCreator(gameRoom.getCreator()).build();
            /* вернуть клиенту данные комнаты */
            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setCreateRoom(createRoom).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
            /* обновить список комнат у всех */
            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void deleteRoom(ChannelHandlerContext ctx) {
        /* если владелец канала владеет комнатой */
        String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();
        if(gameRooms.containsKey(channel_owner)) {
            GameRoom gameRoom = gameRooms.get(channel_owner);
            /* ничего не можем сделать если комната запущенна */
            if(gameRoom.getGameState()) return;
            /* удалить из списка комнат */
            gameRooms.remove(channel_owner);
            /* сообщить подписантам об этом */
            GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setRoomOwner(gameRoom.getCreator()).build();

            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void clearRoomData(ChannelHandlerContext ctx) {
        String player = ctx.channel().attr(CHANNEL_OWNER).get();

        if(gameRooms.containsKey(player)) {
            GameRoom gameRoom = gameRooms.get(player);
            System.out.println(gameRoom.getRoomName());
            /* удалить из списка комнат, если игра не запущенна */
            if(!gameRoom.getGameState()) {
                gameRooms.remove(player);
                /* сообщить подписантам об этом */
                GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setDeleteRoom(true).setRoomName(gameRoom.getRoomName()).build();

                GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                roomListUpdateSubscribers.writeAndFlush(wrapper);
            }
        }
        /* нет комнат, тогда проверить игрока в списках и удалить если он там есть */
        else {
            for(String owner : gameRooms.keySet()) {
                if (gameRooms.get(owner).players.containsKey(player)) {
                    /* комната не в процессе игры */
                    if(!gameRooms.get(owner).getGameState()) {
                        GameRoom gameRoom = gameRooms.get(owner);
                        GameRoomModels.SubscribeRoom.Builder subscribe = GameRoomModels.SubscribeRoom.newBuilder()
                                .setRoomName(gameRoom.getRoomName());

                        if(gameRoom.team1.containsKey(player)) {
                            subscribe.setTeam("team1");
                        } else if (gameRoom.team2.containsKey(player)) {
                            subscribe.setTeam("team2");
                        } else if (gameRoom.undistributed.containsKey(player)) {
                            subscribe.setTeam("undistributed");
                        }

                        GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe.build()).build();
                        roomUnsubscribe(ctx, room);
                    }
                }
            }
        }
    }

    private static void roomUnsubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), room.getSubscribeRoom().getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String player = ctx.channel().attr(CHANNEL_OWNER).get();
                String team = "";

                /* игрок не может удалиться из комнаты если она в бою */
                if(!gameRoom.getGameState()) {
                    gameRoom.players.remove(player);

                    if(gameRoom.team1.containsKey(player)) {
                        team = "team1";
                        gameRoom.team1.remove(player);
                    } else if(gameRoom.team2.containsKey(player)) {
                        team = "team2";
                        gameRoom.team2.remove(player);
                    }  else if(gameRoom.undistributed.containsKey(player)) {
                        team = "undistributed";
                        gameRoom.undistributed.remove(player);
                    }

                    GameRoomModels.SubscribeRoom subscribe = room.getSubscribeRoom().toBuilder().setPlayer(player).setTeam(team).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    System.out.println(out);

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }
}

Нам нужно определиться с её вызовом, Netty не всегда обрабатывает закрытие соединения, но имеется удобный обработчик IdleStateHandler(30, 0, 0), первый параметр это timeout на входящие сообщения, второй на исходящие, третий на всё, если ноль не учитываем. Тут если сообщения от клиента не приходят в течении 30 секунд соединение закрывается, сессия с БД игровые данные очищаются.

Добавим новый обработчик в ServerInitializer, его конечный вид:

ServerInitializer

public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        /* отладка */
        //pipeline.addLast(new LoggingHandler(LogLevel.INFO));

        /* разворачиваем сообщения */
        // Decoders protobuf
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        pipeline.addLast(new ProtobufDecoder(MessageModels.Wrapper.getDefaultInstance()));
        /* оборачиваем сообщения */
        // Encoder protobuf
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new ProtobufEncoder());

        /* The connection is closed when there is no inbound traffic for N seconds */
        pipeline.addLast(new IdleStateHandler(30, 0, 0));
        /* зашифруем исходящее сообщение  */
        pipeline.addLast(new EncryptHandler());
        /* расшифруем входящее сообщение */
        pipeline.addLast(new DecryptHandler());
    }
}

Удобней всего заниматься очищение данных в DecryptHandler:

private void clearPlayerData(ChannelHandlerContext ctx)
           очистка данных и закрытие сессии.

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
           обработчик ошибок

public void channelUnregistered(ChannelHandlerContext ctx) throws Exception
           канал закрылся

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
           ф-я которая выполняет какие-то действия если не было связи с клиентом 30 секунд

Обновленный DecryptHandler

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */
package com.spiky.server.tcp.handlers;

import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MainMenuModels;
import com.spiky.server.protomodels.MessageModels;
import com.spiky.server.protomodels.RegistrationLoginModels;
import com.spiky.server.tcp.logics.*;
import com.spiky.server.utils.Cryptography;
import com.spiky.server.utils.SessionUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.hibernate.Session;
import org.hibernate.Transaction;

import java.util.List;

import static com.spiky.server.ServerMain.CRYPTOGRAPHY;
import static com.spiky.server.ServerMain.HIBER_SESSION;
import static com.spiky.server.ServerMain.HIBER_TRANSACTION;
import static com.spiky.server.tcp.logics.RoomManager.clearRoomData;
import static com.spiky.server.utils.Descriptors.*;

public class DecryptHandler extends MessageToMessageDecoder<MessageModels.Wrapper> {

    private Session session = new SessionUtil().getSession();
    private Transaction transaction  = session.beginTransaction();
    private Cryptography cryptography = new Cryptography();

    /* инициализируем один раз */
    private boolean bInit = false;

    /* сохраняем значения необходимые во всех обработчиках этого канала*/
    private void init(ChannelHandlerContext ctx) {
        if(!bInit) {
            bInit = true;

            ctx.channel().attr(HIBER_SESSION).set(session);
            ctx.channel().attr(HIBER_TRANSACTION).set(transaction);
            ctx.channel().attr(CRYPTOGRAPHY).set(cryptography);
        }
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper, List<Object> list) throws Exception {
        init(ctx);
        /* некоторые сообщения приходят нешифрованными */
        if(wrapper.hasCryptogramWrapper())
        {
            if(wrapper.getCryptogramWrapper().hasField(registration_cw))
            {
                byte[] cryptogram = wrapper.getCryptogramWrapper().getRegistration().toByteArray();
                byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

                RegistrationLoginModels.Registration registration = RegistrationLoginModels.Registration.parseFrom(original);
                new Registration().saveUser(ctx, registration);
            }
            else if (wrapper.getCryptogramWrapper().hasField(login_cw))
            {
                byte[] cryptogram = wrapper.getCryptogramWrapper().getLogin().toByteArray();
                byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

                RegistrationLoginModels.Login login = RegistrationLoginModels.Login.parseFrom(original);
                new Login().hasUser(ctx, login);
            }
            else if(wrapper.getCryptogramWrapper().hasField(mainMenu_cw))
            {
                byte[] cryptogram = wrapper.getCryptogramWrapper().getMainMenu().toByteArray();
                byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

                MainMenuModels.MainMenu mainMenu = MainMenuModels.MainMenu.parseFrom(original);
                new MainMenu(ctx, mainMenu);
            }
            else if(wrapper.getCryptogramWrapper().hasField(room_cw))
            {
                byte[] cryptogram = wrapper.getCryptogramWrapper().getRoom().toByteArray();
                byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey());

                GameRoomModels.Room room = GameRoomModels.Room.parseFrom(original);
                new RoomManager(ctx, room);
            }
        }
        else if(wrapper.hasInputChecking())
        {
            new InputChecking(ctx, wrapper);
        }
        else if(wrapper.hasRegistration())
        {
            new Registration(ctx, wrapper.getRegistration());
        }
        else if(wrapper.hasLogin())
        {
            new Login(ctx, wrapper.getLogin());
        }
    }

    private void clearPlayerData(ChannelHandlerContext ctx) {
        if(session.isOpen()) session.close();
        clearRoomData(ctx);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        clearPlayerData(ctx);
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        super.channelUnregistered(ctx);
        System.out.println("Close connection id: " + ctx.channel().id().asShortText() + " cause ChannelUnregistered");
        clearPlayerData(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            if (e.state() == IdleState.READER_IDLE) {
                System.out.println("Close connection id: " + ctx.channel().id().asShortText() + " cause READER_IDLE");
                clearPlayerData(ctx);
            }
        }
    }
}

На клиенте удалим комнату и вернём игроков в главное меню:

/* если владелец комнаты решил её удалить */
void UGameRoom::DeleteRoom(RoomsListUpdate update)
	удалить комнату из списка
	вернуть игроков которые были в этой комнате в главное меню

/* ф-я которая вызывается при запуске игрового процесса, не распределённые игроки возвращаются в главное меню, или комната просто удаляется из списка */
void UGameRoom::DeleteRoom(std::string name)
	удалить комнату из списка
	проверить находимся ли мы в этой комнате
	если игрок находится в не распределённом списке и не является создателем
		вернуть игроков которые были не распределены в главное меню
		сбросить состояние виджета для последующего использования

UGameRoom::DeleteRoom

#include "GameRoomWidgets.h"
#include "GameRoomUserUnit.h"
#include "CreateRoomWidgets.h"

void UGameRoom::DeleteRoom(RoomsListUpdate update)
{
	// удалить комнату из списка
	for (size_t i = 0; i < USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildrenCount(); i++)
	{
		URoomListUnit* listUnit = Cast<URoomListUnit>(USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildAt(i));
		UHorizontalBox* horBox = Cast<UHorizontalBox>(listUnit->GetChildAt(0));

		UTextBlock* wCreatorName = Cast<UTextBlock>(horBox->GetChildAt(2));

		if (wCreatorName->GetText().ToString() == update.roomowner().c_str())
		{
			USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->RemoveChildAt(i);
		}
	}

	// вернуть игроков которые были в этой комнате в главное меню
	if (roomCreator == update.roomowner().c_str())
	{
		// сбросить состояние виджета для последующего использования
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();
		UGameRoomUserUnit::select_players.clear();
		roomCreator = "";
		roomName = "";

		USpikyGameInstance::DifferentMix->wMainMenuWidgets->SetVisibility(ESlateVisibility::Visible);
		USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
	}
}

void UGameRoom::DeleteRoom(std::string name)
{
	// удалить комнату из списка
	for (size_t i = 0; i < USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildrenCount(); i++)
	{
		URoomListUnit* listUnit = Cast<URoomListUnit>(USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildAt(i));
		UHorizontalBox* horBox = Cast<UHorizontalBox>(listUnit->GetChildAt(0));

		UTextBlock* wCreatorName = Cast<UTextBlock>(horBox->GetChildAt(2));

		if (wCreatorName->GetText().ToString() == name.c_str())
		{
			USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->RemoveChildAt(i);
		}
	}

	// если мы находимся в другой комнате
	if (roomCreator == name)
	{
		// вернуть игроков которые были нераспределены в главное меню
		for (size_t j = 0; j < USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->GetChildrenCount(); j++)
		{
			UGameRoomUserUnit * entity = Cast<UGameRoomUserUnit>(USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->GetChildAt(j));
			FString playerName = Cast<UTextBlock>(entity->GetChildAt(0))->GetText().ToString();

			// если игрок находится в этом списке и не является создателем
			if (playerName == USpikyGameInstance::userLogin.c_str() && playerName != roomCreator.c_str())
			{
				// сбросить состояние виджета для последующего использования
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();
				UGameRoomUserUnit::select_players.clear();
				roomCreator = "";
				roomName = "";

				USpikyGameInstance::DifferentMix->wMainMenuWidgets->SetVisibility(ESlateVisibility::Visible);
				USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
			}
		}
	}
}

void UGameRoom::Handler(Room room)
{
…
	else if (room.has_roomslistupdate())
	{
		if (!room.startgame())
		{
			// удалить комнату из списка, вернуть нераспределённых игроков в меню
			DeleteRoom(room.roomslistupdate());
		}
		else
		{
			DeleteRoom(room.roomslistupdate().roomowner());
		}
	}
}

Обработаем ситуацию когда игрок выбрал комнату, он должен к ней подсоединиться, а списки участников должны обновиться. И ситуацию когда он отправил сигнал отписаться в UGameRoomWidgets::CloseButtonClicked().

В URoomListUnit::OnClick() мы отправляем намерение подписаться и имя целевой комнаты. Обработаем его на сервере. В RoomManager добавим в конструктор новое условие:

else if(room.hasSubscribeRoom()) {
    if(room.getSubscribeRoom().getSubscribe()) {
        roomSubscribe(ctx, room);
    } else {
        roomUnsubscribe(ctx, room);
    }
}

Добавим новую функцию:

private void roomSubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room)
	ищем комнату в которую хотим войти
		проверяем есть ли еще места
			нету возвращаем сигнал ошибки
		получаем имя владельца канала 
			добавить, если игрока если игрока с таким ником нет в комнате
		отправить игроку текущее состояние комнаты, настройки комнаты, и сообщения чата
	обновить списки игроков у всех остальных
		updateRoom(ctx, gameRoom , player);

private void updateRoom(ChannelHandlerContext ctx, GameRoom gameRoom, String player) 
	отправить всем информацию о том что добавился новый игрок, но себе не отправляем
	для этого в Нетти есть ChannelMatchers.isNot

	/* себе не отправляем */
	gameRoom.recipients.writeAndFlush(wrapper, ChannelMatchers.isNot(ctx.channel()));

Добавим еще одно условие обрабатывающее это обновление:

else if(room.hasRoomUpdate()) {
    /* пришло сообщение чата */
    if(room.getRoomUpdate().getRoomDescribe().hasChat()) {
        //chatHandler(ctx, room.getRoomUpdate());
    } else {
        updateRoom(ctx, room.getRoomUpdate());
    }
}

И перегруженную функцию:

private void updateRoom(ChannelHandlerContext ctx, GameRoomModels.RoomUpdate roomUpdate)
        удаляем из старого списка, добавляем в новый, сообщаем об этом клиентам

RoomManager

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MainMenuModels;
import com.spiky.server.protomodels.MessageModels;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelMatchers;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.spiky.server.ServerMain.*;
import static com.spiky.server.utils.Descriptors.deleteRoom_room;

public class RoomManager {
    public RoomManager(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        if(room.hasCreateRoom())
        {
            createRoom(ctx, room.getCreateRoom());
        }
        else if(room.hasRoomsListUpdate()) {
            if(room.getRoomsListUpdate().hasField(deleteRoom_room)) {
                deleteRoom(ctx);
            }
        }
        else if(room.hasSubscribeRoom()) {
            if(room.getSubscribeRoom().getSubscribe()) {
                roomSubscribe(ctx, room);
            } else {
                roomUnsubscribe(ctx, room);
            }
        }
        else if(room.hasRoomUpdate()) {
            /* пришло сообщение чата */
            if(room.getRoomUpdate().getRoomDescribe().hasChat()) {
                //chatHandler(ctx, room.getRoomUpdate());
            } else {
                updateRoom(ctx, room.getRoomUpdate());
            }
        }
    }

    private void createRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom createRoom) {
        /* создать новую комнату на основе этих данных */
        GameRoom gameRoom = new GameRoom(ctx, createRoom);
        /* проверить наличие комнаты с таким владельцем */
        if(!gameRooms.containsKey(gameRoom.getCreator())) {
            gameRooms.put(gameRoom.getCreator(), gameRoom);
            ctx.channel().attr(ROOM_OWNER).set(ctx.channel().attr(CHANNEL_OWNER).get());
            createRoom = createRoom.toBuilder().setRoomName(gameRoom.getRoomName()).setCreator(gameRoom.getCreator()).build();
            /* вернуть клиенту данные комнаты */
            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setCreateRoom(createRoom).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
            /* обновить список комнат у всех */
            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void deleteRoom(ChannelHandlerContext ctx) {
        /* если владелец канала владеет комнатой */
        String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();
        if(gameRooms.containsKey(channel_owner)) {
            GameRoom gameRoom = gameRooms.get(channel_owner);
            /* ничего не можем сделать если комната запущенна */
            if(gameRoom.getGameState()) return;
            /* удалить из списка комнат */
            gameRooms.remove(channel_owner);
            /* сообщить подписантам об этом */
            GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setRoomOwner(gameRoom.getCreator()).build();

            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void clearRoomData(ChannelHandlerContext ctx) {
        String player = ctx.channel().attr(CHANNEL_OWNER).get();

        if(gameRooms.containsKey(player)) {
            GameRoom gameRoom = gameRooms.get(player);
            System.out.println(gameRoom.getRoomName());
            /* удалить из списка комнат, если игра не запущенна */
            if(!gameRoom.getGameState()) {
                gameRooms.remove(player);
                /* сообщить подписантам об этом */
                GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setDeleteRoom(true).setRoomOwner(gameRoom.getCreator()).build();

                GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                roomListUpdateSubscribers.writeAndFlush(wrapper);
            }
        }
        /* нет комнат, тогда проверить игрока в списках и удалить если он там есть */
        else {
            for(String owner : gameRooms.keySet()) {
                if (gameRooms.get(owner).players.containsKey(player)) {
                    /* комната не в процессе игры */
                    if(!gameRooms.get(owner).getGameState()) {
                        GameRoom gameRoom = gameRooms.get(owner);
                        GameRoomModels.SubscribeRoom.Builder subscribe = GameRoomModels.SubscribeRoom.newBuilder()
                                .setRoomName(gameRoom.getRoomName());

                        if(gameRoom.team1.containsKey(player)) {
                            subscribe.setTeam("team1");
                        } else if (gameRoom.team2.containsKey(player)) {
                            subscribe.setTeam("team2");
                        } else if (gameRoom.undistributed.containsKey(player)) {
                            subscribe.setTeam("undistributed");
                        }

                        GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe.build()).build();
                        roomUnsubscribe(ctx, room);
                    }
                }
            }
        }
    }

    private void roomSubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if(Objects.equals(gameRooms.get(owner).getRoomName(), room.getSubscribeRoom().getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                /* вернуть сигнал в случае заполненности комнаты */
                if(gameRoom.players.size() > gameRoom.getMaxPlayers()) {
                    GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setStateCode(1).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    ctx.writeAndFlush(wrapper);
                    return;
                }

                String player = ctx.channel().attr(CHANNEL_OWNER).get();
                /* добавить, если игрока с таким ником нет в комнате */
                if(!gameRoom.players.containsKey(player)) {
                    gameRoom.addPlayer(ctx, player);
                } else {
                    GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setStateCode(2).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    ctx.writeAndFlush(wrapper);
                    return;
                }

                /* вернуть текущее состояние игроков в комнате */
                GameRoomModels.RoomDescribe.Builder builder = GameRoomModels.RoomDescribe.newBuilder();

                for (String p : gameRoom.undistributed.keySet()) {
                    GameRoomModels.TeamPlayer undistributed = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addUndistributed(undistributed);
                }

                for (String p : gameRoom.team1.keySet()) {
                    GameRoomModels.TeamPlayer team1 = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addTeam1(team1);
                }

                for (String p : gameRoom.team2.keySet()) {
                    GameRoomModels.TeamPlayer team2 = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addTeam2(team2);
                }

                GameRoomModels.RoomDescribe roomDescribe = builder.setRoomName(gameRoom.getRoomName())
                        .setMapName(gameRoom.getMapName())
                        .setGameTime(gameRoom.getGameTime() + " minutes")
                        .setMaxPlayers(gameRoom.getMaxPlayers() + " players")
                        .setCreator(gameRoom.getCreator())
                        .build();

                MainMenuModels.Chat.Builder chatBuilder = MainMenuModels.Chat.newBuilder();

                /* создать список последних 100 сообщений */
                for (int i = 0; i < gameRoom.syncMessageList.size(); i++) {
                    MainMenuModels.ChatMessage message = MainMenuModels.ChatMessage.newBuilder()
                            .setTime(gameRoom.syncMessageList.get(i).getTime())
                            .setName(gameRoom.syncMessageList.get(i).getName())
                            .setText(gameRoom.syncMessageList.get(i).getText())
                            .build();
                    chatBuilder.addMessages(message);
                }

                roomDescribe = roomDescribe.toBuilder().setChat(chatBuilder.build()).build();

                System.out.println(roomDescribe);

                GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setRoomDescribe(roomDescribe).build();
                GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                ctx.writeAndFlush(wrapper);

                /* обновить списки у всех остальных */
                updateRoom(ctx, gameRoom , player);

                ctx.channel().attr(ROOM_OWNER).set(gameRoom.getCreator());
            }
        }
    }

    private void updateRoom(ChannelHandlerContext ctx, GameRoomModels.RoomUpdate roomUpdate) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), roomUpdate.getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();

                /* проверяем владеет ли этот пользователь запрошенной комнатой */
                if(Objects.equals(channel_owner, gameRoom.getCreator())) {
                    GameRoomModels.RoomDescribe roomDescribe = roomUpdate.getRoomDescribe();

                    Map<String,Channel> list = new HashMap<>();

                    /* удалить из старых списков */
                    for (GameRoomModels.TeamPlayer p : roomDescribe.getTeam1List()) {
                        list.put(p.getPlayerName(), gameRoom.team1.get(p.getPlayerName()));
                        gameRoom.team1.remove(p.getPlayerName());
                    }

                    for (GameRoomModels.TeamPlayer p : roomDescribe.getTeam2List()) {
                        list.put(p.getPlayerName(), gameRoom.team2.get(p.getPlayerName()));
                        gameRoom.team2.remove(p.getPlayerName());
                    }

                    for (GameRoomModels.TeamPlayer p : roomDescribe.getUndistributedList()) {
                        list.put(p.getPlayerName(), gameRoom.undistributed.get(p.getPlayerName()));
                        gameRoom.undistributed.remove(p.getPlayerName());
                    }

                    /* добавить в целевой */
                    if(Objects.equals(roomUpdate.getTargetTeam(), "team1")) {
                        gameRoom.team1.putAll(list);
                    } else if(Objects.equals(roomUpdate.getTargetTeam(), "team2")) {
                        gameRoom.team2.putAll(list);
                    } else if(Objects.equals(roomUpdate.getTargetTeam(), "undistributed")) {
                        gameRoom.undistributed.putAll(list);
                    }

                    /* обновить списки на клиентах */
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setRoomUpdate(roomUpdate).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }
    
    private void updateRoom(ChannelHandlerContext ctx, GameRoom gameRoom, String player) {
        /* отправить всем информацию о том что добавился новый игрок */
        GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setPlayer(player).build();
        GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

        MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
        MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

        /* себе не отправляем */
        gameRoom.recipients.writeAndFlush(wrapper, ChannelMatchers.isNot(ctx.channel()));
    }

    private static void roomUnsubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), room.getSubscribeRoom().getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String player = ctx.channel().attr(CHANNEL_OWNER).get();
                String team = "";

                /* игрок не может удалиться из комнаты если она в бою */
                if(!gameRoom.getGameState()) {
                    gameRoom.players.remove(player);

                    if(gameRoom.team1.containsKey(player)) {
                        team = "team1";
                        gameRoom.team1.remove(player);
                    } else if(gameRoom.team2.containsKey(player)) {
                        team = "team2";
                        gameRoom.team2.remove(player);
                    }  else if(gameRoom.undistributed.containsKey(player)) {
                        team = "undistributed";
                        gameRoom.undistributed.remove(player);
                    }

                    GameRoomModels.SubscribeRoom subscribe = room.getSubscribeRoom().toBuilder().setPlayer(player).setTeam(team).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    System.out.println(out);

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }
}

На клиенте обновляем UGameRoom::Handler(Room room):

...
else if (room.has_subscriberoom())
{
	Subscribe(room.subscriberoom());
}
else if (room.has_roomupdate())
{
	if (room.roomupdate().roomdescribe().GetReflection()->HasField(room.roomupdate().roomdescribe(), Descriptors::chat_room))
	{
		//UChats * chats = NewObject<UChats>(UChats::StaticClass());
		//chats->Handler(room.roomupdate().roomdescribe().chat(), "gameroom");
	}
	else
	{
		UpdateRoom(room.roomupdate());
	}
}

Обновляем void UGameRoom::Subscribe(SubscribeRoom subscribe), тут обрабатываем ошибку заполненности комнаты, применяем описание комнаты, открываем её скрывая при этом кнопки управления. Или удаляем игрока из комнаты возвращая начальное состояние виджетов.
UGameRoom::UpdateRoom(RoomUpdate update), тут удаляем игрока из какого-то списка, добавляем в какой-то.

Закончим с созданием комнаты, добавив туда чат. На сервере в RoomManager раскомментируем chatHandler(ctx, room.getRoomUpdate()); И добавим функцию chatHandler():

RoomManager

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.GameRoomModels;
import com.spiky.server.protomodels.MainMenuModels;
import com.spiky.server.protomodels.MessageModels;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelMatchers;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.spiky.server.ServerMain.*;
import static com.spiky.server.utils.Descriptors.deleteRoom_room;

public class RoomManager {
    public RoomManager(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        if(room.hasCreateRoom())
        {
            createRoom(ctx, room.getCreateRoom());
        }
        else if(room.hasRoomsListUpdate()) {
            if(room.getRoomsListUpdate().hasField(deleteRoom_room)) {
                deleteRoom(ctx);
            }
        }
        else if(room.hasSubscribeRoom()) {
            if(room.getSubscribeRoom().getSubscribe()) {
                roomSubscribe(ctx, room);
            } else {
                roomUnsubscribe(ctx, room);
            }
        }
        else if(room.hasRoomUpdate()) {
            /* пришло сообщение чата */
            if(room.getRoomUpdate().getRoomDescribe().hasChat()) {
                chatHandler(ctx, room.getRoomUpdate());
            } else {
                updateRoom(ctx, room.getRoomUpdate());
            }
        }
    }

    private void createRoom(ChannelHandlerContext ctx, GameRoomModels.CreateRoom createRoom) {
        /* создать новую комнату на основе этих данных */
        GameRoom gameRoom = new GameRoom(ctx, createRoom);
        /* проверить наличие комнаты с таким владельцем */
        if(!gameRooms.containsKey(gameRoom.getCreator())) {
            gameRooms.put(gameRoom.getCreator(), gameRoom);
            ctx.channel().attr(ROOM_OWNER).set(ctx.channel().attr(CHANNEL_OWNER).get());
            createRoom = createRoom.toBuilder().setRoomName(gameRoom.getRoomName()).setCreator(gameRoom.getCreator()).build();
            /* вернуть клиенту данные комнаты */
            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setCreateRoom(createRoom).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
            /* обновить список комнат у всех */
            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void deleteRoom(ChannelHandlerContext ctx) {
        /* если владелец канала владеет комнатой */
        String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();
        if(gameRooms.containsKey(channel_owner)) {
            GameRoom gameRoom = gameRooms.get(channel_owner);
            /* ничего не можем сделать если комната запущенна */
            if(gameRoom.getGameState()) return;
            /* удалить из списка комнат */
            gameRooms.remove(channel_owner);
            /* сообщить подписантам об этом */
            GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setRoomOwner(gameRoom.getCreator()).build();

            GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
            MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
            MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

            roomListUpdateSubscribers.writeAndFlush(wrapper);
        }
    }

    public static void clearRoomData(ChannelHandlerContext ctx) {
        String player = ctx.channel().attr(CHANNEL_OWNER).get();

        if(gameRooms.containsKey(player)) {
            GameRoom gameRoom = gameRooms.get(player);
            System.out.println(gameRoom.getRoomName());
            /* удалить из списка комнат, если игра не запущенна */
            if(!gameRoom.getGameState()) {
                gameRooms.remove(player);
                /* сообщить подписантам об этом */
                GameRoomModels.RoomsListUpdate update = GameRoomModels.RoomsListUpdate.newBuilder().setDeleteRoom(true).setRoomName(gameRoom.getRoomName()).build();

                GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomsListUpdate(update).build();
                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                roomListUpdateSubscribers.writeAndFlush(wrapper);
            }
        }
        /* нет комнат, тогда проверить игрока в списках и удалить если он там есть */
        else {
            for(String owner : gameRooms.keySet()) {
                if (gameRooms.get(owner).players.containsKey(player)) {
                    /* комната не в процессе игры */
                    if(!gameRooms.get(owner).getGameState()) {
                        GameRoom gameRoom = gameRooms.get(owner);
                        GameRoomModels.SubscribeRoom.Builder subscribe = GameRoomModels.SubscribeRoom.newBuilder()
                                .setRoomName(gameRoom.getRoomName());

                        if(gameRoom.team1.containsKey(player)) {
                            subscribe.setTeam("team1");
                        } else if (gameRoom.team2.containsKey(player)) {
                            subscribe.setTeam("team2");
                        } else if (gameRoom.undistributed.containsKey(player)) {
                            subscribe.setTeam("undistributed");
                        }

                        GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe.build()).build();
                        roomUnsubscribe(ctx, room);
                    }
                }
            }
        }
    }

    private void roomSubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if(Objects.equals(gameRooms.get(owner).getRoomName(), room.getSubscribeRoom().getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                /* вернуть сигнал в случае заполненности комнаты */
                if(gameRoom.players.size() > gameRoom.getMaxPlayers()) {
                    GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setStateCode(1).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    ctx.writeAndFlush(wrapper);
                    return;
                }

                String player = ctx.channel().attr(CHANNEL_OWNER).get();
                /* добавить, если игрока с таким ником нет в комнате */
                if(!gameRoom.players.containsKey(player)) {
                    gameRoom.addPlayer(ctx, player);
                } else {
                    GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setStateCode(2).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    ctx.writeAndFlush(wrapper);
                    return;
                }

                /* вернуть текущее состояние игроков в комнате */
                GameRoomModels.RoomDescribe.Builder builder = GameRoomModels.RoomDescribe.newBuilder();

                for (String p : gameRoom.undistributed.keySet()) {
                    GameRoomModels.TeamPlayer undistributed = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addUndistributed(undistributed);
                }

                for (String p : gameRoom.team1.keySet()) {
                    GameRoomModels.TeamPlayer team1 = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addTeam1(team1);
                }

                for (String p : gameRoom.team2.keySet()) {
                    GameRoomModels.TeamPlayer team2 = GameRoomModels.TeamPlayer.newBuilder()
                            .setPlayerName(p).build();
                    builder.addTeam2(team2);
                }

                GameRoomModels.RoomDescribe roomDescribe = builder.setRoomName(gameRoom.getRoomName())
                        .setMapName(gameRoom.getMapName())
                        .setGameTime(gameRoom.getGameTime() + " minutes")
                        .setMaxPlayers(gameRoom.getMaxPlayers() + " players")
                        .setCreator(gameRoom.getCreator())
                        .build();

                MainMenuModels.Chat.Builder chatBuilder = MainMenuModels.Chat.newBuilder();

                /* создать список последних 100 сообщений */
                for (int i = 0; i < gameRoom.syncMessageList.size(); i++) {
                    MainMenuModels.ChatMessage message = MainMenuModels.ChatMessage.newBuilder()
                            .setTime(gameRoom.syncMessageList.get(i).getTime())
                            .setName(gameRoom.syncMessageList.get(i).getName())
                            .setText(gameRoom.syncMessageList.get(i).getText())
                            .build();
                    chatBuilder.addMessages(message);
                }

                roomDescribe = roomDescribe.toBuilder().setChat(chatBuilder.build()).build();

                System.out.println(roomDescribe);

                GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setRoomDescribe(roomDescribe).build();
                GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                ctx.writeAndFlush(wrapper);

                /* обновить списки у всех остальных */
                updateRoom(ctx, gameRoom , player);

                ctx.channel().attr(ROOM_OWNER).set(gameRoom.getCreator());
            }
        }
    }

    private void updateRoom(ChannelHandlerContext ctx, GameRoomModels.RoomUpdate roomUpdate) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), roomUpdate.getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();

                /* проверяем владеет ли этот пользователь запрошенной комнатой */
                if(Objects.equals(channel_owner, gameRoom.getCreator())) {
                    GameRoomModels.RoomDescribe roomDescribe = roomUpdate.getRoomDescribe();

                    Map<String,Channel> list = new HashMap<>();

                    /* удалить из старых списков */
                    for (GameRoomModels.TeamPlayer p : roomDescribe.getTeam1List()) {
                        list.put(p.getPlayerName(), gameRoom.team1.get(p.getPlayerName()));
                        gameRoom.team1.remove(p.getPlayerName());
                    }

                    for (GameRoomModels.TeamPlayer p : roomDescribe.getTeam2List()) {
                        list.put(p.getPlayerName(), gameRoom.team2.get(p.getPlayerName()));
                        gameRoom.team2.remove(p.getPlayerName());
                    }

                    for (GameRoomModels.TeamPlayer p : roomDescribe.getUndistributedList()) {
                        list.put(p.getPlayerName(), gameRoom.undistributed.get(p.getPlayerName()));
                        gameRoom.undistributed.remove(p.getPlayerName());
                    }

                    /* добавить в целевой */
                    if(Objects.equals(roomUpdate.getTargetTeam(), "team1")) {
                        gameRoom.team1.putAll(list);
                    } else if(Objects.equals(roomUpdate.getTargetTeam(), "team2")) {
                        gameRoom.team2.putAll(list);
                    } else if(Objects.equals(roomUpdate.getTargetTeam(), "undistributed")) {
                        gameRoom.undistributed.putAll(list);
                    }

                    /* обновить списки на клиентах */
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setRoomUpdate(roomUpdate).build();

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }

    private void updateRoom(ChannelHandlerContext ctx, GameRoom gameRoom, String player) {
        /* отправить всем информацию о том что добавился новый игрок */
        GameRoomModels.SubscribeRoom subscribe = GameRoomModels.SubscribeRoom.newBuilder().setPlayer(player).build();
        GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

        MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
        MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

        /* себе не отправляем */
        gameRoom.recipients.writeAndFlush(wrapper, ChannelMatchers.isNot(ctx.channel()));
    }

    private static void roomUnsubscribe(ChannelHandlerContext ctx, GameRoomModels.Room room) {
        /* найти комнату */
        for(String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), room.getSubscribeRoom().getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String player = ctx.channel().attr(CHANNEL_OWNER).get();
                String team = "";

                /* игрок не может удалиться из комнаты если она в бою */
                if(!gameRoom.getGameState()) {
                    gameRoom.players.remove(player);

                    if(gameRoom.team1.containsKey(player)) {
                        team = "team1";
                        gameRoom.team1.remove(player);
                    } else if(gameRoom.team2.containsKey(player)) {
                        team = "team2";
                        gameRoom.team2.remove(player);
                    }  else if(gameRoom.undistributed.containsKey(player)) {
                        team = "undistributed";
                        gameRoom.undistributed.remove(player);
                    }

                    GameRoomModels.SubscribeRoom subscribe = room.getSubscribeRoom().toBuilder().setPlayer(player).setTeam(team).build();
                    GameRoomModels.Room out = GameRoomModels.Room.newBuilder().setSubscribeRoom(subscribe).build();

                    System.out.println(out);

                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(out.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }

    private void chatHandler(ChannelHandlerContext ctx, GameRoomModels.RoomUpdate roomUpdate) {
        /* найти комнату */
        for (String owner : gameRooms.keySet()) {
            if (Objects.equals(gameRooms.get(owner).getRoomName(), roomUpdate.getRoomName())) {
                GameRoom gameRoom = gameRooms.get(owner);
                String channel_owner = ctx.channel().attr(CHANNEL_OWNER).get();

                if (gameRoom.players.containsKey(channel_owner)) {
                    Calendar c = Calendar.getInstance();
                    long ms = c.get(Calendar.HOUR_OF_DAY) * 60 * 60 * 1000 + c.get(Calendar.MINUTE) * 60 * 1000 + c.get(Calendar.SECOND) * 1000;

                    MainMenuModels.Chat chat = roomUpdate.getRoomDescribe().getChat();

                    String str = chat.getText();

                    chat = MainMenuModels.Chat.newBuilder().clear()
                            .setName(ctx.channel().attr(CHANNEL_OWNER).get())
                            .setText(str)
                            .setTime(ms)
                            .build();

                    /* поместили в список сообщений */
                    gameRoom.syncMessageList.add(chat);

                    GameRoomModels.RoomUpdate update = GameRoomModels.RoomUpdate.newBuilder().setRoomDescribe(
                            GameRoomModels.RoomDescribe.newBuilder().setChat(chat).build()).build();

                    GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setRoomUpdate(update).build();
                    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
                    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                    gameRoom.recipients.writeAndFlush(wrapper);
                }
            }
        }
    }
}

На клиенте правим условие UGameRoom::Handler:

...
else if (room.has_roomupdate())
{
	if (room.roomupdate().roomdescribe().GetReflection()->HasField(room.roomupdate().roomdescribe(), Descriptors::chat_room))
	{
		UChats * chats = NewObject<UChats>(UChats::StaticClass());
		chats->Handler(room.roomupdate().roomdescribe().chat(), "gameroom");
	}
	else
	{
		UpdateRoom(room.roomupdate());
	}
}

Открываем UChats::Handler и раскомментируем логику для type == «gameroom», принцип работы тот же что и чата в главном меню:

GameRoom

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "GameRoom.h"
#include "Descriptors.h"
#include "Protobufs/GameRoomModels.pb.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "MainMenuWidgets.h"
#include "Runtime/UMG/Public/Components/HorizontalBox.h"
#include "Runtime/UMG/Public/Components/HorizontalBoxSlot.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"
#include "Runtime/UMG/Public/Components/ButtonSlot.h"
#include "Runtime/UMG/Public/Components/ScrollBox.h"
#include "RoomListUnit.h"
#include "GameRoomWidgets.h"
#include "GameRoomUserUnit.h"
#include "CreateRoomWidgets.h"

std::string UGameRoom::roomCreator = "";
std::string UGameRoom::roomName = "";

void UGameRoom::Handler(Room room)
{
	if (room.has_createroom())
	{
		USpikyGameInstance::DifferentMix->wMainMenuWidgets->AddRoom(NewRoom(room.createroom()));

		// если это создатель то открыть комнату, если нет просто обновить список в главном меню
		if (room.createroom().creator() == USpikyGameInstance::userLogin)
		{
			roomCreator = room.createroom().creator();
			roomName = room.createroom().roomname();
			USpikyGameInstance::DifferentMix->ShowGameRoom();
		}
	}
	else if (room.has_roomslistupdate())
	{
		if (!room.startgame())
		{
			// удалить комнату из списка, вернуть нераспределённых игроков в меню
			DeleteRoom(room.roomslistupdate());
		}
		else
		{
			DeleteRoom(room.roomslistupdate().roomowner());
		}
	}
	else if (room.has_subscriberoom())
	{
		Subscribe(room.subscriberoom());
	}
	else if (room.has_roomupdate())
	{
		if (room.roomupdate().roomdescribe().GetReflection()->HasField(room.roomupdate().roomdescribe(), Descriptors::chat_room))
		{
			UChats * chats = NewObject<UChats>(UChats::StaticClass());
			chats->Handler(room.roomupdate().roomdescribe().chat(), "gameroom");
		}
		else
		{
			UpdateRoom(room.roomupdate());
		}
	}
}

void UGameRoom::Subscribe(SubscribeRoom subscribe) 
{
	if (subscribe.statecode() == 1) {
		GLog->Log("The room is full");
	}
	else if (subscribe.statecode() == 2) {
		GLog->Log("The player is already in the room");
	}
	else if (subscribe.has_roomdescribe()) {
		// применить описание комнаты
		roomCreator = subscribe.roomdescribe().creator();
		roomName = subscribe.roomdescribe().roomname(); // и другие параметры если надо

		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();

		for (TeamPlayer e : subscribe.roomdescribe().team1())
		{
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->AddPlayer(e.player_name().c_str(), "team1");
		}
		for (TeamPlayer e : subscribe.roomdescribe().team2())
		{
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->AddPlayer(e.player_name().c_str(), "team2");
		}

		for (TeamPlayer e : subscribe.roomdescribe().undistributed())
		{
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->AddPlayer(e.player_name().c_str(), "undistributed");
		}

		UChats * chats = NewObject<UChats>(UChats::StaticClass());
		chats->Handler(subscribe.roomdescribe().chat(), "gameroom");

		// открыть комнату
		USpikyGameInstance::DifferentMix->wMainMenuWidgets->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);

		USpikyGameInstance::DifferentMix->wMainMenuChatWidgets->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wMainMenuWidgets->bChatOpen = false;

		USpikyGameInstance::DifferentMix->wGameRoomWidgets->SetVisibility(ESlateVisibility::Visible);
		// скрыть кнопки управления списками
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wToFirstTeamButton->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wToSecondTeamButton->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wToUndistributedTeamButton->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wStartButton->SetVisibility(ESlateVisibility::Hidden);
	}
	else if (subscribe.GetReflection()->HasField(subscribe, Descriptors::player_sub) && !subscribe.GetReflection()->HasField(subscribe, Descriptors::player_team))
	{
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->AddPlayer(subscribe.player().c_str(), "undistributed");
	}
	else if (!subscribe.subscribe())
	{
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->RemovePlayer(subscribe.player().c_str(), subscribe.team().c_str());

		if (USpikyGameInstance::userLogin == subscribe.player())
		{
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
			USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();
			//закрыть комнату
			USpikyGameInstance::DifferentMix->ShowMainMenuScreen();
		}
	}
}

void UGameRoom::UpdateRoom(RoomUpdate update)
{
	for (TeamPlayer e : update.roomdescribe().team1())
	{
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->RemoveAddPlayer(e.player_name().c_str(), "team1", update.targetteam().c_str());
	}

	for (TeamPlayer e : update.roomdescribe().team2())
	{
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->RemoveAddPlayer(e.player_name().c_str(), "team2", update.targetteam().c_str());
	}

	for (TeamPlayer e : update.roomdescribe().undistributed())
	{
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->RemoveAddPlayer(e.player_name().c_str(), "undistributed", update.targetteam().c_str());
	}
}

URoomListUnit* UGameRoom::NewRoom(CreateRoom room)
{
	URoomListUnit* button = NewObject<URoomListUnit>(URoomListUnit::StaticClass());
	UHorizontalBox* horBox = NewObject<UHorizontalBox>(UHorizontalBox::StaticClass());

	UTextBlock* wRoomName = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wMapName = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wCreator = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wGameTime = NewObject<UTextBlock>(UTextBlock::StaticClass());
	UTextBlock* wMaxPlayers = NewObject<UTextBlock>(UTextBlock::StaticClass());

	wRoomName->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.roomname().c_str()))));
	wMapName->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.mapname().c_str()))));
	wCreator->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.creator().c_str()))));
	wGameTime->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.gametime().c_str()))));
	wMaxPlayers->SetText(FText::FromString(TCHAR_TO_UTF8(*FString(room.maxplayers().c_str()))));

	UHorizontalBoxSlot* roomNameSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wRoomName));
	UHorizontalBoxSlot* mapNameSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wMapName));
	UHorizontalBoxSlot* creatorSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wCreator));
	UHorizontalBoxSlot* gameTimeSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wGameTime));
	UHorizontalBoxSlot* maxPlayersSlot = Cast<UHorizontalBoxSlot>(horBox->AddChild(wMaxPlayers));

	roomNameSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	roomNameSlot->SetHorizontalAlignment(HAlign_Center);

	mapNameSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	mapNameSlot->SetHorizontalAlignment(HAlign_Center);

	creatorSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	creatorSlot->SetHorizontalAlignment(HAlign_Center);

	gameTimeSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	gameTimeSlot->SetHorizontalAlignment(HAlign_Center);

	maxPlayersSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	maxPlayersSlot->SetHorizontalAlignment(HAlign_Center);

	UButtonSlot* buttonSlot = Cast<UButtonSlot>(button->AddChild(horBox));
	buttonSlot->SetHorizontalAlignment(HAlign_Fill);
	buttonSlot->SetVerticalAlignment(VAlign_Fill);

	return button;
}

void UGameRoom::DeleteRoom(RoomsListUpdate update)
{
	// удалить комнату из списка
	for (size_t i = 0; i < USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildrenCount(); i++)
	{
		URoomListUnit* listUnit = Cast<URoomListUnit>(USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildAt(i));
		UHorizontalBox* horBox = Cast<UHorizontalBox>(listUnit->GetChildAt(0));

		UTextBlock* wCreatorName = Cast<UTextBlock>(horBox->GetChildAt(2));

		if (wCreatorName->GetText().ToString() == update.roomowner().c_str())
		{
			USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->RemoveChildAt(i);
		}
	}

	// вернуть игроков которые были в этой комнате в главное меню
	if (roomCreator == update.roomowner().c_str())
	{
		// сбросить состояние виджета для последующего использования
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();
		UGameRoomUserUnit::select_players.clear();
		roomCreator = "";
		roomName = "";

		USpikyGameInstance::DifferentMix->wMainMenuWidgets->SetVisibility(ESlateVisibility::Visible);
		USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
		USpikyGameInstance::DifferentMix->wGameRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
	}
}

void UGameRoom::DeleteRoom(std::string name)
{
	// удалить комнату из списка
	for (size_t i = 0; i < USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildrenCount(); i++)
	{
		URoomListUnit* listUnit = Cast<URoomListUnit>(USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->GetChildAt(i));
		UHorizontalBox* horBox = Cast<UHorizontalBox>(listUnit->GetChildAt(0));

		UTextBlock* wCreatorName = Cast<UTextBlock>(horBox->GetChildAt(2));

		if (wCreatorName->GetText().ToString() == name.c_str())
		{
			USpikyGameInstance::DifferentMix->wMainMenuWidgets->wRoomsScrollBox->RemoveChildAt(i);
		}
	}

	// если мы находимся в другой комнате
	if (roomCreator == name)
	{
		// вернуть игроков которые были нераспределены в главное меню
		for (size_t j = 0; j < USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->GetChildrenCount(); j++)
		{
			UGameRoomUserUnit * entity = Cast<UGameRoomUserUnit>(USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->GetChildAt(j));
			FString playerName = Cast<UTextBlock>(entity->GetChildAt(0))->GetText().ToString();

			// если игрок находится в этом списке и не является создателем
			if (playerName == USpikyGameInstance::userLogin.c_str() && playerName != roomCreator.c_str())
			{
				// сбросить состояние виджета для последующего использования
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wFirstTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wSecondTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wUndistributedTeamScrollBox->ClearChildren();
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->wChatScrollBox->ClearChildren();
				UGameRoomUserUnit::select_players.clear();
				roomCreator = "";
				roomName = "";

				USpikyGameInstance::DifferentMix->wMainMenuWidgets->SetVisibility(ESlateVisibility::Visible);
				USpikyGameInstance::DifferentMix->wCreateRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
				USpikyGameInstance::DifferentMix->wGameRoomWidgets->SetVisibility(ESlateVisibility::Hidden);
			}
		}
	}
}

void UGameRoom::StartGame()
{
}

Протестируем! Для этого мы можем собрать игру для windows x64, или запустить два экземпляра прямо из редактора. Для этого в настройках редактора, пункт Play:

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 15

Если вы решили собрать игру, то чтобы задать расширение экрана создадите в
/WindowsNoEditor/Spiky_Client/Saved/Config/WindowsNoEditor — GameUserSettings.ini с таким содержанием:

GameUserSettings.ini

[/Script/Engine.GameUserSettings]
bUseVSync=False
ResolutionSizeX=683
ResolutionSizeY=384
LastUserConfirmedResolutionSizeX=1280
LastUserConfirmedResolutionSizeY=1024
WindowPosX=-1
WindowPosY=-1
bUseDesktopResolutionForFullscreen=False
FullscreenMode=2
LastConfirmedFullscreenMode=2
Version=5

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

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

Создадим папку Pawns и добавим в неё актора MechActor:

MechActor

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "GameFramework/Actor.h"
#include "MechActor.generated.h"

UCLASS()
class SPIKY_CLIENT_API AMechActor : public AActor
{
	GENERATED_BODY()
	
public:	

	AMechActor(const FObjectInitializer& ObjectInitializer);

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = MeshComponent)
	UStaticMeshComponent* MeshComponent;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MechActor.h"
#include "Runtime/Engine/Classes/GameFramework/RotatingMovementComponent.h"
#include "Runtime/Engine/Classes/Components/StaticMeshComponent.h"

AMechActor::AMechActor(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	MeshComponent = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("Mesh"));
	MeshComponent->SetMobility(EComponentMobility::Movable);

	RootComponent = MeshComponent;

	URotatingMovementComponent* RotatingMovement = ObjectInitializer.CreateDefaultSubobject<URotatingMovementComponent>(this, TEXT("RotatingMovement"));
	RotatingMovement->RotationRate = FRotator(0, 80, 0); // speed
	RotatingMovement->PivotTranslation = FVector(0, 0, 0); // point

	RotatingMovement->SetUpdatedComponent(GetRootComponent());
}

URotatingMovementComponent* RotatingMovement = ObjectInitializer.CreateDefaultSubobject<URotatingMovementComponent>(this,TEXT("RotatingMovement"));
RotatingMovement->RotationRate = FRotator(0, 80, 0); // speed
RotatingMovement->PivotTranslation = FVector(0, 0, 0); // point
RotatingMovement->SetUpdatedComponent(GetRootComponent());

Состоит наш актор из конструктора в котором мы устанавливаем MeshComponent как RootComponent и затем в редакторе присваиваем модель. Unreal Engine имеет функционал позволяющий задать вращение компоненту, с определённой скоростью и точкой вращения – UrotatingMovementComponent, всё что нам остаётся так это указать эти параметры и сказать что мы хотим вращать.

Далее в редакторе создам BP от этого актора и присвоим модель меха в MeshComponent.
Blueprints/Pawns создадим MechActor_BP. Увеличим немного пол scale x,y = 10, уберем свет добавим DirectionalLight. Разместим CameraActor, найдем у неё в настройках Auto Player Activation и зададим Player 0. Вид из камеры должен быть примерно таким. Теперь можно запустить и проверить что модель вращается.

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 16

Проверка попаданий на проверочном сервере

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

Для обеспечения синхронизации перемещения я рассылаю новые позиции игроков сразу же по их получению. Каждый клиент отправляет на сервер обновления 20 раз в секунду.

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

У нас в игре есть лазер, мгновенное прямолинейное оружие. С артиллерией которой нужно время для подлёта, было бы иначе, прямолинейное вооружение стреляет мгновенно, достигая цели в момент выстрела. Поэтому для компенсации лага нам нужно отмотать время, разместить объекты соответственно состоянию мира в тот момент и выполнить трассировку линий.

Игра начинается после нажатия кнопки start game в тренировочной комнате. После чего серверу отправляет имя комнаты и сигнал начала:

void UGameRoomWidgets::StartButtonClicked()
{
	std::shared_ptr<Room> room(new Room);
	room->set_startgame(true);
	room->set_roomname(UGameRoom::roomName);
	UMessageEncoder::Send(room.get(), true, true);
}

В условие в RoomManager добавим новое условие и пустую функцию:

...
else if(room.hasField(startGame_room)) {
    startGame(ctx, room.getRoomName());
}
...
private void startGame(ChannelHandlerContext ctx, String roomName)
{}

Нам понадобится объект в котором мы будем хранить данные относительно определённого игрока, создадим на сервере в логике класс PlayerState:

PlayerState

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

public class PlayerState {

    private String team;
    private NavigableMap<Long,Position> positionByTime = Collections.synchronizedNavigableMap(new TreeMap<>());
    private long lastUpdateTime;

    PlayerState() {
        startCleaner();
    }

    public class Position {
        Location location;
        Rotation rotation;

        Position(Location location, Rotation rotation) {
            this.location = location;
            this.rotation = rotation;
        }
    }

    /* Всё что старше пяти секунд удалить! */
    private void startCleaner() {
        new Thread(() -> {
            long lifetime = 5000;
            while (true) {
                try {
                    if(positionByTime.size() <= 0) continue;

                    lastUpdateTime = positionByTime.lastEntry().getKey();

                    Thread.sleep(5000);

                    /* если позиция за 5 секунд не изменилась, оставить один элемент и обновить его время */
                    if(positionByTime.lastEntry().getKey() == lastUpdateTime) {
                        long ms = System.currentTimeMillis();
                        lastUpdateTime = ms;

                        Position old_pos = positionByTime.lastEntry().getValue();
                        positionByTime.clear();
                        positionByTime.put(ms, old_pos);
                    } else {
                        positionByTime.entrySet().removeIf(e -> System.currentTimeMillis() - e.getKey() > lifetime);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public class Location {
        private int x, y, z;

        public Location(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        public int getZ() {
            return z;
        }

        public void setZ(int z) {
            this.z = z;
        }
    }

    public class Rotation {
        private int pitch, roll, yaw;

        public Rotation(int pitch, int roll, int yaw) {
            this.pitch = pitch;
            this.roll = roll;
            this.yaw = yaw;
        }

        public int getPitch() {
            return pitch;
        }

        public void setPitch(int pitch) {
            this.pitch = pitch;
        }

        public int getRoll() {
            return roll;
        }

        public void setRoll(int roll) {
            this.roll = roll;
        }

        public int getYaw() {
            return yaw;
        }

        public void setYaw(int yaw) {
            this.yaw = yaw;
        }
    }

    Map.Entry<Long, Position> getLastPosition() {
        return positionByTime.lastEntry();
    }

    void addPosition(long time, Position position) {
        positionByTime.put(time, position);
    }

    public Map.Entry<Long, Position> getClosestMs(long ms) {
        // todo задать чувствительность, например не старше 2 секунд
        return positionByTime.lowerEntry(ms);
    }

    public String getTeam() {
        return team;
    }

    public void setTeam(String team) {
        this.team = team;
    }
}

Задача этого класса сохранять данные о перемещении в течении нескольких секунд, хранить данные о принадлежности к команде. Он содержит три подкласса Position, Location и Rotation. Position хранит в себе два последних:

private String team;
private NavigableMap<Long,Position> positionByTime = Collections.synchronizedNavigableMap(new TreeMap<>());
private long lastUpdateTime;

Я использую NavigableMap для хранения позиций привязанных ко времени, NavigableMap позволяет найти ближайший элемент к заданному времени (lowerEntry()), lastUpdateTime – игрок может просто не двигаться, в таком случае мы не получаем обновлений, на сервере мы удаляем элементы все кроме последнего, если не приходят обновления, храним последний элемент и обновляем ему время, чтобы можно было получить доступ к нему, например если чувствительность, время когда возможно попадание — секунда, то сравнить мы можем только те данные которые не старше секунды. В конструкторе запускаем очиститель старых данных тем же способом что и удаляли старые капчи. В GameRoom добавим playersState, каждая комната хранит состояние своих игроков:

Map<String,PlayerState> playersState = Collections.synchronizedMap(new HashMap<>());

private void startGame(ChannelHandlerContext ctx, String roomName)
	находит комнату
	проверяет соврадает ли тот кто начинает игру с владельцем
	помечаем что комната в состоянии игры
	удаляем комнату из общего доступа
	удалить и отписать из комнаты нераспределённых игроков
	отправить находящимся в комнате игрокам сигнал начала игры, 
	отправить начальные данные – это случайная начальная позиция

private void startGame(ChannelHandlerContext ctx, String roomName)

private void startGame(ChannelHandlerContext ctx, String roomName) {
    /* найти комнату */
    for(String owner : gameRooms.keySet()) {
        if (Objects.equals(gameRooms.get(owner).getRoomName(), roomName)) {
            GameRoom gameRoom = gameRooms.get(owner);
            /* проверить владельца */
            if (Objects.equals(gameRoom.getCreator(), ctx.channel().attr(CHANNEL_OWNER).get())) {
                /* от повторных нажатий */
                if(gameRoom.getGameState()) return;
                /* удалить комнату из общего доступа, пометить что она в процессе игры */
                gameRoom.setGameState(true);

                GameRoomModels.RoomsListUpdate roomsListUpdate =  GameRoomModels.RoomsListUpdate.newBuilder().setRoomOwner(gameRoom.getCreator()).build();

                GameRoomModels.Room room = GameRoomModels.Room.newBuilder().setStartGame(true).setRoomsListUpdate(roomsListUpdate).build();
                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setRoom(ByteString.copyFrom(room.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
                /* удалить комнату из списка, без каких либо последствий */
                roomListUpdateSubscribers.writeAndFlush(wrapper);

                /* удалить и отписать из комнаты нераспределённых игроков */
                for (String p : gameRoom.undistributed.keySet()) {
                    /* но не владельца */
                    if(!Objects.equals(gameRoom.getCreator(), p)) {
                        gameRoom.players.remove(p);
                        gameRoom.recipients.remove(gameRoom.undistributed.get(p));
                    }
                }
                gameRoom.undistributed.clear();

                /* отправить находящимся в комнате игрокам сигнал начала игры, отдельно, включает начальные данные */
                GameModels.GameInitialState initialState = GameModels.GameInitialState.newBuilder().setStartGame(true).build();

                Random rand = new Random();

                for (String name : gameRoom.players.keySet()) {
                    String team = "";
                    if(gameRoom.team1.containsKey(name)) { team = "team1"; }
                    else if(gameRoom.team2.containsKey(name)) { team = "team2"; }

                    PlayerState playerState = new PlayerState();
                    playerState.setTeam(team);

                    GameModels.Player playerProto = GameModels.Player.newBuilder().build();

                    int randomX = rand.nextInt((2000 - (-2000)) + 1) + (-2000);
                    int randomY = rand.nextInt((2000 - (-2000)) + 1) + (-2000);

                    PlayerState.Location l = playerState.new Location(randomX, randomY, 0);
                    PlayerState.Rotation r = playerState.new Rotation(0, 0, 0);

                    playerState.addPosition(System.currentTimeMillis(), playerState.new Position(l, r));

                    gameRoom.playersState.put(name, playerState);

                    GameModels.PlayerPosition.Location loc = GameModels.PlayerPosition.Location.newBuilder()
                            .setX(playerState.getLastPosition().getValue().location.getX())
                            .setY(playerState.getLastPosition().getValue().location.getY())
                            .setZ(playerState.getLastPosition().getValue().location.getZ())
                            .build();

                    GameModels.PlayerPosition.Rotation rot = GameModels.PlayerPosition.Rotation.newBuilder()
                            .setPitch(playerState.getLastPosition().getValue().rotation.getPitch())
                            .setYaw(playerState.getLastPosition().getValue().rotation.getYaw())
                            .setRoll(playerState.getLastPosition().getValue().rotation.getRoll())
                            .build();

                    GameModels.PlayerPosition playerPosition = GameModels.PlayerPosition.newBuilder().setLoc(loc).setRot(rot).build();
                    /* имя игрока, имя команды, позиция (локэйшен и ротейшен) */
                    playerProto = playerProto.toBuilder().setPlayerName(name).setTeam(playerState.getTeam()).setPlayerPosition(playerPosition).build();

                    initialState = initialState.toBuilder().addPlayer(playerProto).build();
                }

                GameModels.GameData gameData = GameModels.GameData.newBuilder().setGameInitialState(initialState).build();

                MessageModels.CryptogramWrapper cw2 = MessageModels.CryptogramWrapper.newBuilder().setGameModels(ByteString.copyFrom(gameData.toByteArray())).build();
                MessageModels.Wrapper wrapper2 = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw2).build();

				for (Channel c : gameRoom.players.values()) c.writeAndFlush(wrapper2);
            }
        }
    }
}

Теперь клиент, добавим Character игроку ATPSCharacter : public ACharacter. Настроим модель, передвижение и возможность стрелять лазером:

ATPSCharacter

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "GameFramework/Character.h"
#include "TPSCharacter.generated.h"

UCLASS()
class SPIKY_CLIENT_API ATPSCharacter : public ACharacter
{
	GENERATED_BODY()

public:

	ATPSCharacter();

protected:

	virtual void BeginPlay() override;

public:	

	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UFUNCTION()
	void MoveForward(float Val);

	UFUNCTION()
	void MoveRight(float Val);
	
	void Fire();
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "TPSCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Runtime/Engine/Classes/Components/InputComponent.h"
#include "Runtime/Engine/Classes/GameFramework/Controller.h"
#include "Runtime/Engine/Classes/GameFramework/PlayerController.h"
#include "Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h"
#include "Protobufs/GameModels.pb.h"
#include "MessageEncoder.h"

ATPSCharacter::ATPSCharacter()
{
 	PrimaryActorTick.bCanEverTick = true;
}

void ATPSCharacter::BeginPlay()
{
	Super::BeginPlay();
}

void ATPSCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void ATPSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis("MoveForward", this, &ATPSCharacter::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &ATPSCharacter::MoveRight);
	PlayerInputComponent->BindAxis("Turn", this, &ATPSCharacter::AddControllerYawInput);
	PlayerInputComponent->BindAxis("LookUp", this, &ATPSCharacter::AddControllerPitchInput);
	PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ATPSCharacter::Fire);
}

void ATPSCharacter::MoveForward(float Value)
{
	if ((Controller != NULL) && (Value != 0.0f))
	{
		// find out which way is forward
		FRotator Rotation = Controller->GetControlRotation();
		// Limit pitch when walking or falling
		if (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling())
		{
			Rotation.Pitch = 0.0f;
		}
		// add movement in that direction
		const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X);
		AddMovementInput(Direction, Value);
	}
}

void ATPSCharacter::MoveRight(float Value)
{
	if ((Controller != NULL) && (Value != 0.0f))
	{
		// find out which way is right
		const FRotator Rotation = Controller->GetControlRotation();
		const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::Y);
		// add movement in that direction
		AddMovementInput(Direction, Value);
	}
}

void ATPSCharacter::Fire()
{
	GLog->Log("FIRE");
	
	APlayerController* controller = Cast<APlayerController>(GetController());

	FVector start = controller->PlayerCameraManager->GetCameraLocation();
	FVector end = start + controller->PlayerCameraManager->GetActorForwardVector() * 512;

	FCollisionQueryParams TraceParams(FName(TEXT("Trace")));
	TraceParams.bTraceComplex = true;

	FHitResult HitData(ForceInit);

	//Trace!
	GetWorld()->LineTraceSingleByChannel(HitData, start, end, ECC_Pawn, TraceParams);

	FVector startZ = FVector(start.X, start.Y, start.Z - 10);
	FVector endZ = FVector(end.X, end.Y, end.Z + 10);
	UKismetSystemLibrary::DrawDebugLine(this, startZ, endZ, FColor(255, 0, 0), 3.f, 1.f);

	std::shared_ptr<Shot> shot(new Shot);
	std::shared_ptr<Shot::Start> shotStart(new Shot::Start);
	std::shared_ptr<Shot::End> shotEnd(new Shot::End);

	shotStart->set_x(start.X);
	shotStart->set_y(start.Y);
	shotStart->set_z(start.Z);

	shotEnd->set_x(end.X);
	shotEnd->set_y(end.Y);
	shotEnd->set_z(end.Z);

	shot->set_allocated_start(shotStart.get());
	shot->set_allocated_end(shotEnd.get());

	if (IsValid(HitData.GetActor()))
		if (HitData.GetActor()->Tags.Num() > 0)
			shot->set_requestto(TCHAR_TO_UTF8(*HitData.GetActor()->Tags.Top().ToString()));

	shot->set_timestamp(USpikyGameInstance::DifferentMix->GetMS());

	std::shared_ptr<GameData> gameData(new GameData);
	gameData->set_allocated_shot(shot.get());

	// начало и конец выстрела, время попадания в мс, имя игрока в которого папали
	UMessageEncoder::Send(gameData.get(), true, true);

	gameData->release_shot();
	shot->release_end();
	shot->release_start();
}

ATPSCharacter::Fire() — стреляет из головы, от положения камеры, лазером служит функция UKismetSystemLibrary::DrawDebugLine данные о попадании собираем с помощью LineTraceSingleByChannel. Для проверки было ли попадание серверу отправляем имя цели в которую попали, время попадания, начало и конец луча выстрела.

Чтобы получить время в мс воспользуемся средствами Unreal. Обновим DifferentMix:

...
int64 GetMS();
...
int64 UDifferentMix::GetMS()
{
	FDateTime now = FDateTime::UtcNow();
	return now.ToUnixTimestamp() * 1000 + now.GetMillisecond();
}

Создадим в логике класс для этих событий GameProcess и обработаем начальное состояние присланное сервером:

GameProcess

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include <vector>
#include "GameProcess.generated.h"

class GameData;
class PlayerPosition;

UCLASS()
class SPIKY_CLIENT_API UGameProcess : public UObject
{
	GENERATED_BODY()

public:

	void Handler(GameData gData);

	static GameData gameData;

	void UpdatePositions(PlayerPosition playerPosition);

	static std::vector<int64> pings;

	void ComputePing(GameData gData);

	void ComputeShot(GameData gData);
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "GameProcess.h"
#include "Protobufs/GameModels.pb.h"

GameData UGameProcess::gameData;
std::vector<int64> UGameProcess::pings;

void UGameProcess::Handler(GameData gData)
{
}

void UGameProcess::ComputeShot(GameData gData)
{
}

void UGameProcess::UpdatePositions(PlayerPosition playerPosition)
{
}

void UGameProcess::ComputePing(GameData gData)
{
}

Для нового уровня, понадобится новый GameMode, создадим MapGameMode:

void AMapGameMode::BeginPlay()
	Здесь мы будем размещать всех игроков комнаты исходя из пришедшего начального состояния.

AActor* AMapGameMode::ChoosePlayerStart_Implementation(AController* Player)
	Размещаем игрока

void AMapGameMode::SendLocation()
	Пока не пользуемся, отправляет позицию игрока на сервер с определённой частотой
void AMapGameMode::SendPing()
	вычисляем Ping

void AMapGameMode::ComputeFrameRate()
	вычисляем FPS

Настроим в редакторе пешку игрока, назначим кнопки управления. Создаём в Blueprints/Pawns
TPSCharacter_BP. В настройках TPSCharacter устанавливаем GenericMale. Установим кнопки управления в ProjectSettings->Input:

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 17

У камеры в TPSCharacter_BP нужно выставить Use Pawn Control Rotation, чтобы мы могли управлять пешкой мышью:

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 18

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 19

В конструкторе GameMode получаем ссылку на БП:

static ConstructorHelpers::FClassFinder<ATPSCharacter> PlayerPawnObject(TEXT("Blueprint'/Game/Blueprints/Pawns/TPSCharacter_BP.TPSCharacter_BP_C'"));

if (PlayerPawnObject.Class != NULL)
{
	pawnObject = PlayerPawnObject.Class->GetDefaultObject<ATPSCharacter>();
}

MapGameMode

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "GameFramework/GameModeBase.h"
#include "MapGameMode.generated.h"

class ATPSCharacter;

UCLASS()
class SPIKY_CLIENT_API AMapGameMode : public AGameModeBase
{
	GENERATED_BODY()

	AMapGameMode(const FObjectInitializer& ObjectInitializer);

	virtual void BeginPlay() override;

	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

	virtual void Tick(float DeltaTime) override;

	ATPSCharacter* pawnObject;

	virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;

	FTimerHandle UpdateLocationTimerHandle;
	void SendLocation();

	FVector old_position, position;
	FRotator old_rotation, rotation;

	FTimerHandle PingTimerHandle;
	void SendPing();

	FTimerHandle FPSTimerHandle;
	void ComputeFrameRate();
	int32 fps = 0;
};


// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "MapGameMode.h"
#include "SpikyGameInstance.h"
#include "Runtime/Engine/Classes/Engine/World.h"
#include "TPSCharacter.h"
#include "Protobufs/GameModels.pb.h"
#include "GameProcess.h"
#include "DifferentMix.h"
#include "MessageEncoder.h"

AMapGameMode::AMapGameMode(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	PrimaryActorTick.bCanEverTick = true;

}

void AMapGameMode::BeginPlay()
{
	Super::BeginPlay();

	GLog->Log("AMapGameMode::BeginPlay()");

	USpikyGameInstance* gameInstance = Cast<USpikyGameInstance>(GetWorld()->GetGameInstance());
	gameInstance->DifferentMixInit(GetWorld());

	// задать начальные значения всех остальных игроков
	for (::Player p : UGameProcess::gameData.gameinitialstate().player())
	{
		if (p.player_name() != USpikyGameInstance::userLogin)
		{
			FVector pos = FVector(p.playerposition().loc().x(), p.playerposition().loc().y(), p.playerposition().loc().z());
			auto character = GetWorld()->SpawnActor<ATPSCharacter>(pawnObject->GetClass(), FTransform(pos));

			character->Tags.Add(p.player_name().c_str());
		}	
	}

}

void AMapGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
}

void AMapGameMode::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

AActor* AMapGameMode::ChoosePlayerStart_Implementation(AController* Player)
{
	FVector pos;

	for (::Player p : UGameProcess::gameData.gameinitialstate().player())
	{
		if (p.player_name() == USpikyGameInstance::userLogin)
		{
			pos = FVector(p.playerposition().loc().x(), p.playerposition().loc().y(), p.playerposition().loc().z());
		}
	}

	ATPSCharacter* playerCharacter = GetWorld()->SpawnActor<ATPSCharacter>(pawnObject->GetClass(), FTransform(pos));
	GetWorld()->GetFirstPlayerController()->Possess(playerCharacter);

	playerCharacter->Tags.Add(USpikyGameInstance::userLogin.c_str());

	return playerCharacter;
}

void AMapGameMode::SendLocation()
{
	ATPSCharacter* character = Cast<ATPSCharacter>(GetWorld()->GetFirstPlayerController()->GetCharacter());

	position = character->GetActorLocation();
	rotation = character->GetActorRotation();

	// ничего не отправляем если нет изменений
	if (position.Equals(old_position) && rotation.Equals(old_rotation)) return;

	old_position = position;
	old_rotation = rotation;

	std::shared_ptr<PlayerPosition> playerPosition(new PlayerPosition);
	std::shared_ptr<PlayerPosition::Location> playerLocation(new PlayerPosition::Location);
	std::shared_ptr<PlayerPosition::Rotation> playerRotation(new PlayerPosition::Rotation);

	playerLocation->set_x(position.X);
	playerLocation->set_y(position.Y);
	playerLocation->set_z(position.Z);

	playerRotation->set_pitch(rotation.Pitch);
	playerRotation->set_roll(rotation.Roll);
	playerRotation->set_yaw(rotation.Yaw);

	playerPosition->set_allocated_loc(playerLocation.get());
	playerPosition->set_allocated_rot(playerRotation.get());

	playerPosition->set_timestamp(USpikyGameInstance::DifferentMix->GetMS());

	std::shared_ptr<GameData> gameData(new GameData);
	gameData->set_allocated_playerposition(playerPosition.get());

	UMessageEncoder::Send(gameData.get(), true, true);

	gameData->release_playerposition();
	playerPosition->release_rot();
	playerPosition->release_loc();
}

void AMapGameMode::SendPing()
{
}

void AMapGameMode::ComputeFrameRate()
{
}

Так же нам нужно создать новую карту Map1 (добавьте из исходников с Github).

В UGameProcess::Handler:

void UGameProcess::Handler(GameData gData)
{
	if (gData.has_gameinitialstate())
	{
		gameData = gData;
		UGameplayStatics::OpenLevel(USpikyGameInstance::DifferentMix->GetWorld(), "Map1", false, "game= Spiky_Client.MapGameMode");
	}
}

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

Думаю нам понадобится прицел, для этого добавим HUD который будет размещать текстуру прицела по центру. Добавьте класс SpikyHUD в корень Public/Private:

SpikyHUD

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "GameFramework/HUD.h"
#include "Runtime/Engine/Classes/Engine/Texture2D.h"
#include "SpikyHUD.generated.h"

UCLASS()
class SPIKY_CLIENT_API ASpikyHUD : public AHUD
{
	GENERATED_BODY()
	
	ASpikyHUD();
	
private:
	/** Crosshair asset pointer */
	UTexture2D* CrosshairTex;

	virtual void DrawHUD() override;
	
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SpikyHUD.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
#include "Runtime/Engine/Classes/Engine/Canvas.h"

ASpikyHUD::ASpikyHUD()
{
	// Set the crosshair texture
    static ConstructorHelpers::FObjectFinder<UTexture2D> CrosshairTexObj(TEXT("Texture2D'/Game/ProjectResources/Images/crosshair.crosshair'"));
    CrosshairTex = CrosshairTexObj.Object;
}

void ASpikyHUD::DrawHUD()
{
	Super::DrawHUD();
    // Draw very simple crosshair
    // find center of the Canvas
    const FVector2D Center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);
    // offset by half the texture's dimensions so that the center of the texture aligns with the center of the Canvas
    const FVector2D CrosshairDrawPosition((Center.X - (CrosshairTex->GetSurfaceWidth() * 0.5)), (Center.Y - (CrosshairTex->GetSurfaceHeight() * 0.5f)));
    // draw the crosshair
    FCanvasTileItem TileItem(CrosshairDrawPosition, CrosshairTex->Resource, FLinearColor::White);
    TileItem.BlendMode = SE_BLEND_Translucent;
    Canvas->DrawItem(TileItem);
}

Обновим SpikyGameMode в конструкторе:

HUDClass = ASpikyHUD::StaticClass();

Соберем и протестируем, чтобы добавить к сборке еще одну карту Map1 нужно указать к ней путь:

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 20

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

Добавим синхронизацию перемещения и выстрелов. В SpikyGameMode создадим таймер отправляющий информацию о расположении игрока 20 раз в секунду:

изменения в SpikyGameMode

//.h

FTimerHandle UpdateLocationTimerHandle;

void SendLocation();

FVector old_position, position;
FRotator old_rotation, rotation;

//.cpp

void AMapGameMode::BeginPlay()
{
	// отправляем таймером 30 раз в секунду позицию игрока /10 .1 /20 .05 / 30 .03
	GetWorld()->GetTimerManager().SetTimer(UpdateLocationTimerHandle, this, &AMapGameMode::SendLocation, .05f, true);
}

void AMapGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
	GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
}

// отправляем на сервер данные только если были какие-то изменения
void AMapGameMode::SendLocation() {...}

В UGameProcess::Handler добавляем два новых условия и функцию:

...
#include "Runtime/CoreUObject/Public/UObject/UObjectIterator.h"
...
	else if (gData.has_playerposition())
	{
		UpdatePositions(gData.playerposition());
	}
	else if (gData.has_shot())
	{
		ComputeShot(gData);
	}

void UGameProcess::ComputeShot(GameData gData)
	получаем параметры выстрела, рисуем выстрел

void UGameProcess::UpdatePositions(PlayerPosition playerPosition)
	получает позицию какого-то игрока, ищет этого игрока на карте, обновляет позицию

На сервере в GameState:

private void updatePosition(ChannelHandlerContext ctx, GameModels.PlayerPosition playerPosition) 
	сохраняем позицию игрока в истории
	отправляем подписчикам

В конструкторе проверяем тип сообщения, для выстрела, две ветви если попали, пришел тег в кого попали, отправляем на проверочный сервер, пока пусто, если тега нет, то отправляем всем параметры выстрела для симуляции:

GameState

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp.logics;

import com.google.protobuf.ByteString;
import com.spiky.server.protomodels.GameModels;
import com.spiky.server.protomodels.MessageModels;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelMatchers;

import java.util.Map;

import static com.spiky.server.ServerMain.CHANNEL_OWNER;
import static com.spiky.server.ServerMain.ROOM_OWNER;
import static com.spiky.server.ServerMain.gameRooms;
import static com.spiky.server.utils.Descriptors.requestTo_shot_gd;

public class GameState {
    public GameState(ChannelHandlerContext ctx, GameModels.GameData gameData) {
        if(gameData.hasPlayerPosition())
        {
            updatePosition(ctx, gameData.getPlayerPosition());
        }
        else if(gameData.hasShot())
        {
            /* найти цель */
            GameRoom gameRoom = gameRooms.get(ctx.channel().attr(ROOM_OWNER).get());
            /* проверить попадание если в кого то попали, если нет разослать всем событие выстрела */
            if(gameData.getShot().hasField(requestTo_shot_gd)) {
                gameData = gameData.toBuilder().setShot(gameData.getShot().toBuilder().clearTimeStamp().build()).build();

                MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setGameModels(ByteString.copyFrom(gameData.toByteArray())).build();
                MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

                gameRoom.recipients.writeAndFlush(wrapper, ChannelMatchers.isNot(ctx.channel()));
            } else {

            }
        }
    }

    private void updatePosition(ChannelHandlerContext ctx, GameModels.PlayerPosition playerPosition) {
        GameRoom gameRoom = gameRooms.get(ctx.channel().attr(ROOM_OWNER).get());

        /* сохраняем позицию игрока в истории */
        PlayerState state = gameRoom.playersState.get(ctx.channel().attr(CHANNEL_OWNER).get());

        PlayerState.Location l = state.new Location(playerPosition.getLoc().getX(), playerPosition.getLoc().getY(), playerPosition.getLoc().getZ());
        PlayerState.Rotation r = state.new Rotation(playerPosition.getRot().getPitch(), playerPosition.getRot().getYaw(), playerPosition.getRot().getRoll());
        state.addPosition(playerPosition.getTimeStamp(), state.new Position(l, r));

        //System.out.println(playerPosition.getTimeStamp());

        /* отправляем подписчикам */
        playerPosition = playerPosition.toBuilder().setPlayerName(ctx.channel().attr(CHANNEL_OWNER).get()).build();

        GameModels.GameData gameData = GameModels.GameData.newBuilder().setPlayerPosition(playerPosition).build();

        MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setGameModels(ByteString.copyFrom(gameData.toByteArray())).build();
        MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

        gameRoom.recipients.writeAndFlush(wrapper, ChannelMatchers.isNot(ctx.channel()));
    }
}

Можно тестировать. Перед тем как начать делать проверочный сервер, добавим виджет отображающий ping и fps. Добавим виджет состоящий из двух текстовых полей:

PingFpsWidgets

// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "PingFpsWidgets.generated.h"

class UTextBlock;

UCLASS()
class SPIKY_CLIENT_API UPingFpsWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:
	UTextBlock* wFps = nullptr;
	UTextBlock* wPing = nullptr;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "PingFpsWidgets.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"

void UPingFpsWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wFps = Cast<UTextBlock>(GetWidgetFromName(TEXT("fps")));
	wPing = Cast<UTextBlock>(GetWidgetFromName(TEXT("ping")));
}

Присвоим нового родителя и добавим виджет через DifferentMix:

Добавление PingFpsWidgets в DifferentMix

//.h
class UPingFpsWidgets;
UPingFpsWidgets* tmpPingFpsRef;
UPingFpsWidgets* wPingFpsWidgets;
UCanvasPanelSlot* pingFpsSlot;

.cpp

#include "PingFpsWidgets.h"
...
	static ConstructorHelpers::FClassFinder<UPingFpsWidgets> pingFpsWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/PingFPS_W.PingFPS_W_C'"));

	if (pingFpsWidgets.Class != NULL)
	{
		tmpPingFpsRef = pingFpsWidgets.Class->GetDefaultObject<UPingFpsWidgets>();
}
...
	wPingFpsWidgets = CreateWidget<UPingFpsWidgets>(GetWorld(), tmpPingFpsRef->GetClass());
	pingFpsSlot = Cast<UCanvasPanelSlot>(wWidgetContainer->wCanvas->AddChild(wPingFpsWidgets));
	pingFpsSlot->SetZOrder(10);
	pingFpsSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	pingFpsSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wPingFpsWidgets->SetVisibility(ESlateVisibility::Hidden);

Включим отображение в AMapGameMode::BeginPlay():

#include "PingFpsWidgets.h"
...
gameInstance->DifferentMix->wPingFpsWidgets->SetVisibility(ESlateVisibility::Visible);

	// отправляем пакет с временем отправки пять раз в секунду
	GetWorld()->GetTimerManager().SetTimer(PingTimerHandle, this, &AMapGameMode::SendPing, .25f, true);

	// вычисляем frame rate (fps)
	GetWorld()->GetTimerManager().SetTimer(FPSTimerHandle, this, &AMapGameMode::ComputeFrameRate, 1.f, true);

Вычисляем пинг и частоту кадров, напомню что пинг это время путешествия пакета от клиента на сервер и обратно:

...
#include "Runtime/UMG/Public/Components/TextBlock.h"
...
void AMapGameMode::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	fps++;
}

void AMapGameMode::SendPing()
{
	std::shared_ptr<Ping> ping(new Ping);

	ping->set_time(USpikyGameInstance::DifferentMix->GetMS());

	std::shared_ptr<GameData> gameData(new GameData);
	gameData->set_allocated_ping(ping.get());

	UMessageEncoder::Send(gameData.get(), true, true);

	gameData->release_ping();
}

void AMapGameMode::ComputeFrameRate()
{
	USpikyGameInstance::DifferentMix->wPingFpsWidgets->wFps->SetText(FText::FromString(FString::FromInt(fps) + " FPS"));
	fps = 0;
}

На сервере мы должны отправить эхо. В UGameProcess::Handler добавляем новое условие:

...
else if (gData.has_ping())
{
	ComputePing(gData);
}
...
void UGameProcess::ComputePing(GameData gData)
	накапливаем 5 значений, вычисляем среднее, очищаем

void UGameProcess::ComputePing(GameData gData)

void UGameProcess::ComputePing(GameData gData)
{
	int64 ping = USpikyGameInstance::DifferentMix->GetMS() - gData.ping().time();

	if (pings.size() < 5)
	{			
		pings.push_back(ping);
	}
	else
	{
		int64 avr_ping = 0;

		for (int64 p : pings)
		{
			avr_ping += p;
		}

		avr_ping /= 5;
		pings.clear();

		FString str = "Ping: " + FString::FromInt(avr_ping) + " ms";
		USpikyGameInstance::DifferentMix->wPingFpsWidgets->wPing->SetText(FText::FromString(str));
	}
}

в GameState отправляем эхо:

else if(gameData.hasPing()) {
    MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setGameModels(ByteString.copyFrom(gameData.toByteArray())).build();
    MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();
    ctx.writeAndFlush(wrapper);
}

Запустим и проверим что всё работает!

Последняя часть проекта касается проверочного сервера. Работает он так: передаём серверу намерение выстрела и его параметры, проверочный сервер размещает все игровые объекты в момент выстрела и проверяет было ли попадание, а если было — применяет урон и обновляет состояние на клиентах. Обновлять мы не будем, ограничимся сообщениями на сервере.

Общение с сервером будет происходить на java socket’ах.

Создадим пустой Unreal проект VerificationServer. В него мы перетаскиваем общение с сетью из клиента, модель обработки сообщений MessageDecoder-MessageEncoder, protobuf модели и экземпляры тестируемых объектов. Добавим в пакет tcp на сервере новый класс VerificationServerConnection, который будет слушать и отправлять сообщения по сокету:

VerificationServerConnection

/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.tcp;

import com.spiky.server.protomodels.GameModels;
import com.spiky.server.tcp.logics.GameState;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class VerificationServerConnection {
    private OutputStream socketWriter;

    public VerificationServerConnection(int verificationServerPort) {
        try {
            ServerSocket serverSocket = new ServerSocket(verificationServerPort);
            Socket activeSocket = serverSocket.accept();

            InputStream socketReader = activeSocket.getInputStream();
            socketWriter = new DataOutputStream(activeSocket.getOutputStream());

            // получатель
            new Thread(() -> {
                try {
                    while (true) {
                        byte[] messageByte = new byte[1024];
                        socketReader.read(messageByte);

                        ByteArrayInputStream input = new ByteArrayInputStream(messageByte);
                        GameModels.GameData gameData = GameModels.GameData.parseDelimitedFrom(input);

                        new GameState(gameData);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void SendToVerificationServer(GameModels.GameData gameData) {
        try {
            ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
            gameData.writeDelimitedTo(output);
            byte sendData[] = output.toByteArray();
            socketWriter.write(sendData);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Добавим новый конструктор к GameState:

public GameState(GameModels.GameData gameData) {
    if(gameData.hasShot()) {
        System.out.println(gameData);

        GameRoom gameRoom = gameRooms.get(gameData.getShot().getRoomOwner());

        gameData = gameData.toBuilder().setShot(gameData.getShot().toBuilder()
                .clearTimeStamp()
                .clearPlayerPosition()
                .clearRoomOwner().build()
        ).build();

        MessageModels.CryptogramWrapper cw = MessageModels.CryptogramWrapper.newBuilder().setGameModels(ByteString.copyFrom(gameData.toByteArray())).build();
        MessageModels.Wrapper wrapper = MessageModels.Wrapper.newBuilder().setCryptogramWrapper(cw).build();

        gameRoom.recipients.writeAndFlush(wrapper);
    }
}

Сюда приходят данные после проверки на проверочном сервере. Добавим порт проверочного сервера в файл настроек:

verificationServerPort = 7682

И прочитаем его в ServerMain:

private static final int verificationServerPort = Integer.valueOf(configurationBundle.getString("verificationServerPort"));

Запустим соединение с сервером:

/* соединение с сервером проверяющим попадания */
public static VerificationServerConnection verificationServerConnection;

public static void main(String[] args) {
    new Thread(ServerMain::run_tcp).start();
    //new Thread(ServerMain::run_udp).start();

    captchaCleaner();
    verificationServerConnection = new VerificationServerConnection(verificationServerPort);
}

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

/* найти ближайшую позицию игрока ко времени */
Map.Entry<Long, PlayerState.Position> pos = state.getClosestMs(gameData.getShot().getTimeStamp());
									
/* отправить на проверку */
ServerMain.verificationServerConnection.SendToVerificationServer(gd);

На сервере прямо в декодере мы проверяем:

UMessageDecoder::SendProtoToDecoder(GameData* gameData)
	размещаем на карте
	имитируем выстрел
	если мы во что-то попали 
		возвращаем сигнал что мы попали и имя кости, имя игрока уже содержится 
			в сообщении, которое так же содержит от кого идет запрос

Далее мы можем обновить состояние игрока у всех. Я не стал этого делать, а только вернул результат проверки. Вот так выглядит процесс проверки попадания на проверочном сервере:

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 21

На этом все. Надеюсь, материал был полезен.

Интересные статьи по теме и литература

Хорошая статья о компенсации лагов:
Компенсация лагов для оружия в MechWarrior Online

Обширный материал по сетевой архитектуре DOOM III:
The DOOM III Network Architecture

Статья компании Valve:
Source Multiplayer Networking

MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square - 22

Автор: Вадим

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js