UE4 中实现简单的本地登录注册功能 (UMG C++ 入门向)
前言
之前在 minigame 中开发游戏的登录注册功能时,发现网上关于 UE4 的介绍资料寥寥,对于登录注册功能的介绍更少,且大多要求通过 HTTP 等网络协议与服务器或者数据库进行通信交互,提高了开发难度,且不符合我们只是想做一个休闲单机小游戏的预期。
在翻阅了相关资料后,找到了一种通过本地文件交互实现登录注册功能的方法,考虑到相关资料介绍并不多,将实现方式总结如下,仅是一篇抛砖引玉的文章,希望有兴趣的人多多交流学习。
一、前期准备
Unreal Engine 4.25.4
Visual Studio 2019
1. 新建空白项目,命名为 LoginRegister
2. 新建空白关卡和对应的用户控件
如何新建关卡可以参考以下视频
新关卡一片黑?初学者必知的UE4新建关卡时的一些设置【虚幻引擎】。
3. 修改关卡 Login 的关卡蓝图,将 UMG 添加到视口
二、修改控件
首先修改控件 LoginUI,添加两个文本框和两个按钮。
需要注意的地方
- 按钮和可编辑文本框需要暴露为变量,而且为了方便后续编码需要起合适的变量名,比如
UsernameTextBox
、PasswordTextBox
、LoginButton
和RegisterButton
。 - 密码文本框需要在外观中设置成
为密码
- 可编辑文本框的提示文本可以在外观中进行设置,比如上图中的
请输入用户名
和请输入密码
。 - 按钮里的文字需要使用文本控件,并用外层按钮进行包裹作为子控件。
这样就完成了基本的 UI 设计,修改完后别忘了保存和编译。
执行当前 Login 关卡,显示以下界面。
三、UMG C++ 添加控件逻辑
新建 C++ 类,注意需要打开 显示所有类
,继承自 UserWidget
父类,并将生成的类命名为 LoginWidget
,如下图所示。
生成源文件后,编辑源码 LoginWidget.h
和 LoginWidget.cpp
。
// LoginWidget.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h" // 使用按钮控件 api
#include "Components/EditableTextBox.h" // 使用可编辑文本框 api
#include "LoginWidget.generated.h"
/**
*
*/
UCLASS()
class LOGINREGISTER_API ULoginWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 构造函数
ULoginWidget(const FObjectInitializer& ObjectInitializer);
// 点击登录按钮的响应事件
UFUNCTION()
void PlayerLogin();
// 点击注册按钮的响应事件
UFUNCTION()
void PlayerRegister();
protected:
virtual void NativeConstruct();
// 工具函数: 检查数据文件是否存在
bool CheckTextFileExists(FString FileName);
// 工具函数: 将数据存入文本文件
bool SaveToTextFile(FString FileName);
// 工具函数: 从文本文件读取数据
bool LoadFromTextFile(FString FileName);
private:
UButton* LoginButton;
UButton* RegisterButton;
UEditableTextBox* UsernameTextBox;
UEditableTextBox* PasswordTextBox;
FString Username;
FString Password;
};
// LoginWidget.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "LoginWidget.h"
#include "HAL/PlatformFileManager.h"
ULoginWidget::ULoginWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
LoginButton = nullptr;
RegisterButton = nullptr;
UsernameTextBox = nullptr;
PasswordTextBox = nullptr;
}
void ULoginWidget::NativeConstruct()
{
Super::NativeConstruct();
UE_LOG(LogTemp, Warning, TEXT("ULoginWidget::NativeConstruct()"));
if (UButton* SBtn = Cast<UButton>(GetWidgetFromName("LoginButton")))
{
LoginButton = SBtn;
FScriptDelegate sgbDelegate;
sgbDelegate.BindUFunction(this, "PlayerLogin");
LoginButton->OnClicked.Add(sgbDelegate);
}
if (UButton* SBtn = Cast<UButton>(GetWidgetFromName("RegisterButton")))
{
RegisterButton = SBtn;
FScriptDelegate sgbDelegate;
sgbDelegate.BindUFunction(this, "PlayerRegister");
RegisterButton->OnClicked.Add(sgbDelegate);
}
if (UEditableTextBox* SText = Cast<UEditableTextBox>(GetWidgetFromName("UsernameTextBox")))
{
UsernameTextBox = SText;
}
if (UEditableTextBox* SText = Cast<UEditableTextBox>(GetWidgetFromName("PasswordTextBox")))
{
PasswordTextBox = SText;
}
}
void ULoginWidget::PlayerLogin()
{
if (UsernameTextBox == nullptr || PasswordTextBox == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Widget can't get"));
return;
}
// 获取当前编辑框内的文本内容
FText UsernameText = UsernameTextBox->GetText();
FText PasswordText = PasswordTextBox->GetText();
FString UsernameStr = UsernameText.ToString();
FString PasswordStr = PasswordText.ToString();
if (UsernameStr.Len() <= 0 || PasswordStr.Len() <= 0)
{
UE_LOG(LogTemp, Warning, TEXT("Username or Password must not be empty"));
return;
}
// 数据文件路径
FString FileName = TEXT("Player_") + UsernameStr + TEXT(".data");
// 找不到数据文件,用户未注册
if (!this->CheckTextFileExists(FileName))
{
UE_LOG(LogTemp, Warning, TEXT("Player-[%s] Not Registered"), *UsernameStr);
return;
}
// 读取数据文件
if (!this->LoadFromTextFile(FileName))
{
UE_LOG(LogTemp, Warning, TEXT("Player-[%s] Login Failed. DataFile Wrong Occured"), *UsernameStr);
return;
}
// 检查密码正确性
if (this->Password != PasswordStr)
{
UE_LOG(LogTemp, Warning, TEXT("Player-[%s] Login Failed. Password Not Correct"), *UsernameStr);
return;
}
// Login Successfully. Do Something !
GEngine->AddOnScreenDebugMessage(0, 5.0f, FColor::Red, TEXT("Login Successfully"), false, FVector2D(2.0, 2.0));
}
void ULoginWidget::PlayerRegister()
{
if (UsernameTextBox == nullptr || PasswordTextBox == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Widget can't get"));
return;
}
// 获取当前编辑框内的文本内容
FText UsernameText = UsernameTextBox->GetText();
FText PasswordText = PasswordTextBox->GetText();
FString UsernameStr = UsernameText.ToString();
FString PasswordStr = PasswordText.ToString();
if (UsernameStr.Len() <= 0 || PasswordStr.Len() <= 0)
{
UE_LOG(LogTemp, Warning, TEXT("Username or Password must not be empty"));
return;
}
// 数据文件路径
FString FileName = TEXT("Player_") + UsernameStr + TEXT(".data");
// 存在数据文件,用户已注册
if (this->CheckTextFileExists(FileName))
{
UE_LOG(LogTemp, Warning, TEXT("Player-[%s] Already Registered"), *UsernameStr);
return;
}
// 准备注册信息
this->Username = UsernameStr;
this->Password = PasswordStr;
// 存储数据文件
if (!this->SaveToTextFile(FileName))
{
UE_LOG(LogTemp, Warning, TEXT("Player-[%s] Register Failed. DataFile Wrong Occured"), *UsernameStr);
return;
}
// Register Successfully. Do Something !
GEngine->AddOnScreenDebugMessage(0, 5.0f, FColor::Red, TEXT("Register Successfully"), false, FVector2D(2.0, 2.0));
}
bool ULoginWidget::CheckTextFileExists(FString FileName)
{
// 获取文件操作接口
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
return PlatformFile.FileExists(*FileName);
}
bool ULoginWidget::SaveToTextFile(FString FileName)
{
TArray<FString> SaveArray; // 构造字符串数组 保存需要存储的数据
SaveArray.Add(this->Username);
SaveArray.Add(this->Password);
if (!FFileHelper::SaveStringArrayToFile(SaveArray, *FileName, FFileHelper::EEncodingOptions::ForceUTF8))
{
UE_LOG(LogTemp, Warning, TEXT("Save Text File-[%s] Failed"), *FileName);
return false;
}
return true;
}
bool ULoginWidget::LoadFromTextFile(FString FileName)
{
TArray<FString> SaveArray;
if (!FFileHelper::LoadFileToStringArray(SaveArray, *FileName))
{
UE_LOG(LogTemp, Warning, TEXT("Load Text File-[%s] Failed"), *FileName);
return false;
}
if (SaveArray.Num() < 2)
{
UE_LOG(LogTemp, Warning, TEXT("Text File-[%s] Broken"), *FileName);
return false;
}
this->Username = SaveArray[0];
this->Password = SaveArray[1];
return true;
}
编写好源文件后,需要将 LoginUI 空间中的类设置改为继承 LoginWidget
。
重新运行 Login 关卡,用户名输入 test,密码输入 test,先点击注册按钮,屏幕上会打印出红色日志,如下图所示。
然后再点击登录按钮。
存档数据在 $UE_INSTALL_DIR/Engine/Binaries/Win64/Player_test.data
,其中 $UE_INSTALL_DIR
为 ue4 的安装路径,比如在我的电脑上为 C:\Program Files\Epic Games\UE_4.25
。
使用记事本或其他软件打开存档文件,可以发现数据是分行进行存储,第一行是用户名,第二行是密码,密码是明文存储,实际应用中可以再通过单向加密算法等转为密文存储。
以上,成功通过本地文件存储的方式实现了简单的登录注册功能,读者可以在此基础上更进一步扩展开发,比如加入登录成功的关卡跳转功能、对用户或密码的合法性检查等,此处不再赘述。
参考文献
- UE4/CPP C++绑定UMG中的按钮事件_I_itaiit的博客-CSDN博客
- UE4 UMG中C++成员变量绑定蓝图Widget - 知乎
- UE4 开发之实现按钮事件响应 - 腾讯云开发者社区-腾讯云
- [UE4]UMG widget构造初始化函数中获取其内部组件_玄冬Wong的博客-CSDN博客
- 虚幻4(Unreal Engine4)中的代理委托Delegate的使用方法 - 知乎
- UE4-文件操作 - 知乎
- 【UE4 C++】读写Text文件 FFileHelper - 砥才人 - 博客园
- 【UE4】UE4文件系统_Goulandis的博客-CSDN博客_ue4game是什么文件夹
- 浅谈UE4序列化系列(1) 结合用例浅谈 UE4序列化 - 知乎