-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51


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

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

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





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


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



Описать алгоритм можно в пяти шагах:
  1. Инициализация угловых точек. Присваивание им значений высот выбором случайных чисел.
  2. Нахождение срединной точки, присваивание ей значения, на основе среднего от угловых, плюс случайное число.
  3. Нахождение срединной точек для ромбов, отмеченных черными точками (на этом шаге по одной точке каждого ромба выходят за пределы массива).
  4. Для каждого квадрата (на этом шаге их 4), повторяем шаг № 2.
  5. Повторяем шаг № 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, на него можно добавить созданный виджет и растягивать, наблюдая за масштабированием.



Мы не будем создавать виджеты шаг за шагом. Вам нужно взять их и ресурсы к ним из исходников и поместить в 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 
#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(GetWidgetFromName(TEXT("AddressBox")));
	wPortBox = Cast(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(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(GetWidgetFromName(TEXT("Gear1")));
	wGear2 = Cast(GetWidgetFromName(TEXT("Gear2")));
}

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

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




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

USetServerWidgets::NativeConstruct()
     wAddressBox = Cast(GetWidgetFromName(TEXT("AddressBox")));
     wPortBox = Cast(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(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::StaticClass());
	DifferentMix->AddToRoot();
	DifferentMix->Init();
}

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

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

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

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

wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
registrationSlot = CastwCanvas->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 RegistrationWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Reg_W.Reg_W_C'"));
    
    if (RegistrationWidgets.Class != NULL)
    {
    	tmpRegistrationRef = RegistrationWidgets.Class->GetDefaultObject();
    }
    

  7. Затем в UDifferentMix::Init() создаём виджет и размещаем его в слоте:
    wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
    registrationSlot = CastwCanvas->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 WidgetContainer(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/WidgetContainer.WidgetContainer_C'"));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
	registrationSlot = Cast(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(GetWorld(), tmpLoginScreenRef->GetClass());
	loginScreenSlot = Cast(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(GetWorld(), tmpServerSettingsButtonRef->GetClass());
	serverSettingsButtonsSlot = Cast(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(GetWorld(), tmpServerSettingsRef->GetClass());
	serverSettingsSlot = Cast(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(GetWorld(), tmpTermsPrivacyRef->GetClass());
	TermsPrivacySlot = Cast(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(GetWorld(), tmpWaitingScreenRef->GetClass());
	waitingScreenSlot = Cast(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(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(new Utility);
	utility->set_alive(true);

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


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

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




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

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(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 bn.h>
#include 
#include 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 
#include 
#include bio.h>
#include evp.h>
#include buffer.h>
#include err.h>
// Sha256
#include 
#include 
// DH
#include crypto.h>
#include dh.h>

#include 
#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] = '\0';

	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(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(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(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(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(GetWidgetFromName(TEXT("LoginTextBox")));
wLoginTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnLoginTextChanged);

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

static ConstructorHelpers::FObjectFinder 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(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 accept_ref(TEXT("Texture2D'/Game/ProjectResources/Images/accept.accept'"));

	accept_tex = accept_ref.Object;

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

	denied_tex = denied_ref.Object;

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

	empty_tex = empty_ref.Object;
}

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

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

	wCaptchaImage = Cast(GetWidgetFromName(TEXT("CaptchaImage")));

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

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

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

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

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

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

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

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

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

void URegWidgets::ShowTermPrivacyClicked()
{
	USpikyGameInstance::DifferentMix->wTerms
                                        
Метки:  

 

Добавить комментарий:
Текст комментария: смайлики

Проверка орфографии: (найти ошибки)

Прикрепить картинку:

 Переводить URL в ссылку
 Подписаться на комментарии
 Подписать картинку