- 前言
- Multiplayer Plugin
- Game Project
- 准备工作
- 测试插件 & 使用
ThirdPerson - 导入资源
- 游戏实现一
- 游戏框架
- 游戏实现二
- bug: 所有指针类型,需要初始化为nullptr
前言
这门课程简直无敌,内容非常全,事无巨细,老师讲的也非常认真仔细,让我受益贼多
局域网连接
蓝图方式
-
创建大厅地图
File->New Level,然后Save Level,命名为Lobby -
创建联机房间
通过Open Level打开Lobby关卡,listen表明该关卡配置为ListenServer,可以接受多个玩家的连接

- 加入联机房间
Open ip地址就会加入这个ip计算机上的关卡

- 编译保存,并打包游戏
创建一个Build文件夹,然后选择该文件夹打包

- 局域网联机测试
将打包的程序,分别运行在两个电脑上,一名玩家按1创建房间,另一名玩家按2加入房间,即可完成联机
C++方式
- 首先定义蓝图可调用函数,在任意类中(如
MPTestingCharacter.h)

- 实现创建联系房间,
OpenLobby
传入的参数是Lobby.map相对Content的路径,然后前面加上/Game/

- 实现加入联机房间,
CallOpenLevel和CallClientTravel

- 在蓝图中测试这些函数

在线子系统 (UE Online Sussystem)
多人游戏需要知道其他玩家的ip,通过Server或者Service得到,UE提供了一个抽象层Online Subsystem,只需要使用这个子系统并对服务进行配置即可


创建工程并配置Steam子系统
-
添加
Online Subsystem Steam插件 -
在工程的
Build.cs中添加OnlineSubsystemSteam和OnlineSubsystem依赖
OnlineSubsystem依赖是完整的子系统模块;OnlineSubsystemSteam是特定平台的子系统模块
- 使用
Online Subsystem Steam还需要配置一些东西,可以在官方文档查看;打开项目目录下Config/DefaultEngine.ini添加配置信息

- 重新编译,删除
Saved,Intermediate,Binaries,重新生成代码
访问在线子系统
- 定义
IOnlineSessionPtr并获取
// MenuSystemCharacter.h
public:
// class IOnlineSessionPtr OnlineSessionInterface;
TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe> OnlineSessionInterface;
// MenuSystemCharacter.cpp
AMenuSystemCharacter::AMenuSystemCharacter()
{
...
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
if(OnlineSubsystem)
{
OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Blue,
FString::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName->ToString())
);
}
}
}
- 测试是否访问成功
在编辑器下,无论是什么网络模式Standalone, listenserver, client,都无法连接在线子系统
只能先打包,然后运行打包程序才可以!
创建会话
- 定义函数和委托
MenuSystemCharacter.h
protected:
UFUNCTION(BlueprintCallable)
void CreateGameSession();
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
private:
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
- 初始化和绑定函数到委托
MenuSystemCharacter.cpp
AMenuSystemCharacter::AMenuSystemCharacter() :
CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
{}
- 实现创建会话函数
void AMenuSystemCharacter::CreateGameSession()
{
// 测试函数,蓝图中按键调用
if(!OnlineSystemInterface.IsValid())
{
return;
}
// NAME_GameSession: 存在的全局命名
auto ExistingSession = OnlineSessionInterface->GetNameSession(NAME_GameSession);
if(ExistSession != nullptr)
{
OnlineSystemInterface->DestorySession(NAME_GameSession);
}
// 委托添加到事件中
OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
// 创建Session设置
TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings);
SessionSettings->bIsLANMatch = false; //是否是局域网连接
SessionSettings->NumPublicConnections = 4; // 会话最多存在多少玩家
SessionSettings->bAllowJoinInProgress = true; // 是否允许中途加入
SessionSettings->bAlloJoinViaPresence = true; // 根据所在地区加入
SessionSettings->bShouldAdvertise = true; // 是否发布(其他人可以查找该会话)
SessionSettings->bUsedPresence = true; // 是否使用所在地区
SessionSettings->bUseLobbiesIfAvailable = true; // 某些版本查找会话报错,就需要加上是否使用大厅
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);
}
- 会话完成回调,测试会话是否创建成功
void AMenuSystemCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
if(bWasSuccessful)
{
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Blue,
FString::Printf(TEXT("Created session: %s"), *SessionName.ToString())
);
}
}
else
{
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Blue,
FString::Printf(TEXT("Failed to create session!"))
);
}
}
}
- 接着在蓝图中调用
CreateGameSession测试
无法创建会话,修改配置文件bInitServerOnClient=true
加入会话
查找会话
- 定义函数和委托
MenuSystemCharacter.h
protected:
UNFUNCTION(BlueprintCallable)
void JoinGameSession();
void OnFindSessionComplete(bool bWasSuccessful);
private:
FOnFindSessionCompleteDelegate FindSessionCompleteDelegate;
TSharedPtr<FOnlineSessionSearch> SessionSearch;
- 委托绑定函数
AMenuSystemCharacter::AMenuSystemCharacter()
FindSessionCompleteDelegate(FOnFindSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionComplete))
{}
- 实现加入游戏会话
void AMenuSystemCharacter::JoinGameSession()
{
// 寻找游戏会话
if(!OnlineSessionInterface.IsValid())
{
return;
}
// 绑定委托
OnlineSessionInterface->AddOnFindSessionCompleteDelegate_Handle(FindSessionCompleteDelegate);
SessionSearch = MakeShareable(new FOnlineSessionSearch);
SessionSearch->MaxSearchResults = 10000; // 最大搜索数量
SessionSearch->bIsLanQuery = false; // 不适用局域网
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparionOp::Equals); // 是否使用presence,以及比较方式
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());
}
void AMenuSystemCharacter::OnFindSessionComplete(bool bWasSuccessful)
{
for(auto Result : SessionSearch->SearchResults)
{
FString Id = Result.GetSessionIdStr();
FString User = Result.Session.OwningUserName;
if(GEngine)
{
GEngine.AddOnScreenDebugMessage(
-1,
15.f,
FColor::Cyan,
FString::Printf(TEXT("Id: %s, User: %s"), *Id, *User)
);
}
}
}
加入会话二

大厅关卡
- 创建大厅关卡
File->New Level->Default, 调整后,File->Save Current Level,命名为Lobby
- 创建会话,就进入大厅关卡
MenuSystemCharacter.cpp
void AMenuSystemCharacter::OnCreateSessionComplelte(FName SessionName, bool bWasSuccessful)
{
if(bWasSuccessful)
{
...
UWorld* World = GetWorld();
if(World)
{
// 复制路径,保留content之后的,前面加上/Game/
World->ServerTravel(FString("/Game/ThirdPersonCPP/Maps/Lobby?listen"));
}
}
}
指定匹配类型
可能有多种游戏模式,比如团队竞技、爆破模式,所以需要根据类型来匹配对话
- 在创建会话时指定类型
MenuSystemCharacter.cpp
void AMenuSystemCharacter::CreateGameSession()
{
...
// 创建Session设置
TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings);
...
// 指定匹配键值对,和访问类型
SessionSettings.Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);
}
- 在查找会话时,检查是否匹配指定类型
void AMenuSystemCharacter::OnFindSessionComplete(bool bWasSuccessful)
{
for(auto Result : SessionSearch->SearchResults)
{
FString Id = Result.GetSessionIdStr();
FString User = Result.Session.OwningUserName;
FString MatchType;
Result.Session.SessionSettings.Get(FName("MatchType"), MatchType);
...
if(MatchType == FString("FreeForAll"))
{
if(GEngine)
{
GEngine.AddOnScreenDebugMessage(
-1,
15.f,
FColor::Cyan,
FString::Printf(TEXT("Joining Match Type: %s"), *MatchType);
);
}
}
}
}
加入会话 并 获得地址
- 创建
加入会话完成委托回调
MenuSystemCharacter.h
protected:
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
private:
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
- 绑定委托函数 并 加入会话
MenuSystemCharacter.cpp
AMenuSystemCharacter::AMenuSystemCharacter():
JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete))
{}
void AMenuSystemCharacter::OnFindSessionComplete(bool bWasSuccessful)
{
if(!OnlineSessionInterface.IsValid)
{
return;
}
...
for(auto Result : SessionSearch->SearchResults)
{
...
if(MatchType == FString("FreeForAll"))
{
...
OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result);
}
}
}
加入会话完成回调中获取地址 并 进入大厅
void AMenuSystemCharacter::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if(!OnlineSessionInterface.IsValid)
{
return;
}
FString Address;
if(OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address))
{
if(GEngine)
{
GEngine.AddOnScreenDebugMessage(
-1,
15.f,
FColor::Yellow,
FString::Printf(TEXT("Connect String: %s"), *Address);
);
}
APlayerContoller* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
if(PlayerController)
{
PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
}
}
}
Multiplayer Plugin
多人会话插件
-
创建插件
Edit->Plugin->右下角Create Plugin,创建一个空插件MultiplayerSessions -
添加对在线子系统的插件依赖
MultiplayerSessions.uplugin
"Modules": [
...
],
"Plugins": [
{
"Name": "OnlineSubsystem",
"Enabled": true
},
{
"Name": "OnlineSubsystemSteam",
"Enabled": true
}
]
- 添加模块依赖
MultiplayerSessions.Build.cs
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"OnlineSubsystem",
"OnlineSubsystemSteam",
}
);
多人会话子系统 : Game Instance Subsystem

创建和定义基础内容
- 创建
MultiplayerSessionsSubsytem继承Game Instance Subsystem,并属于MultiplayerSessions模块

- 定义基础内容
MultiplayerSessionsSubsytem.h
pbulic:
UMultiplayerSessionsSubsystem();
private:
IOnlineSessionPtr SessionInterface;
MultiplayerSessionsSubsytem.cpp
UMultiplayerSessionsSubsystem::UMultiplayerSessionsSubsystem()
{
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if(Subsystem)
{
SessionInterface = Subsystem->GetSessionInterface();
}
}
初始化 委托和回调
MultiplayerSessionsSubsystem.h
public:
//
// 提供处理会话功能的函数
//
void CreateSession(int32 NumPublicConnections, FString MatchType);
void FindSession(int32 MaxSearchResults);
void JoinSession(const FOnlineSessionSearchResult& SessionResult);
void DestorySession();
void StartSession();
protected:
//
// 添加到内部委托的回调函数
//
void OnCreateSessionComplete(FName SessionName, bool bWWasSuccessful);
void OnFindSessionComplete(bool bWWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnDestorySessionComplete(FName SessionName, bool bWWasSuccessful);
void OnStartSessionComplete(FName SessionName, bool bWWasSuccessful);
private:
//
// 添加到OnlineSessionInterface回调的委托列表 及 句柄
//
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
FDelegateHandle CreateSessionCompleteDelegateHandle;
FOnFindSessionCompleteDelegate FindSessionCompleteDelegate;
FDelegateHandle FindSessionCompleteDelegateHandle;
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
FDelegateHandle JoinSessionCompleteDelegateHandle;
FOnDestorySessionCompleteDelegate DestorySessionCompleteDelegate;
FDelegateHandle DestorySessionCompleteDelegateHandle;
FOnStartSessionCompleteDelegate StartSessionCompleteDelegate;
FDelegateHandle StartSessionCompleteDelegateHandle;
UMultiplayerSessionsSubsystem.cpp
UMultiplayerSessionsSubsystem::UMultiplayerSessionsSubsystem():
CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)),
FindSessionCompleteDelegate(FOnFindSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionComplete)),
JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)),
DestorySessionCompleteDelegate(FOnDestorySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnDestorySessionComplete)),
StartSessionCompleteDelegate(FOnStartSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnStartSessionComplete))
{}
void CreateSession(int32 NumPublicConnections, FString MatchType);
void FindSession(int32 MaxSearchResults);
void JoinSession(const FOnlineSessionSearchResult& SessionResult);
void DestorySession();
void StartSession();
创建菜单控件(可视化配置和调试)
准备工作
Build.cs中添加UMG相关模块
MultiplayerSessions.Build.cs
PublicDependencyModuleNames.AddRange(
new string[]
{
...
"UMG",
"Slate",
"SlateCore"
}
);
创建Menu:UserWidget类
-
创建
UserWidget子类Menu,属于MultiplayerSessions模块 -
编写
Menu代码
Menu.h
public:
UFUNCTION(BlueprintCallable)
void MenuSetup(int32 NumberOfPublicConnections = 4, FString TypeOfMatch = FString(TEXT("FreeForAll")));
private:
int32 NumPublicConnections{4};
FString MatchType{TEXT("FreeForAll")};
Menu.cpp
void UMenu::UMenu(int32 NumberOfPublicConnections = 4, FString TypeOfMatch = FString(TEXT("FreeForAll")))
{
NumPublicConnections = NumberOfPublicConnections;
MatchType = TypeOfMatch;
AddToViewport(); // 添加到视口
SetVisibility(ESlateVisibility::Visible); // 可见
bIsFocusable = true; // 聚焦到当前控件
UWorld* World = GetWorld();
if(World)
{
APlayerController PlayerController = World->GetFirstPlayerController();
if(PlayerController)
{
// 需求是只输入应用到UI,不应用到Pawn人物
FInputModeUIOnly InputModeData;
InputModeData.SetWidgetToFoucus(TakeWidget()); // 聚焦到任一个SWidget
InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); // 鼠标锁定模式
PlayerController->SetInputMode(InputModeData); //设置输入模式
PlayerController->SetShowMouseCursor(true); // 显示鼠标光标
}
}
}
创建WBP_Menu控件蓝图,并完善Menu类
- 在
MultiplayerSessions插件下创建WBP_Menu

-
设置父类为
Menu,并添加两个功能按钮Hold和Join -
关卡蓝图中创建蓝图,并调用
MenuSetup函数

添加按钮回调
Menu.h
protected:
// 自动初始化控件时调用,MenuSetup初始化菜单,然后Initialize初始化菜单内控件
virtual bool Initialize() override;
// 在切换关卡时调用
virtual void OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) override;
private:
UPROPERTY(meta = (BindWidget))
class UButton* HostButton; // 命名和蓝图中保持一致
UPROPERTY(meta = (BindWidget))
UButton* JoinButton; // 命名和蓝图中保持一致
UFUNCTION()
void HostButtonClicked();
UFUNCTION()
void JoinButtonClicked();
// 清除Setup的输入设置
UFUNCTION(BlueprintCallable)
void MenuTearDown();
Menu.cpp
//
// 初始化菜单内控件
//
bool Menu::Initialize()
{
if(!Super::Initialize())
{
return false;
}
if(HostButton)
{
HostButton->OnClicked.AddDynamic(this, &ThisClass::HostButtonClicked);
}
if(JoinButton)
{
JoinButton->OnClicked.AddDynamic(this, &ThisClass::JoinButtonClicked);
}
return true;
}
void Menu::HostButtonClicked()
{
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Yellow,
FString(TEXT("Host Button Clicked"))
);
}
}
void Menu::JoinButtonClicked()
{
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Yellow,
FString(TEXT("Join Button Clicked"))
);
}
}
void UMenu::OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld)
{
// 切换关卡,清除输入模式
MenuTearDown();
Super::OnLevelRemovedFromWorld(InLevel, InWorld);
}
//
// 清除Setup中的输入设置
//
void UMenu::MenuTearDown()
{
RemoveFromParent(); // 从视口移除
UWorld* World = GetWorld();
if(World)
{
APlayerController PlayerController = World->GetFirstLocalPlayerController();
if(PlayerConntroller)
{
FInputModeGameOnly InputModeData; // 仅处理游戏数据, 不接受ui
PlayerController->SetInputMode(InputModeData);
PlayerController->SetShowMouseCursor(false); // 不显示鼠标指针
}
}
}
Menu类中访问子系统
Menu.h
private:
...
class UMultiplayerSessionsSubsystem* MultiplayerSessionsSubsystem;
Menu.cpp
void Menu::MenuSetup()
{
...
UGameInstance GameInstance = GetGameInstance();
if(GameInstance)
{
MultiplayerSessionsSubsystem = GameIntance->GetSubsystem<UMultiplayerSessionsSubsystem>();
}
}
void Menu::HostButtonClicked()
{
...
if(MultiplayerSessionsSubsystem)
{
// 参数分别是 连接数量(几人房间)、匹配类型(游戏模式)
MultiplayerSessionsSubsystem->CreateSession(NumPublicConnections, MatchType);
UWorld* World = GetWorld();
if(World)
{
World->ServerTravel("/Game/ThirdPersonCPP/Maps/Lobby?listen");
}
}
}
实现HostButtonClicked
CreateSession
MultiplayerSessionSubsystem.h
private:
TSharedPtr<FOnlineSessionSettings> LastSessionSettings;
MultiplayerSessionsSubsystem.cpp
void MultiplayerSessionsSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType)
{
if(!SessionInterface.IsValid())
{
return;
}
auto ExistingSession = SessionInterface->GetNameSession(NAME_GameSession);
if(ExistingSession != nullptr)
{
SessionInterface->DestorySession(NAME_GameSession);
}
// 存储委托句柄
CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
// Session设置
LastSessionSettings = MakeShareable(new FOnlineSessionSettings());
LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; // 如果连接到在线服务(如steam)就返回false,否则就是true 局域网匹配
LastSessionSettings->NumPublicConnections = NumPublicConnections; // 最大连接数
LastSessionSettings->bAllowJoinInProgress = true; // 中途加入
LastSessionSettings->bAllowJoinViaPresence = true;
LastSessionSettings->bShouldAdvertise = true; // 是否公布,允许查找
LastSessionSettings->bUsePresence = true;
LastSessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); // 匹配类型
auto ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if(SessionInterface->CreateSession(LocalPlayer.GetPreferedUniqueNetId(), NAME_GameSession, *LastSessionSettings.))
{
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
}
}
功能测试

按键退出游戏

模块之间的交互关系
Menu菜单 <-> MultiplayerSessions子系统 <-> OnlineSubsystem->OnlineSessionInterface 之间的交互关系

Menu菜单 <-> MultiplayerSessions子系统交互实现
在MultiplayerSessions子系统中定义委托,然后Menu菜单中绑定回调函数
MultiplayerSessions子系统中定义委托
MultiplayeSessionsSubsystem.h
// 定义通知Menu的委托,Menu中绑定回调函数
//
// 注意:动态多播的输入,类型后有逗号,普通多播则没有; 回调函数必须有UFUNCTION()
//
// DYNAMIC: 可以序列化、在蓝图中存储和加载;MULTICAST: 可以绑定多个类的函数;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnCreateSessionComplete, bool, bWasSuccessful);
// 因为FOnlineSessionSearchResult不是新类,与蓝图不兼容,所以不能加入动态委托中
DECLARE_MULTICAST_DELEGATE_TwoParams(FMultiplayerOnFindSessionComplete, const TArray<FOnlineSessionSearchResult>& SessionResult, bool bWasSuccessful);
// EOnJoinSessionCompleteResult与蓝图不兼容
DECLARE_MULTICAST_DELEGATE_OneParam(FMultiplayerOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnDestroySessionComplete, bool, bWasSuccessful);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnStartSessionComplete, bool, bWasSuccessful);
public:
FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete;
FMultiplayerOnFindSessionComplete MultiplayerOnFindSessionComplete;
FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete;
FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete;
FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete;
MultiplayerSessionsSubsystem.cpp
void UMultiplayerSessionsSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType)
{
...
if(SessionInterface->CreateSession(LocalPlayer.GetPreferedUniqueNetId(), NAME_GameSession, *LastSessionSettings.))
{
...
// 通知Menu菜单
MultiplayerOnCreateSessionComplete.Broadcast(false);
}
}
void UMultiplayerSessionsSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
// 清除绑定委托,并通知Menu
if(SessionInterface)
{
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
}
MultiplayerOnCreateSessionComplete.Broadcast(bWasSuccessful);
}
Menu菜单中绑定回调函数
Menu.h
protected:
//
// MultiplayerSessionsSubsystem委托的回调函数
//
UFUNCTION()
void OnCreateSession(bool bWasSuccessful);
UFUNCTION()
void OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResult, bool bWasSuccessful);
UFUNCTION()
void OnJoinSession(EOnJoinSessionCompleteResult::Type Result);
UFUNCTION()
void OnDestroySession(bool bWasSuccessful);
UFUNCTION()
void OnStartSession(bool bWasSuccessful);
Menu.cpp
void Menu::MenuSetup()
{
...
if(MultiplayerSessionsSubsystem)
{
MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession);
MultiplayerSessionsSubsystem->MultiplayerOnFindSessionComplete.AddUObject(this, &ThisClass::OnFindSession);
MultiplayerSessionsSubsystem->MultiplayerOnJoinSessionComplete.AddUObject(this, &ThisClass::OnJoinSession);
MultiplayerSessionsSubsystem->MultiplayerOnDestroySessionComplete.AddDynamic(this, &ThisClass::OnDestroySession);
MultiplayerSessionsSubsystem->MultiplayerOnStartSessionComplete.AddDynamic(this, &ThisClass::OnStartSession);
}
}
void Menu::OnCreateSession(bool bWasSuccessful)
{
if(bWasSuccessful)
{
...
UWorld* World = GetWorld();
if(World)
{
World->ServerTravel("/Game/ThirdPersonCPP/Maps/Lobby?listen");
}
}
else
{
if(GEngine)
{
GEngine.AddOnScreenDebugMessage(
-1,
15.f,
FColor::Red,
FString(TEXT("Failed to create session!"))
);
}
}
}
void Menu::HostButtonClicked()
{
...
/*
UWorld* World = GetWorld();
if(World)
{
World->ServerTravel("/Game/ThirdPersonCPP/Maps/Lobby?listen");
}
*/
}
实现JoinButtonClicked
- 实现点击按钮,执行
FindSessions
Menu.cpp
void UMenu::JoinButtonClicked()
{
if(MultiplayerSessionSubsystem)
{
MultiplayerSessionSubsystem->FindSessions(10000);
}
}
- 子系统中实现
FindSessions并通知Menu
MultiplayerSessionsSubsystem.h
private:
...
TSharedPtr<FOnlineSessionSearch> LastSessionSearch;
MultiplayerSessionsSubsystem.cpp
void UMultiplayerSessionsSubsystem::FindSessions(int32 MaxSearchResults)
{
if(!SessionInterface.IsValid())
{
return;
}
FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);
LastSessionSearch = MakeShareable(new FOnlineSessionSearch());
LastSessionSearch->MaxSearchResults = MaxSearchResults;
LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false;
LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparion::Equals);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if(!SessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToShareRef()))
{
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);
MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
}
}
void UMultiplayerSessionsSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
{
if(SessionInterface)
{
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);
}
if(LastSessionSearch->SearchResults.Num() <= 0)
{
MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
return;
}
MultiplayerOnFindSessionsComplete.Broadcast(LastSessionSearch->SearchResults, bWasSuccessful);
}
Menu收到通知,调用JoinSession
Menu.cpp
void UMenu::OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful>)
{
for(auto Result : SessionResults)
{
FString SettingsValue;
Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue);
if(SettingsValue == MatchType)
{
MultiplayerSessionsSubsystem->JoinSession(Result);
return;
}
}
}
- 子系统实现
JoinSession,并在OnJoinSessionComplete回调时通知Menu
MultiplayerSessionsSubsystem.cpp
void UMultiplayerSessionsSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult)
{
if(!SessionInterface.IsValid())
{
MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
return;
}
JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if(!SessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionResult))
{
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
}
}
void MultiplayerSessionsSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if(SessionInterface)
{
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
}
MultiplayerOnJoinSessionComplete.Broadcast(Result);
}
- 消息回调到
Menu处理,并ClientTravel
Menu.cpp
void UMenu::OnJoinSession(EOnJoinSessionCompleteResult::Type Result)
{
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if(Subsystem)
{
IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface();
if(SessionInterface)
{
FString Address;
SessionInterface->GetResolvedConnectString(NAME_GameSession, Address);
APlayerController* PlayerController = GetGameIntance()->GetFirstLocalPlayerFromController();
if(PlayerController)
{
PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
}
}
}
}
跟踪游戏状态 (GameMode & GameState)
跟踪玩家数量、进入和退出游戏
- 创建
LobbyGameMode继承GameModeBase,属于MenuSystem模块即项目模块,非插件
LobbyGameMode.h
public:
// 玩家进入游戏,并为其创建Player Controller时调用
virtual void PostLogin(APlayerController* NewPlayer) override;
// 玩家退出前调用
virtual void Logout(AController* Exiting) override;
LobbyGameMode.cpp
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
if(GameState)
{
int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num();
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(
1, // 键变,则清除已显示消息
60.f,
FColor::Yellow,
FString::Printf(TEXT("Players in game: %d"), NumberOfPlayers)
);
APlayerState* PlayerState = NewPlayer->GetPlayerState<APlayerState>();
if(PlayerState)
{
FString PlayerName = PlayerState->GetPlayerName();
GEngine->AddOnScreenDebugMessage(
2,
60.f,
FColor::Cyan,
FString::Printf(TEXT("%s has joined the game!"), *PlayerName)
);
}
}
}
}
void ALobbyGameMode::Logout(AController* Exiting)
{
Super::Logout(Exiting);
if(GEngine)
{
APlayerState* PlayerState = Exiting->GetPlayerState<APlayerState>();
if(PlayerState)
{
int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num();
GEngine->AddOnScreenDebugMessage(
1, // 键变,则清除已显示消息
60.f,
FColor::Yellow,
FString::Printf(TEXT("Players in game: %d"), NumberOfPlayers - 1) // 退出前调用,需要减一
);
FString PlayerName = PlayerState->GetPlayerName();
GEngine->AddOnScreenDebugMessage(
2,
60.f,
FColor::Cyan,
FString::Printf(TEXT("%s has exited the game!"), *PlayerName)
);
}
}
}
补充修改(LastSessionSettings->BuildUniqueId = 1; DefaultEngine.ini)
MultiplayerSessionsSubsystem.cpp
...
LastSessionSettings->BuildUniqueId = 1; // 保证每个会话有独一无二的ID可以区分

-
创建
BP_LobbyGameMode蓝图类,指定DefaultPawnClass->ThirdPersonCharacter,在网络同步时会把这个类信息复制给其他人,从而同步位置、动画等信息 -
打开
Lobby关卡,在World Settings->GameMode Override->BP_LobbyGameMode
指定大厅路径
把大厅路径以参数形式传入
Menu.h
public:
MenuSetup(int32 NumberOfPublicConnections = 4, FString TypeOfMatch = FString(TEXT("FreeForAll")), FString LobbyPath = FString(TEXT("/Game/ThirdPersonCPP/Maps/Lobby")))
private:
...
FString PathToLobby{TEXT("")};
Menu.cpp
void UMenu:MenuSetup(int32 NumberOfPublicConnections, FString TypeOfMatch, FString LobbyPath)
{
PathToLobby = FString.Printf(TEXT("%s?listen"), *LobbyPath);
}
void UMenu::OnCreateSession(bool bWasSuccessful)
{
...
World->ServerTravel(PathToLobby);
}
bug:刚从一个会话离开,再点Host会创建会话失败
是因为再UMultiplayerSessionSubsystem::CreateSession中,如果存在会话则销毁,然后创建新会话
但由于销毁会话需要通知所有玩家,存在网络时延,此时创建会话会失败
所以需要确保销毁后,才可以创建新会话
DestroySession
MultiplayerSessionsSubsystem.h
public:
...
// 销毁完成是否需要建立新会话
bool bCreateSessionOnDestroy{false};
// 记录建立新会话的数据
int32 LastNumPublicConnections;
FString LastMatchType;
MultiplayerSessionsSubsystem.cpp
void UMultiplayerSessionSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType)
{
...
if(ExistingSession != nullptr)
{
bCreateSessionOnDestroy = true;
LastNumPublicConnections = NumPublicConnections;
LastMatchType = MatchType;
// 调用函数
DestroySession();
} else {
...
}
}
void UMultiplayerSessionSubsystem::DestroySession()
{
if(!SessionInterface.IsValid())
{
MultiplayerOnDestroySessionComplete.Broadcast(false);
return;
}
// 监听销毁完成
DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegate);
if(!SessionInterface->DestroySession(NAME_GameSession))
{
SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);
MultiplayerOnDestroySessionComplete.Broadcast(false);
}
}
void UMultiplayerSessionSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
if(SessionInterface)
{
SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);
}
if(bWasSuccessful && bCreateSessionOnDestroy)
{
// 销毁后再建立新会话
bCreateSessionOnDestroy = false;
CreateSession(LastNumPublicConnections, LastMatchType);
}
MultiplayerOnDestroySessionComplete.Broadcast(bWasSuccessful);
}
完善菜单
退出游戏按钮
WBP_Menu中添加一个QuitButton,添加OnClick事件->QuitGame即可
禁用和启动按钮
- 不可以连续多次点击,点击后禁用
void UMenu::HostButtonClicked()
{
HostButton->SetIsEnabled(false);
...
}
void UMenu::JoinButtonClicked()
{
JoinButton->SetIsEnabled(false);
...
}
- 建立或加入会话失败后,应重新启动按钮
void UMenu::OnCreateSession(bool bWasSuccessful)
{
...
else
{
...
HostButton->SetIsEnabled(true);
}
}
void UMenu::OnFindSessions(...)
{
...
if(!bWasSuccessfule || SessionResults.Num() == 0)
{
JoinButton.SetIsEnabled(true);
}
}
void UMenu::OnJoinSession(...)
{
...
if(Result != EOnJoinSessionCompleteResult::Success)
{
JoinButton->SetIsEnabled(true);
}
}
Game Project
准备工作
- 新建工程
Blaster,将MultiplayerSessionsPlugin添加到项目中,启动Online Subsystem Steam插件
将MultiplayerSessions文件夹,放到项目目录的Plugins下(没有就新建)

- 更改配置文件
在DefaultEngine.ini中添加需要的配置

在DefaultGame.ini中添加玩家数量配置

-
插件生成代码
-
保存当前关卡为
GameStartupMap,然后新建Lobby关卡
指定起始地图

- 关卡蓝图中创建菜单控件,并指定
LobbyPath

补充修改,LastSessionSettings->bUseLobbiesIfAvailable=true
在MultiplayerSessionsSubsystem.cpp->Find相关中补充
关卡地图打包

- 创建
Build文件夹,然后打包程序到Build里
测试插件 & 使用ThirdPerson
现状:能够进入共同大厅,但是没有网络复制,看不到对方的操作;(默认使用的是空工程,没有人物)
如果使用Third Person,会自动同步动作
将Lobby的GameMode设置为ThirdPersonGameMode,此时就会同步动作了
导入资源
武器资源
资源商店Military Weapon免费,不支持UE5.0
解决办法:先添加到支持的UE版本,如Ue4.21,然后右键文件夹Migrate迁移到工程的Content上
角色资源
Unreal Learning Kit
动画资源
Animation Starter Pack
存在问题
- 缺少动画(如站立和蹲下的原位动画)
- 骨骼和要使用的人物骨骼不匹配
从Maximo获取动画
- 找到一个接近
UE骨骼的人物模型,老师选择这个,下载并导入UE


骨骼层次类似UE骨骼,但是命名还是不一样,后续动画需要调整
- 下载动画资源

- Turn right/left crouching: 原地,循环往右移动
- Turn right/left standing: 原地,循环右移
- Jump Up/Down/Loop: 跳跃
导入UE,不需要Import Mesh


如果出现重复动画资源,就删掉

Miximo重定位到UE(retarget)
- 打开
Retarget Manager

- 选择
Humanoid人形骨骼,修改Maximo骨骼到UE骨骼

- 使UE骨骼和
Maximo骨骼姿势相同,也摆成T-Pose

Maximo骨骼点击Apply To Asset

- 重定向动画资源


bug:动画重定向出现骨头错位




这样一般就能够修复该问题
最后整理一下动画

UE重定向到EpicCharacter
因为骨骼本身就与UE骨骼一致,所以不需要重定向骨骼
- 调整姿势为
T-Pose,并Modify Pose

- 重定向所有动画到
EpicCharacter

游戏实现一
创建角色
- 创建
BlasterCharacter:Character类,和对应蓝图类


BP_BlasterCharacter初始化
- 选择
skeletal mesh->SK_EpicCharacter Rotation-z->-90Location-z->-88(和胶囊体高度一致)
摄像机
先把人物拖到关卡中,并设置Auto Possess->Player 0,这样运行游戏就会自动持有了
- 添加摄像机和弹簧臂组件
BlasterCharacter.h
private:
UPROPERTY(VisibleAnywhere, Category = Camera)
class USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, Category = Camera)
class UCameraComponent* FollowCamera;
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(GetMesh());
CameraBoom->TargetArmLength = 600.f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
BP_BlasterCharacter中做些调整
CameraBoom的Location-z->88
人物移动
Project Settings->Input


- 代码中实现绑定
BlasterCharacter.h
protected:
void MoveForward(float Value);
void MoveRight(float Value);
void Turn(float Value);
void LookUp(float Value);
BlasterCharacter.cpp
void ABlasterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 从Character继承的Jump函数
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ThisClass::Jump);
PlayerInputComponent->BindAxis("MoveForward", this, &ThisClass::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ThisClass::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &ThisClass::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &ThisClass::LookUp);
}
void ABlasterCharacter::MoveForward(float Value)
{
if(Controller() != nullptr && Value != 0.f)
{
// 得到控制器旋转(随鼠标x轴旋转),也是摄像机旋转
const FRotator YawRotation(0.f, Controller->GetControllerRotation().Yaw, 0.f);
// 得到旋转的forward方向,即沿着摄像机正前方移动
const FVector Direction(FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X));
// 移动速度和加速度,在MovementComponent中设置
AddMovementInput(Direction, Value);
}
}
void ABlasterCharacter::MoveRight(float Value)
{
if(Controller() != nullptr && Value != 0.f)
{
// 得到控制器旋转(随鼠标x轴旋转)
const FRotator YawRotation(0.f, Controller->GetControllerRotation().Yaw, 0.f);
// 得到旋转的Right方向
const FVector Direction(FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y));
// 移动速度和加速度,在MovementComponent中设置
AddMovementInput(Direction, Value);
}
}
void ABlasterCharacter::Turn(float Value)
{
AddControllerYawInput(Value);
}
void ABlasterCharacter::LookUp(float Value)
{
AddControllerPitchInput(Value);
}
移动动画
- 先创建一个
BlasterAnimInstance:AnimInstance类
BlasterAnimInstance.h
public:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaTime) override;
private:
UPROPERTY(BlueprintReadOnly, Category = Character, meta = (AllowPrivateAccess = "true"))
class ABlasterCharacter BlasterCharacter;
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true"))
float speed;
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true"))
bool bIsInAir;
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true"))
bool bIsAccelerating;
BlasterAnimInstance.cpp
void UBlasterAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
BlasterCharacter = Cast<ABlasterCharacter>(TryGetPawnOwner());
}
void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
Super::NativeUpdateAnimation(float DeltaTime);
if(BlasterCharacter == nullptr)
{
BlasterCharacter = Cast<ABlasterCharacter>(TryGetPawnOwner());
}
if(BlasterCharacter == nullptr) return;
FVector Velocity = BlasterCharacter->GetVelocity();
Velocity.Z = 0.f;
Speed = Velocity.Size();
bIsInAir = BlasterCharacter->GetCharacterMovement()->IsFalling();
bIsAccelerating = BlasterCharacter->GetCharacterMovement()->GetCurrentAcceleration().Size() > 0.f ? true : false;
}
- 创建动画蓝图
ABP_Blaster:AnimInstance,打开后在Class Setting中设置父类为BlasterAnimInstance
Unequipped状态机

- 内部状态,添加对应输出姿势

动作后摇太大,就早点结束规则

- 对于
IdleWalkRun使用1D混合空间,BS_UnequippedIdleWalkRun


- 最后在
BP_BlasterCharacter中设置使用动画蓝图ABP_Blaster
bug: 人物并没有朝视角方向移动,原因Actor随控制器旋转,没有朝向运动方向
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
bUseControllerRotationYaw = false; // 不随控制器旋转
GetCharacterMovement()->bOrientRotationToMovement = true; // 朝向运动方向
}
同时注意,c++修改,蓝图中还需要恢复默认值!!!
无缝切换 (Seamless Travel)
大厅关卡
监听玩家数量,数量够了就进入游戏
- 创建
LobbyGameMode:GameMode
LobbyGameMode.h
public:
virtual void PostLogin(APlayerController* New Player) override;
注意:GameMode只存在于服务器,客户端会随着服务端改变关卡而改变
LobbyGameMode.cpp
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num();
if(NumberOfPlayers == 2)
{
UWorld* World = GetWorld();
if(World)
{
// 使用无缝切换
bUseSeamlessTravel = true;
// 只存在于服务器,客户端会随着切换
World()->ServerTravel(FString("/Game/Maps/BlasterMap?listen"));
}
}
}
-
创建
BP_LobbyGameMode:LobbyGameMode蓝图类,设置Default Pawn Class->BP_BlasterCharacter -
Lobby关卡World Settings->BP_LobbyGameMode
过渡关卡
-
创建
Empty Level(越简单越好),命名为TransitionMap -
Project Settings->Mpas & Modes
Transition Map->TransitionMapMap Package->TransitionMap
关卡润色
GameStartup放置一个骨骼网格,Animation Mode->Use Animation Asset

游戏关卡模式
-
创建
BP_BlasterGameMode:GameMode,Default Pawn Class->BP_BlasterCharacter -
BlasterMap选择World Settings->GmaeMode->BP_BlasterGameMode
Network Role
- 创建
UW_Overhead:UserWidget类
UW_Overhead.h
public:
UPROPERTY(meta = (BindWidget)) // 与蓝图中的控件绑定
class UTextBlock* DisplayText;
- 创建
WBP_Overhead,Class Settings->UW_Overhead
- 删除
Canvas,添加Text->命名为DisplayText, 屏幕大小拖拽到仅文本大小
- 继续丰富
UW_Overhead类
UW_Overhead.h
public:
void SetDisplayText(FString TextToDisplay);
UFUNCTION(BlueprintCallable)
void ShowPlayerNetRole(APawn* InPawn);
protected:
// virtual void NativeDestrct() override;
virtual void OnLevelRemoveFromWorld(ULevel* InLevel, UWorld* InWorld) override;
UW_Overhead.cpp
void UUW_Overhead::SetDisplayText(FString TextToDisplay)
{
if(DisplayText)
{
DisplayText->SetText(FText::FromString(TextToDisplay));
}
}
void UUW_Overhead::ShowPlayerNetRole(APawn* InPawn)
{
ENetRole LocalRole = InPawn->GetLocalRole();
FString Role;
switth(LocalRole)
{
case ENetRole::ROLE_Authority:
Role = FString("Authority");
break;
case ENetRole::ROLE_AutonomousProxy:
Role = FString("Autonomous Proxy");
break;
case ENetRole::ROLE_SimulatedProxy:
Role = FString("Simulated Proxy");
break;
case ENetRole::ROLE_None:
Role = FString("None");
break;
}
FString LocalRoleString = FString::Printf(TEXT("Local Role: %s"), *Role);
SetDisplayText(LocalRoleString);
}
void UUW_Overhead::OnLevelRemoveFromWorld(ULevel* InLevel, UWorld* InWorld)
{
RemoveFromParent(); // 先移除视口
Super::OnLevelRemoveFromWorld(InLevel, InWorld);
}
- 在
BlasterCharacter中持有该组件
BlasterCharacter.h
private:
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
class UWidgetComponent* OverheadWidget;
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
OverheadWidget = CreateDefaultSubobject<<UWidgetComponent>(TEXT("OverheadWidget"));
OverheadWidget->SetupAttachment(RootComponent);
}
- 在
BP_BlasterCharacter中设置OverheadWidget
拖拽到头上方

BeginPlay

Local & Remote Net Role
- Listen Server 3人
- Local Net Role
服务端:都是Authority
客户端:当前控制的角色是AutonomousProxy,其他角色是SimulatedProxy- Remote Net Role
服务端:当前控制的角色是AutomousProxy,其他角色是SimulateProxy
客户端:都是Authority
- Remote Net Role
武器
Weapon类及蓝图
- 创建
Weapon:Actor类
Weapon.h
private:
UPROPERTY(VisiableAnywhere, Category = "Weapon Properties")
USkeletalMeshComponent* WeaponMesh; // 武器的骨骼网格
UPROPERTY(VisiableAnywhere, Category = "Weapon Properties")
class USphereComponent* AreaSphere; // 摄取范围触发
Weapon.cpp
AWeapon::AWeapon()
{
PrimaryActorTick.bCanEverTick = false;
// 因为只在服务器见检测碰撞,所以状态变化需要复制
// Actor使可复制的,其中变量才可以设置Replicated
bReplicates = true;
WeaponMesh = CreateDefaultSubobject<USkeletalMesh>(TEXT("WeaponMesh"));
SetRootComponent(WeaponMesh);
// 没被持有时,枪支会和任何物体碰撞,但不会和人物碰撞
Weapon->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
Weapon->SetCollisionResponToChannel(ECollisionChnanel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
Weapon.SetCollisionEnabled(ECollisionEnabled::NoCollision); // 持有就没有碰撞,丢下后才开启
AreaSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AreaSphere"));
AreaSphere->SetupAttachment(RootComponent);
// 重叠检测应该在服务器上做,所以默认关闭,在服务器上才开启
AreaSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void AWeapon::BeginPlay()
{
Super::BeginPlay();
if(HasAuthority)
{
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}
}
- 武器状态枚举
Weapon.h
UENUM(BlueprintType)
enum class EWeaponState : uint8
{
// DisplayName是在蓝图中显示的枚举名字
EWS_Initial UMETA(DisplayName = "Initial State"),
EWS_Equipped UMETA(DisplayName = "Equipped"),
EWS_Dropped UMETA(DisplayName = "Dropped"),
EWS_MAX UMETA(DisplayName = "DefaultMAX")
};
UCLASS()
class BLASTER_API AWeapon : public AActor
{
private:
UPROPERTY(VisibleAnyWhere, Category = "Weapon Properties")
EWeaponState WeaponState;
}
- 创建
BP_Weapon:Weapon蓝图类,调整一下AreaSphere的位置和半径
Widget组件
显示文字指示,按E拾取武器
-
创建
WBP_PickupWidget,添加Text命名PickupText,初始文本E-Pick Up,调整屏幕大小仅包含文本 -
Weapon类中添加WidgetComponent
Weapon.h
private:
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class UWidgetComponent* PickupWidget;
Weapon.cpp
AWeapon::AWeapon()
{
...
PickupWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PickupWidget"));
PickupWidget.SetupAttachment(RootComponent);
}
- 然后打开
BP_Weapon调整Pickup Widget位置并设置

- 隐藏与显示
- 显示
Weapon.h
protected:
// 绑定到overlap委托中,需要UFUNCTION
UFUNCTION()
virtual void OnSphereOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
);
Weapon.cpp
void AWeapon::BeginPlay()
{
if(HasAuthority)
{
...
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnSphereOverlap);
}
if(PickupWidget)
{
PickupWidget->SetVisibility(false);
}
}
void AWeapon::OnSphereOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
)
{
ABlasterCharacter BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if(BlasterCharacter && PickupWidget)
{
// 玩家靠近,就显示提示
PickupWidget->SetVisibility(true);
}
}
bug: 客户端/服务端靠近武器,提示仅在服务端上显示
这是因为BeginOverlap函数仅在服务端进行绑定,解决思路:
- 在
BlasterCharacter新增一个变量OverlapWeapon用来描述是否靠近武器,同时复制这个OverlapWeapon,在OnRep_OverlapWeaon中显示PickupWidget - 注意这种情况下,服务端也不会显示,因为
OnRep_只会在客户端复制时调用
BlasterCharacter.h
protected:
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
private:
PROPERTY(ReplicatedUsing=OnRep_OverlapingWeapon)
AWeapon* OverlapWeapon;
UFUNCTION()
void OnRep_OverlapingWeapon();
public:
FORCEINLINE void SetOverlapingWeapon(AWeapon* Weapon) {OverlappingWeapon = Weapon;}
BlasterCharacter.cpp
void BlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DEREPLIFETIME(ABlasterCharacter, OverlappingWeapon); // 复制给所有客户端
DEREPLIFETIME_CONDITION(ABlasterCharacter, OverlappingWeapon, COND_OwerOnly); // 只复制给拥有者,如客户端1靠近武器,导致服务端OverlapWeapon改变并显示提示,则只复制给客户端1
}
void AWeapon::OnRep_OverlapWeapon()
{
if(OverlapWeapon)
{
OverlapWeapon->ShowPickupWidget(true);
}
}
Weapon.h
public:
void ShowPickupWidget(bool bShowWidget);
Weapon.cpp
void AWeapon::OnSphereOverlap(...)
{
...
if(BlasterCharacter)
{
// 替换为更改状态
BlasterCharacter->SetOverlapWeapon(this);
}
}
void AWeapon::ShowPickupWidget(bool bShowWidget)
{
if(PickupWidget)
{
PickupWidget.SetVisibility(bShowWidget);
}
}
bug: OnRep只在客户端通知,导致ListenServer靠近武器不会显示提示
解决思路:服务器在处理Overlap时间时,判断是否是本地控制,即服务器自己控制的角色,是的话直接调用OnRep_OverlappingWeapon即可
BlasterCharacter.cpp
void ABlasterCharacter::SetOverlappingWeapon(...)
{
...
if(IsLocallyControlled())
{
// 服务器自己控制的角色
OverlappingWeapon->ShowPickupWidget(true);
}
}
- 隐藏
如果离开武器,就隐藏提示
bug: 服务端离开不会隐藏
Weapon.h
protected:
...
UFUNCTION()
virtual void OnSphereEndOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex
);
private:
UFUNCTION()
void OnRep_OverlapingWeapon(AWeapon* LastWeapon); // 只能有一个参数,就是上一次复制的值
Weapon.cpp
void AWeapon::AWeapon()
{
...
if(HasAuthority())
{
...
AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AWeapon::OnSphereEndOverlap);
}
}
void AWeapon::OnSphereEndOverlap(...)
{
ABlasterCharacter BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if(BlasterCharacter)
{
BlasterCharacter->SetOverlappingWeapon(nullptr);
}
}
void ABlaster::OnRep_OverlapingWeapon(AWeapon* LastWeapon)
{
if(OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true); // 如果新值不为空,就显示提示
}
if(LastWeapon)
{
LastWeapon->ShowPickupWidget(false); // 如果旧值不为空,就隐藏提示
}
}
void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
{
// IsLocallyControlled() 不需要这个条件
// 因为服务端只有LocalControlled角色才需要看到提示,所以非locallyControlled无论如何都需要隐藏
// 相当于让非LocallyContolled角色多隐藏了一遍,尽管原本就不会显示
if(OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(false);
}
OverlappingWeapon = Weapon;
...
}
装备武器
-
创建
BlasterComponents/CombatComponent:ActorComponent -
BlasterCharacter中持有该组件
BlasterCharacter.h
private:
UPROPERTY(VisibleAnywhere)
class UCombatComponent* Combat;
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
Combat = CreateDefaultSubobject<UCombatComponent>(TEXT("CombatComponent"));
// 组件内肯定含有变量,所以也需要复制
// 组件比较特殊:不需要设置LifetimeProps
Combat->SetIsReplicated(true);
}
- 添加持枪
Socket
找到SK_EpicCharacter_SkeletalMesh->hand_r->Add the Socket->RightHandSocket->Add Preview Asset

- 装备输入绑定

BlasterCharacter.h
public:
...
// 最早能够访问到组件的生命周期
virtual void PostInitializeComponents() override;
BlasterCharacter.cpp
void ABlasterCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
}
void ABlasterCharacter::SetupPlayerInputComponent(...)
{
...
PlayerInputComponent->BindAction("Equip", IE_Pressed, this, &ThisClass::EquipButtonPressed);
}
void ABlasterCharacter::EquipButtonPressed()
{
// 装备武器,也应该由服务器操作
if(Combat && HasAuthority)
{
Combat->EquipWeapon(OverlappingWeapon);
}
}
CombatComponent.h
public:
// 声明Character是组件的友元类,则Character中而可以随意访问protected和private的成员
friend class ABlasterCharacter;
void EquipWeapon(class AWeapon* WeaponToEquip);
private:
// 需要访问Character中的成员
class ABlasterCharacter* Character; // Owner应该也能获取
class AWeapon* EquippedWeapon;
CombatComponent.cpp
void ACombatComponent::EquipWeapon(class AWeapon* WeaponToEquip)
{
if(Character == nullptr || WeaponToEquip == nullptr) return;
EuippedWeapon = WeapnToEquip;
EquippedWeapon.SetWeaponState(EWeaponState::EWS_Equipped);
const USkeletalMeshSocket* HandSocket = Charcter->GetMesh()->GetSocketByName(FName("RightHandSocket"));
if(HandSocket)
{
HandSocket->AttachActor(EquippedWeapon, Character->GetMesh());
}
EquippedWeapon->SetOwner(Character);
EquippedWeapon->ShowPickupWidget(false);
}
Weapon.h
public:
FORCEINLINE void SetWeaponState(EWeaponState State) {WeaponState = State;}
bug: 装备武器只允许在服务端进行,所以客户端按E无响应->RPC解决
客户端要装备则通知服务端装备,服务端再同步到客户端
BlasterCharacter.h
protected:
UFUNCTION(Server, Reliable)
void Server_EquipButtonPressed();
BlasterCharacter.cpp
void ABlasterCharacter::EquipButtonPressed()
{
if(Combat)
{
if(HasAuthority)
{
// 服务端就直接装备
Combat->EquipWeapon(OverlappingWeapon);
}
else
{
// 客户端要通知服务端装备
// 因为Conbat组件是Replicated,所以只要让服务器装备,就会同步到所有的客户端(Conbat->EquippedWeapon)
Server_EquipButtonPressed();
}
}
}
void ABlasterCharacter::Server_EquipButtonPressed_Implementation()
{
// 装备武器,也应该由服务器操作
if(Combat)
{
Combat->EquipWeapon(OverlappingWeapon);
}
}
bug: 武器被装备后,服务端上不显示提示了,但是客户端还是会显示
因为装备武器时隐藏的提示组件,同样只在服务端执行
CombatComponent.cpp
// 这个函数只会在服务端执行
void ACombatComponent::EquipWeapon(class AWeapon* WeaponToEquip)
{
if(Character == nullptr || WeaponToEquip == nullptr) return;
EuippedWeapon = WeapnToEquip;
EquippedWeapon.SetWeaponState(EWeaponState::EWS_Equipped);
const USkeletalMeshSocket* HandSocket = Charcter->GetMesh()->GetSocketByName(FName("RightHandSocket"));
if(HandSocket)
{
HandSocket->AttachActor(EquippedWeapon, Character->GetMesh());
}
EquippedWeapon->SetOwner(Character);
EquippedWeapon->ShowPickupWidget(false); // 在这里隐藏
}
解决办法:服务端执行时应该通知客户端(1)是否显示提示控件(2)是否还需要sphere重叠检测
- 可以通过RPC,通知所有客户端
- 这两个条件都和
EWeaponState相关,所以复制EWeaponState通知即可
Weapon.h
public:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
private:
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties", ReplicatedUsingt=OnRep_WeaponState)
EWeaponState WeaponState;
UFUNCTION()
void OnRep_WeaponState();
public:
void SetWeaponState(EWeaponState State);
FORCEINLINE USphereComponent* GetAreaSphere() const { return AreaSphere; }
Weapon.cpp
AWeapon::AWeapon()
{
...
// Actor是可复制的,其成员才可以设置Replicated
bReplicates = true;
...
}
void AWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AWeapon, WeaponState);
}
void OnRep_WeaponState()
{
// 根据当前的武器状态,调整检测和提示显示
switch(WeaponState)
{
case EWeaponState::EWS_Equipped:
ShowPickupWidget(false);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
break;
}
}
void AWeapon::SetWeaponState(EWeaponState State)
{
WeaponState = State;
// 这个函数只在服务器上调用,需要手动调用`OnRep`
OnRep_WeaponState();
}
Combat中调用OnRep_WeaponState_Implementation
CombatComponent.cpp
void ACombatComponent::EquipWeapon(class AWeapon* WeaponToEquip)
{
if(Character == nullptr || WeaponToEquip == nullptr) return;
//
// AWeapon设置是可复制的,但是这俩变量好像还不能复制
// 找到Owner声明,可以看到UPRPERTY(ReplicatedUsing=OnRep_Owner),所以Owner本身就会复制
// EquippedWeapon没有设置Replicated,目前课程还没考虑复制
//
EuippedWeapon = WeapnToEquip;
EquippedWeapon->SetOwner(Character);
//
// Character会复制,所以改变骨骼网格也不需要考虑同步问题
//
const USkeletalMeshSocket* HandSocket = Charcter->GetMesh()->GetSocketByName(FName("RightHandSocket"));
if(HandSocket)
{
HandSocket->AttachActor(EquippedWeapon, Character->GetMesh());
}
// 改变Weapon状态需要处理
EquippedWeapon->SetWeaponState(EWeaponState::EWS_Equipped);
}
角色动画
装备武器
复制EquippedWeapon,保持bIsWeaponEquipped一致
- 复制
EquippedWeapon
CombatComponent.h
public:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
private:
UPROPERTY(Replicated)
AWeapon* EquippedWeapon; //装备武器的代码只在服务器上执行,所以需要复制
CombatComponent.cpp
void UCombatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UCombatComponent, EquippedWeapon);
}
- 动画类中增加是否装备武器的变量
BlasterAnimInstance.h
private:
UPROPERTY(BlueprintReadOnly, Category = Weapon, meta = (AllowPrivateAccess = "true"))
bool bWeaponEquipped;
BlasterAnimInstance.cpp
void ABlasterAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
...
bWeaponEquipped = BlasterCharacter->IsWeaponEquipped();
}
BlasterCharacter.h
public:
bool IsWeaponEquipped() { return (Combat && Combat->EquippedWeapon); }
-
调整动画蓝图`ABP_BlasterCharacter
- 根据是否装备切换状态机

蹲伏
- 输入绑定

- 角色类中添加绑定事件
BlasterCharacter.h
protected:
void CrouchButtonPressed();
BlasterCharacter.cpp
void ABlasterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
...
PlayerInputComponent->BindAction("Crouch", IE_Pressed,, this, &ThisClass::CrouchButtonPressed);
}
void ABlasterCharacter::CrouchButtonPressed()
{
// bIsCrouched=true;
if(bIsCrouched)
UnCrouch();
else
Crouch();
}
注意:Crouch()函数会进行同步处理,包括Replicate:bIsCrouched以及通知服务器广播,和Jump一样,不需要考虑同步问题

- 动画实例中添加变量
BlasterAnimInstance.h
private:
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true"))
bool bIsCrouched;
BlasterAnimInstance.cpp
void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
...
bIsCrouched = BlasterCharacter->bIsCrouched;
}
- 调整动画蓝图

bug: 不能蹲,需要设置NavAgentProps.bCanCrouch=true, Crouch()函数中会检查
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
GetCharacterMovement()->NavAgentProps.bCanCrouch = true;
}
瞄准
- 动作映射

- 类中绑定输入
BlasterCharacter.h
protected:
void AimButtonPressed();
void AimButtonReleased();
public:
bool IsAiming() { return Combat && Combat->bAiming; }
BlasterCharacter.cpp
void SetupPlayerComponent(...)
{
...
PlayerInputComponent->BindAction("Aim", IE_Pressed, this, &ThisClass::AimButtonPressed);
PlayerInputComponent->BindAction("Aim", IE_Released, this, &ThisClass::AimButtonReleased);
}
void ABlasterCharacter::AimButtonPressed()
{
if(Conmbat)
{
Conmbat.SetAiming(true);
}
}
void ABlasterCharacter::AimButtonReleased()
{
if(Conmbat)
{
Conmbat.SetAiming(false);
}
}
CombatComponent.h
private:
UPROPERTY(Replicated)
bool bAiming;
UFUNCTION(Server, Reliable)
void Server_SetAiming(bool bIsAiming);
public:
void SetAiming(bool bIsAiming) { bAiming = bIsAiming; }
CombatComponent.cpp
void UCombatComponent::SetAiming(bool bIsAiming)
{
bAiming = bIsAiming; // 客户端先变
Setver_SetAiming(bIsAiming); // 然后通知服务器变并广播
}
void UCombatComponent::Server_SetAiming_Implementation(bool bIsAiming)
{
bAiming = bIsAiming;
}
void UCombatComponent::GetLifetimeReplicatedProps(...) const
{
...
DOREPLIFETIME(UCombatComponent, bAiming);
}
- 动画实例类中添加变量
BlasterAnimInstance.h
private:
UPROPERTY(BlueprintReadOnly, Category = Combat, meta = (AlloPrivateAccess = "true"))
bool bAiming;
BlasterAnimInstance.cpp
void ABlasterAnimInstance::NativeUpdateAnimation(...)
{
...
bAiming = BlasterCharacter->IsAiming();
}
- 动画蓝图中设置状态

- 瞄准放大Zoom
Weapon.h
private:
// Zoomed FOV while aiming
UPROPERTY(EditAnywhere)
float ZoomedFOV = 30.f;
UPROPERTY(EditAnywhere)
float ZoomInterpSpeed = 20.f;
public:
FORCEINLINE float GetZoomedFOV() const { return ZoomedFOV; }
FORCEINLINE float GetZoomInterpSpeed() const { return ZoomInterpSpeed; }
CombatComponent.h
private:
// Aiming and FOV
float DefaultFOV;
float CurrentFOV;
float ZoomEndInterpSpeed = 30.f; // 控制恢复FOV的速度
void InterpFOV(float DeltaTime);
CombatComponent.cpp
void UCombatComponent::BeginPlay()
{
...
if(Character && Character->GetFollowCamera())
{
DefaultFOV = Character->GetFollowCamera()->FieldOfView;
CurrentFOV = DefaultFOV;
}
}
void UCombatComponent::TickComponent(float DeltaTime)
{
...
if(Character && Character->IsLocallyContolled)
{
// 本地控制的才需要调整FOV
InterpFOV(DeltaTime);
}
}
void UCombatComponent::InterpFOV(float DeltaTime)
{
if(EquippedWeapon == nullptr) return;
if(bAiming)
{
CurrentFOV = FMath::FInterpTo(CurrentFOV, EquippedWeapon->GetZoomedFOV(), DeltaTime, EquippedWeapon->GetZoomInterpSpeed());
}
else
{
CurrentFOV = FMath::FInterpTo(CurrentFOV, DefaultFOV, DeltaTime, ZoomEndInterpSpeed);
}
if(Character && Character->GetFollowCamera())
{
Character->GetFollowCamera()->SetFieldOfView(CurrentFOV);
}
}
BlasterCharacter.h
public:
FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; }
bug: zoom放大场景变糊
调整相机参数
Focal Distance: 看远处物体不会糊了,但看近处会糊Aperture: 使看近处也不糊了

装备武器移动
未装备时移动,可以Actor朝着移动方向,所以只需要1D Blend Space即可
而携带武器时,需要始终朝向鼠标瞄准位置,所以人物移动变为八向移动,使用2D Blend Space
- 创建
EquippedRun混合空间


-
制作倾斜动画
-
Fwd
- 找到
Jog_Fwd_Rifle动画,右键Duplicate,打开,选择root骨骼,朝右边旋转20度

- 添加
Key,并出另存为Jog_Fwd_Lean_R资源


-
删除刚才
Duplicate的Jog_Fwd_Rifle1 -
同理,制作
Jog_Fwd_Lean_L资源 -
添加到
BS_EquippedRun

- Right
-
找到
Jog_Rt_Rifle,同Fwd,先沿往右倾斜十度,存为Jog_Rt_Lean_R -
再往左十度,记为
Jog_Rt_Lean_L -
添加到
BS_EquippedRun

-
Lefe: 10度
-
Bwd: 10度
- 找到
最终效果

-
使用
BS_EquippedRun- 动画实例类中添加变量
注意,为什么需要在代码中插值过渡动画
如果在混合空间中过渡,向后移动,会出现
-180<->180的抽象过渡BlasterAnimInstance.h private: UPROPERTY(BlueprintReadOnly, Category = RifleMovement, meta = (AllowPrivateAccess = "true")) float YawOffset; UPROPERTY(BlueprintReadOnly, Category = RifleMovement, meta = (AllowPrivateAccess = "true")) float Lean; FRotator CharacterRotationLastFrame; FRotator CharacterRotation; FRotator DeltaRotation;BlasterAnimInstance.cpp void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) { ... // UE已经为人物速度和旋转实现了同步, 所以计算的结果(YawOffset,Lean)都一致 // // 都是世界坐标旋转 FRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 视角方向 FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 移动方向 FRotator DeltaRot = UKismetMathLibrary::NormalizeDeltaRotator(MovementRotation, AimRotation); DeltaRotation = FMath::RInterpTo(DeltaRotation, DeltaRot, DeltaTime, 6.f); YawOffset = DeltaRotation.Yaw; // 两帧之间人物旋转做插值,作为倾斜 CharacterRotationLastFrame = CharacterRotation; CharacterRotation = BlasterCharacter->GetActorRotation(); const FRotator Delta = UKimetMathLibrary::NormalizedDeltaRotator(CharacterRotation, CharacterRotationLastFrame); const float Target = Delta.Yaw / DeltaTime; const float Interp = FMath::FInterpTo(Lean, Target, DeltaTime, 6.f); Lean = FMath::Clamp(Interp, -90.f, 90.f); }- 装备武器下,人物不该朝着运动方向,而应该随控制器旋转
CombatComponent.h protected: UFUNCTION() void OnRep_EquippedWeapon(); private: UPROPERTY(ReplicatedUsing = OnRep_EquippedWeapon) AWeapon* EquippedWeapon;CombatComponent.cpp void UCombatComponent::EquipWeapon(...) { ... // 这个函数只会在服务器调用!!! OnRep_EquippedWeapon(); } void OnRep_EquippedWeapon() { if(EquippedWeapon && Character) { // 必须保证状态变化后,才附加到character // 因为状态变化可能导致SimulatePhysics,无法附加 const USkeletalMeshSocket* HandSocket = Chracter->GetMesh()->GetSocketByName(FName("RightHandSocket")); if(HandSocket) { HandSocket->AttachActor(EquippedWeapon, Character->GetMesh()); } Character->GetCharacterMovement()->bOrientRotationToMovement = false; Character->bUseControllerRotationYaw = true; } }- 更改
BP_BlasterAnim






-
创建
BS_CrouchWalking:1D Blend Space


- 修改
BP_BlasterAnim





瞄准移动
- 创建
BS_AimWalk:1D Blend Space

- 创建
BS_CrouchAimWalk:1D Blend Space

- 修改
BP_BlasterAnim
Run的时候瞄准,才会进入AimWalking


CrouchWalking的时候瞄准,进入CrouchAimWalking



- 修改
CombatComponent,瞄准后应该更新移动速度
CombatComponent.h
private:
UPROPERTY(EditAnywhere)
float BaseWalkSpeed;
UPROPERTY(EditAnywhere)
float AimWalkSpeed;
CombatComponent.cpp
UCombatComponent::UCombatComponent()
{
...
BaseWalkSpeed = 600.f;
AimWalkSpeed = 450.f;
}
void UCombatComponent::BeginPlay()
{
...
if(Character)
{
Character->GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
}
}
void UCombatComponent::Server_SetAiming_Implementation(bool bIsAiming)
{
...
if(Character)
{
Character->GetCharacterMovement()->MaxWalkSpeed = bIsAiming ? AimWalkSpeed : BaseWalkSpeed;
}
}
瞄准偏移
- 获取瞄准偏移动画
-
HipAO: 非瞄准状态下的持枪瞄准偏移
-
找到
Aim_Space_Hip动画,需要从中截取瞄准偏移动画,复制并重命名 -
依次找到对应帧,右键删除前面喝后面的,只保留一帧
-
复制
AO_CC命名为Zero_Pose,用作Additive基础

-
-
AimAO:找到
Aim_Space_Ironsight,操作同上

- 将动画都设置
Additive,通过批量属性矩阵修改

- 创建
HipAimOffset
视角旋转和角色朝向的差值

设置基础姿势


-
创建
AimAimOffset同上 -
应用
AimOffsetBlasterAnimInstance中获取数据
BlasterAnimInstance.h private: UPROPERTY(BlueprintReadOnly, Category = Aim, meta = (AllowPrivateAccess = "true")) float AO_Yaw; UPROPERTY(BlueprintReadOnly, Category = Aim, meta = (AllowPrivateAccess = "true")) float AO_Pitch;BlasterAnimInstance.cpp void UBlasterAnimInstance::NativeUpdateAnimation(...) { ... AO_Yaw = BlasterCharacter->GetAO_Yaw(); AO_Pitch = BlasterCharacter->GetAO_Pitch(); }BlasterCharacter.h protected: void AimOffset(float DeltaTime); private: float AO_Yaw; float AO_Pitch; FRotator StartingAimRotation; public: FORCEINLINE float GetAO_Yaw() const { return AO_Yaw; } FORCEINLINE float GetAO_Pitch() const { return AO_Pitch; }BlasterCharacter.cpp void ABlasterCharacter::Tick(float DeltaTime) { ... AimOffset(DeltaTime); } void ABlasterCharacter::AimOffset(float DeltaTime) { if(Combat && Combat->EnquippedWeapon == nullptr) return; FVector Velocity = GetVelocity(); Velocity.Z = 0.f; float Speed = Velocity.Size(); bool bIsInAir = GetCharacterMovement()->IsFalling(); if(Speed == 0.f && !bIsInAir) // 静止 && 没跳 { bUseControllerRotationYaw = false; //静止不随控制器旋转, 控制器旋转用于瞄准偏移 FRotator CurrentAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); FRotator DeltaAimRotation = UKismetMathLibrary::NormalizedDeltaRotator(CurrentAimRotation, StartingAimRotation); AO_Yaw = DeltaAimRotation.Yaw; // 当前AimRotation和起始Rotaion的插值 } if(Speed > 0.f || bIsInAir) // 跑 或 跳 { bUseControllerRotationYaw = true; // 控制器旋转控制人物旋转 StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); AO_Yaw = 0.f; // 此时不需要瞄准偏移,并且会更改起始Rotation } AO_Pitch = GetBaseAnimRotation().Pitch; if(AO_Pitch > 90.f && !IsLocallyControlled()) { FVector2D InRange(270.f, 360.f); FVector2D OutRange(-90.f, 0.f); AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch); } }- 在
BP_BlasterAnim中混合


bug: 控制端俯仰角正常,模拟端出现问题,原因是UE会压缩角度信息到无符号整数,[-90, 0]->[270, 360]
AO_Pitch = GetBaseAnimRotation().Pitch;
if(AO_Pitch > 90.f && !IsLocallyControlled())
{
FVector2D InRange(270.f, 360.f);
FVector2D OutRange(-90.f, 0.f);
AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch);
}
摄像机碰撞
其他玩家从摄像机面前走过时,摄像机会拉近到角色,存在摄像机碰撞
- 类中修改
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
GetMesh()->GetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
}
- 蓝图中修改

反向动力学
目前问题:武器和手并没有贴合,并且不同的武器,手放置的位置也不同
- 先给武器模型添加插槽

- 在动画类中获取变量
BlasterAinmInstance.h
private:
class AWeapon* EquippedWeapon;
UPROPERTY(BlueprintReadOnly, Category = IK, meta = (AllowPrivateAccess = "true"))
FTransfrom LeftHandTransform;
BlasterAnimInstance.cpp
void ABlasterAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
...
EquippedWeapon = BlasterCharacter->GetEquippedWeapon();
...
if(bWeaponEquipped && EquippedWeapon && EquippedWeapon->GetWeaponMesh() && BlasterCharacter->GetMesh())
{
// 武器左手的世界位置
LeftHandTransfrom = EquippedWeapon->GetWeaponMesh()->GetSocketTransform(FName("LeftHandSocket"), ERelativeTransformSpace::RTS_World);
FVector OutPosition;
FRotator OutRotation;
// 与人物的右手骨骼位置对齐
BlasterCharacter->GetMesh()->TransformToBoneSpace(FName("hand_r"), LeftHandTransform.GetLocation(), FRotator::ZeroRotator);
// 更新为对齐的位置
LeftHandTransform.SetLocation(OutPosition);
LeftHandTransform.SetRotation(FQuat(OutRotation));
}
}
- 动画蓝图中使用
FABRIK(Foward and Backward Reaching IK)
创建新的状态机FABRIK


- 运行游戏,并调整
LeftHandSocket的位置
根据游戏运行结果,调整位置

原地转身 (Turn in Place) & 旋转根骨骼
原地静止时,旋转鼠标会瞄准偏移
P54和P55
网络更新频率
- 配置网络速率
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
NetUpdateFrequency = 66.f;
MinNetUpdateFrequency = 33.f;
}
- 配置文件
DefalutEngine.ini
[/Script/OnlineSubsystemUtils.IpNetDriver]
NetServerMaxTickRate=60
脚步和跳跃音效
- 创建
Sounds->Sound Attenuation,给音效添加距离衰减

- 打开音效如
SCue_FS_Tile,选择新建的Sound Attenuation

- 在需要播放声音的动画中添加通知


武器攻击
动画
角色动画(会自动同步)
- 添加开火输入
BlasterCharacter.h
public:
void PlayFireMontage(bool bAiming);
protected:
void FireButtonPressed();
void FireButtonReleased();
private:
UPROPERTY(EditAnywhere, Category = Combat)
class UAnimMontage* FireWeaponMontage; // 在动画蓝图中赋值 FireWeapon
BlasterCharacter.cpp
void ABlasterCharacter::PlayFireMontage(bool bAiming)
{
if(Combat == nullptr || Combat->EquippedWeapon == nullptr) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if(AnimInstance && FireWeaponMontage)
{
AnimIntance->Montage_Play(FireWeaponMontage);
FName SectionName = bAiming ? FName("RifleAim") : FName("RifleHip");
AnimInstance->Montage_JumpToSection(SectionName);
}
}
void ABlasterCharacter::SetupPlayerInputComponent(...)
{
...
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ThisClass::FireButtonPressed);
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ThisClass::FireButtonReleased);
}
void ABlasterCharacter::FireButtonPressed()
{
if(Combat)
{
Combat->FireButtonPressed(true);
}
}
void ABlasterCharacter::FireButtonReleased()
{
if(Combat)
{
Combat->FireButtonPressed(false);
}
}
CombatComponent.h
protected:
void FireButtonPressed(bool bPressed);
private:
bool bFireButtonPressed;
CombatComponent.cpp
void UCombatComponent::FireButtonPressed(bool bPressed)
{
bFireButtonPressed = bPressed;
if(Character && bFireButtonPressed)
{
Character->PlayFireMontage(bAiming);
}
}
- 把开火动画调整成
Additive的
Fire_Rifle_Hip:持枪非瞄准的开火动画
Fire_Rifle_Ironsight:持枪瞄准的开火动画


- 创建蒙太奇动画,右键
Fire_Rifle_Hip->Create->Create AnimMontage,命名为FireWeapon- 右键新建
RifleHip,并删除Default部分
- 右键新建

2. Anim Slot Manager->Add Slot->WeaponSlot,并选择使用

3. 把Fire_Rifle_Ironsights也拖进来,并创建Montage Section

Montage Sessions中Clear

武器动画(包含开火特效)(多播RPC)
- 添加开火动画播放

Weapon.h
public:
void Fire();
private:
UPROPERTY(EditAnywhere, Category = "Weapon Properties")
class UAnimationAsset* FireAnimation;
Weapon.cpp
void AWeapon::Fire()
{
if(FireAnimation)
{
WeaponMesh->PlayAnimation(FireAnimation, false); // 是否循环
}
}
CombatComponent.h
CombatComponent.cpp
void UCombatComponent::FireButtonPressed(bool bPressed)
{
...
if(Character && EquippedWeapon && bFireButtonPressed)
{
Character->PlayFireMontage(bAiming);
EquippedWeapon->Fire();
}
}
- 创建武器蓝图
BP_AssaultRifle:ProjectileWeapon,给组件赋值并指定开火动画

bug: 武器开火动画没有同步
不管是客户端还是服务端开火,都只在本地播放特效及动画,其他端没有反应;
说明开火特效及动画并没有复制,所以服务端行为不会广播到其他客户端,通过RPC解决问题。
CombatComponent.h
protected:
UFUNCTION(Server, Reliable)
void Server_Fire();
UFUNCTION(NetMulticast, Reliable)
void Multicast_Fire();
CombatComponent.cpp
void UCombatComponent::FireButtonPressed(bool bPressed)
{
bFireButtonPressed = bPressed;
if(bFireButtonPressed)
{
Server_Fire();
}
}
void UCombatComponent::Server_Fire_Implementation()
{
Multicast_Fire();
}
void UCombatComponent::Multicast_Fire_Implementation()
{
f(Character && EquippedWeapon)
{
Character->PlayFireMontage(bAiming);
EquippedWeapon->Fire();
}
}
射击方向
- 得到十字准信的目标
CombatComponent.h
protected:
// 获取当前十字准信的瞄准结果,以引用参数的方式返回
void TraceUnderCrosshairs(FHitResult& TraceHitResult);
CombatComponent.cpp
#define TRANCE_LENGTH 80000
void UCombatComponent::TranceUnderCrosshairs(FHitResult& TraceHitResult)
{
// 获取准心屏幕坐标
FVector2D ViewportSize();
if(GEngine && GEngine->GetViewport())
{
GEngine->GetViewport()->GetViewportSize(ViewportSize);
}
FVector2D CrosshairLocation(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
FVector CrosshairWorldPosition, CrosshairWorldRotation; // 屏幕坐标转世界坐标, 变换矩阵的逆
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0),
CrosshairLocation,
CrosshairWorldPosition,
CrosshairWorldRotation
);
if(bScreenToWorld)
{
// 射线检测
FVector Start = CrosshairWorldPosition; // 不应该从摄像机开始,会被自己人物/身后人物遮挡
if(Character)
{
// 应该从自己人物身前打射线
float DistanceToCharacter = (Character->GetActorLocation - Start).size();
Start += (DistanceToCharacter + 100.f) * CrosshairWorldDirection;
}
FVector End = Start + CrosshairWorldDirection * TRACE_LENGTH;
GetWorld()->LineTraceSingleByChannel(
TraceHitResult,
Start,
End,
ECollisionChannel::ECC_Visibility
);
if(!TraceHitResult.bBlockingHit)
{
// 如果没打到物体,就设置到终点
TraceHitResult.ImpactPoint = End;
}
else
{
DrawDebugSphere(
GetWorld(),
TraceHitResult.ImpactPoint,
12.f,
12,
FColor::Red
);
}
}
}
bug: 打不到任何场景物体
因为被人物模型遮挡,一般摄像机都会在人物的右上方,设置弹簧臂

武器朝向射击方向
P75
生成炮弹
- 新建
Projectile:Actor,作为炮弹,并给与初始速度
Projectile.h
protected:
UFUNCTION()
virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult); // 碰撞检测函数
// 会自动广播给所有客户端,可以利用来生成特效及声音,不必使用PRC
virtual void Destroyed() override;
UPROPERTY(EditAnywhere)
float Damage = 20.f;
private:
UPROPERTY(EditAnywhere)
class UBoxComponent* CollisionBox; // 碰撞检测
UPROPERTY(VisibleAnywhere)
class UProjectileMovementComponent* ProjectileMovementComponent; // 投射运动
UPROPERTY(EditAnywhere)
UParticleSystem* ImpactParticles; // 炮弹爆炸特效
UPROPERTY(EditAnywhere)
USoundCue* ImpactSound; // 炮弹爆炸音效
Projectile.cpp
AProjectile::AProjectile()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = true; // 网络同步
//
// 碰撞盒初始化
//
CollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
SetRootComponent(CollisionBOx);
CollisionBox->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
// 设置碰撞对象
CollisionBox->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
// 设置碰撞通道
CollisionBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Block);
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
//
// 抛射运动组件
//
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
ProjectileMovementComponent->bRotationFollowVelocity = true; // 弹头朝着运动方向
}
void AProjectile::BeginPlay()
{
...
if(HasAuthority())
{
CollisionBox->OnComponentHit.AddDynamic(this, &ThisClass::OnHit);
}
}
void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult)
{
Destroy(); // 销毁会自动广播给所有客户端,所以没必要用rpc
}
void AProjectile::OnDestroy()
{
Super::Destroyed();
if(ImpaceParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, GetActorTransform());
}
if(ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation());
}
}
- 创建
BP_Projectile:Projectile




- 给枪支模型添加开火点
确保x轴朝前方,这样炮弹生成就会往前方发射

- 新建
ProjectileWeapon:Weapon
ProjectileWeapon.h
public:
virtual void Fire(const FVector& HitTarget) override;
private:
UPROPERTY(Editwhere)
TSubclassOf<class AProjectile> ProjectileClass;
ProjectileWeapon.cpp
void AProjectileWeapon::Fire(const FVector& HitTarget)
{
Super::Fire(HitTarget);
if(!HasAuthority()) return; // 服务器才可以生成炮弹
const USkeletalMeshSocket* MuzzleFlashSocket = GetWeaponMesh()->GetSocketByName(FName("MuzzleFlash"));
if(MuzzleFlashSocket)
{
FTransform SocketTransform = MuzzleFlashSocket->GetSocketTransform(GetWeaponMesh());
FVector ToTarget = HitTarget - SocketTransform.GetLocation();
FRotator TargetRotation = ToTarget.Rotation(); // 得到发射方向
APawn* InstigatorPawn = Cast<APawn>(GetOwner());
if(ProjectileClass && InstigatorPawn)
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner(); // 拥有者
SpawnParams.Instigator = InstigatorPawn; // 始作俑者
UWorld* World = GetWorld();
if(World)
{
World->SpawnActor<AProjectile>(
ProjectileClass,
SocketTransform.GetLocation(), // 发射点
TargetRotation,
SpawnParams
);
}
}
}
}
Weapon.h
public:
virtual void Fire(const FVector& HitTarget);
Weapon.cpp
void AWeapon::Fire(const FVector& HitTarget)
{
if(FireAnimation)
{
WeaponMesh->PlayAnimation(FireAnimation, false);
}
}
CombatComponent.h
protected:
UFUNCTION(Server, Reliable) // 网络量化版的vector,最多存储2^20大小的整数范围,节省带宽
void Server_Fire(const FVector_NetQuantize& HitTarget);
UFUNCTION(NetMulticast, Reliable)
void Multicast_Fire(const FVector_NetQuantize& HitTarget);
CombatComponent.cpp
void UCombatComponent::FireButtonPressed(bool bPressed)
{
...
if(bFireButtonPressed)
{
// 只在服务器调用,适合射线检测
FHitResult& HitResult;
TranceUnderCrosshairs(HitResult);
ServerFire(HitTarget.ImpactPoint); // 注意:ImpacePoint本来就被优化为FVector_NetQuantize
}
}
void UCombatComponent::Server_Fire_Implementation(const FVector_NetQuantize& HitTarget)
{
Multicast_Fire(HitTarget);
}
void UCombatComponent::Multicast_Fire_Implementation(const FVector_NetQuantize& HitTarget)
{
f(Character && EquippedWeapon)
{
Character->PlayFireMontage(bAiming);
EquippedWeapon->Fire(HitTarget);
}
}
- 打开
BP_AssaultRifle,选择炮弹蓝图

- 直接造成伤害的炮弹,创建
ProjectileBullet:Projectile
ProjectileBullet.h
protected:
virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult) override;
ProjectileBullet.cpp
void AProjectileBullet::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult)
{
ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
if(OwnerCharacter)
{
AController* OwnerController = OwnerCharacter->Controller;
if(OwnerController)
{
UGameplayStatics::ApplyDamage(OtherActor, Damage, OwnerController, this, UDamageType::StaticClass());
}
}
// 父类要放到最后!! 因为父类中调用了Destroy()
Super::OnHit(HitComp, OtherActor, OtherComp, NormalImpluse, HitResult);
}
- 创建
BP_ProjectileBullet:ProjectileBullet



BP_AssaultRifle中的子弹类型调整为BP_ProjectileBullet

子弹踪迹
给子弹添加Attach Socket
Projectile.h
private:
UPROPERTY(EditAnywhere)
UParticleSystem* Tracer; // 蓝图中指定对应特效
UPROPERTY(EditAnywhere)
UParticleSystemComponent* TracerComponent;
Projectile.cpp
void AProjetile::BeginPlay()
{
...
if(Tracer)
{
TracerComponent = UGameplay::SpawnEmitterAttached(
Tracer,
CollisionBox,
FName(), // 如果需要附加到某个骨骼上就添加,不需要就置空
GetActorLocation(),
GetActorRotation(),
EAttachLocation::KeepWorldPosition
);
}
}
子弹壳

- 创建
Casing:Actor
Casing.h
private:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* CasingMesh;
UPROPERTY(EditAnywhere)
float ShellEjectionImpluse;
Casing.cpp
ACasing::ACasing()
{
CasingMesh = CreateDefaultSubobject<UStaticMeshComponent>("CasingMesh");
SetRootComponent(CasingMesh);
CasingMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore); // 忽略摄像机碰撞
CasingMesh->SetSimulatePhysics(true); // 模拟物理
CasingMesh->SetEnabledGravity(true); // 应用重力
ShellEjectionImpluse = 10.f;
}
void ACasing::BeginPlay()
{
...
CasingMesh->AddImpluse(GetActorForwardVector() * ShellEjectionImpluse);
}
-
创建
BP_Casing蓝图,指定StaticMesh -
枪支中持有弹壳
Weapon.h
private:
UPROPERTY(EditAnywhere)
TSubclassOf<class ACasing> CasingClass;
Weapon.cpp
void AWeapon::Fire(...)
{
...
if(CasingClass)
{
const USkeletalMeshSocket* AmmoEjectSocket = WeaponMesh->GetSocketByName(FName("AmmoEject"));
if(AmmoEjectSocket)
{
FTransform SocketTransform = AmmoEjectSocket->GetSocketTransform(WeaponMesh);
FWorld* World = GetWorld();
if(World)
{
World->SpawnActor<ACasing>(
CasingClass,
SocketTransform.GetLocation(),
SocketTransform.GetRotation().Rotator()
);
}
}
}
}
给弹壳添加光晕
-
找到弹壳材质的父类

-
按住
4+鼠标左键创建4维向量,连接到Emissive Color,然后右键Convert to Parameter,命名为Emissive -
接着把弹壳材质复制一份命名为
M_Ammo,进入调整EMissive

弹壳落地音效及销毁
Casing.h
private:
UFUNCTION()
virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult); // 碰撞检测函数
URPOPERTY(EditAnywhere)
SoundCue* ShellSound;
Casing.cpp
ACasing::ACasing()
{
....
CasingMesh->SetSimulatePhysics(true); // 模拟物理
CasingMesh->SetEnabledGravity(true); // 应用重力
CasingMesh->SetNotifyRigidBodyCollision(true); // 通知刚体碰撞
....
}
void ACasing::BeginPlay()
{
...
CasingMesh->OnComponentHit.AddDynamic(this, &ThisClass::OnHit);
...
}
void ACasing::OnHit(...)
{
if(ShellSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ShellSound, GetActorLocation());
}
// Destroy();
SetLifeSpan(3.f);
}
自动开火
CombatComponent.h
private:
UPROPERTY(EditAnywhere, Category = Combat)
bool bCanFire = true;
FTimerHandle FireTimer;
void StartFireTimer();
void FireTimerFinished();
CombatComponent.cpp
void UCombatComponent::FireButtonPressed(bool bPressed)
{
...
if(bFireButtonPressed && EquippedWeapon)
{
Fire();
}
}
void UCombatComponent::Fire()
{
if(bCanFire)
{
bCanFire = false;
Server_Fire();
CrosshairShootingFactor = 0.75f;
StartFireTimer(); // 开火就开启下一次Timer
}
}
void UCombatComponent::StartFireTimer()
{
if(Character == nullptr || EquippedWeapon == nullptr) return;
Character->GetTimerManager()->SetTimer(
FireTimer,
this,
&UCombatComponent::FireTimerFinished,
EquippedWeapon->FireDelay
);
}
void UCombatComponent::FireTimerFinished()
{
if(EquippedWeapon == nullptr) return;
bCanFire = true;
if(bFireButtonPressed && EquippedWeapon->bAutomatic)
{
// 如果还在开火,就继续Fire
Fire(); // 每次进入bCanFire都会->false,如果没进Fire,就肯定为true了
}
}
Weapon.h
public:
UPROPERTY(EditAnywhere, Category = Combat)
float FireDelay = .15f;
UPROPERTY(EditAnywhere, Category = Combat)
bool bAutomatic = true;
HUD (十字准心)
Heah-Up Display,和UMG(UI)很类似,不同之处在于,HUD是在角色前方显示的和游戏内容相关的信息,比如准星、枪械信息、血量等,这些信息一般是不需要交互的。

- 移动和跳跃,准心扩散
- 瞄准准心收缩
- 射击准心扩散
-
创建
BlasterPlayerController:PlayerController -
创建
BP_BlasterPlayerController:BlasterPlayerController -
创建
BlasterHUD:HUD
BlasterHUD.h
USTRUCT(BlueprintType)
struct FHUDPackage
{
GENERATED_BODY()
public:
UTexture2D* CrosshairsCenter;
UTexture2D* CrosshairsLeft;
UTexture2D* CrosshairsRight;
UTexture2D* CrosshairsTop;
UTexture2D* CrosshairsBottom;
float CrosshairSpread; // 准心扩散
FLinearColor CrosshairsColor;
}
class BlasterHUD ...
{
...
public:
virtual void DrawHUD() override;
private:
FHUDPackage HUDPackage;
void DrawCrosshair(UTexture2D* Texture, FVector2D ViewportCenter, FVector2D Spread);
UPROPERTY(EditAnywhere)
flaot CrosshairSpreadMax = 16.f;
public:
FORCEINLINE void SetHUDPackage(const FHUDPackage& Package) { HUDPackage = Package; }
}
BlasterHUD.cpp
// 每帧调用
void ABlasterHUD::DrawHUD()
{
Super::DrawHUD();
// 绘制准心
FVector2D ViewportSize;
if(GEngine)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
const FVector2D ViewportCenter(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
// 扩散强度乘上基数
float SpreadScaled = CrosshairSpreadMax * HUDPackage.CrosshairSpread;
if(HUDPackage.CrosshairsCenter)
{
FVector2D Spread(0.f, 0.f); // 不需要扩散
DrawCrosshair(CrosshairsCenter, ViewportCenter, Spread);
}
if(HUDPackage.CrosshairsLeft)
{
FVector2D Spread(-SpreadScaled, 0.f); // 往左扩散
DrawCrosshair(CrosshairsLeft, ViewportCenter, Spread);
}
if(HUDPackage.CrosshairsRight)
{
FVector2D Spread(SpreadScaled, 0.f); // 往右扩散
DrawCrosshair(CrosshairsRight, ViewportCenter, Spread);
}
if(HUDPackage.CrosshairsTop)
{
FVector2D Spread(0.f, -SpreadScaled); // 往上扩散
DrawCrosshair(CrosshairsTop, ViewportCenter, Spread);
}
if(HUDPackage.CrosshairsBottom)
{
FVector2D Spread(0.f, SpreadScaled); // 往下扩散
DrawCrosshair(CrosshairsBottom, ViewportCenter, Spread);
}
}
}
void ABlasterHUD::DrawCrosshair(UTexture2D* Texture, FVector2D ViewportCenter, FVector2D Spread)
{
const float TextureWidth = Texture->GetSizeX();
const float TextureHeight = Texture->GetSizeY();
// 屏幕坐标从左上角开始,往右x正轴,往下y正轴,所有纹理要往左上方平移
const FVector2D TextureDrawPoint(
ViewportCenter.X - TextureWidth / 2.f + Spread.X, // 扩散方向
ViewportCenter.Y - TextureHeight / 2.f + Spread.Y // 扩散方向
);
DrawTexture(
Texture,
TextureDrawPoint.X,
TextureDrawPoint.Y,
TextureWidth,
TextureHeight,
0.f,
0.f,
1.f,
1.f,
HUDPackage.CrosshairColor // 颜色
);
}
- 枪上添加准心纹理
Weapon.h
public:
UPROPERTY(EditAnywhere, Category = Crosshairs)
UTexture2D* CrosshairsCenter;
UPROPERTY(EditAnywhere, Category = Crosshairs)
UTexture2D* CrosshairsLeft;
UPROPERTY(EditAnywhere, Category = Crosshairs)
UTexture2D* CrosshairsRight;
UPROPERTY(EditAnywhere, Category = Crosshairs)
UTexture2D* CrosshairsTop;
UPROPERTY(EditAnywhere, Category = Crosshairs)
UTexture2D* CrosshairsBottom;
- 获取纹理资源
- github仓库
- 右键属性矩阵,统一压缩格式改为
UserInterface2D(RGBA)
-
创建
BP_BlasterHUD:BlasterHUD,将BP_BlasterGameMode->HUD Class->BP_BlasterHUD -
在战斗组件中设置HUD中的准心数据
ComabatComponent.h
protected:
// 准心可能随角色速度、后坐力等发生变化,涉及到插值,输入参数需要DeltaTime
void SetHUDCrosshairs(float DeltaTime);
private:
ABlasterPlayerController* Controller;
ABlasterHUD* HUD;
FHUDPackage HUDPackage;
float CrosshairVelocityFactor; // 速度扩散因子
float CrosshairInAirFactor; // 空中扩散因子
float CrosshairAimFactor; // 瞄准扩散因子
float CrosshairShootingFactor; // 射击扩散因子
CombatComponent.cpp
void UCombatComponent::SetHUDCrosshairs(float DeltaTime)
{
if(Character == nullptr || Character->Controller) return;
Controller = Controller == nullptr ? Cast<ABlasterPlayerController>(Character->Controller) : Controller;
if(Controller)
{
HUD = HUD == nullptr ? Cast<ABlasterHUD>(Controller->GetHUD()) : HUD;
if(HUD)
{
// 根据是否装备武器改变
if(EquippedWeapon)
{
HUDPackage.CrosshairsCenter = EquippedWeapon->CrosshairsCenter;
HUDPackage.CrosshairsLeft = EquippedWeapon->CrosshairsLeft;
HUDPackage.CrosshairsRight = EquippedWeapon->CrosshairsRight;
HUDPackage.CrosshairsTop = EquippedWeapon->CrosshairsTop;
HUDPackage.CrosshairsBottom = EquippedWeapon->CrosshairsBottom;
}
else
{
HUDPackage.CrosshairsCenter = nullptr;
HUDPackage.CrosshairsLeft = nullptr;
HUDPackage.CrosshairsRight = nullptr;
HUDPackage.CrosshairsTop = nullptr;
HUDPackage.CrosshairsBottom = nullptr;
}
// 水平速度的准心扩散
FVector2D WalkSpeedRange(0.f, Character->GetCharacterMovement()->MaxWalkSpeed);
FVector2D VelocityMultiplierRange(0.f, 1.f);
FVector Velocity = Character->GetVelocity();
Velocity.Z = 0.f;
CrosshairVelocityFactor = FMath::GetMappedRangeValueClamped(WalkSpeedRange, VelocityMultiplierRange, Velocity.Size());
// 垂直速度的准心扩散
if(Character->GetCharacterMovement()->IsFailling())
{
// 上升就逐渐增大扩散因子
CrosshairInAirFactor = FMath::FInterpTo(CrosshairInAirFactor, 2.25f, DeltaTime, 2.25f);
}
else
{
// 下降就逐渐减小扩散因子
CrosshairInAirFactor = FMath::FInterpTo(CrosshairInAirFactor, 0.f, DeltaTime, 30f);
}
if(bAiming)
{
CrosshairAimFactor = FMath::FInterpTo(CrosshairAimFactor, 0.58f, DeltaTime, 30.f);
}
else
{
CrosshairAimFactor = FMath::FInterpTo(CrosshairAimFactor, 0.f, DeltaTime, 30.f);
}
// 射击时才增加,平时都一直减少
CrosshairShootingFactor = FMath::FInterpTo(CrosshairShootingFactor, 0.f, DeltaTime, 40.f);
HUDPackage.CrosshairSpread =
0.5f + // 起始扩散量,避免-AimFactor后过小
CrosshairVelocityFactor +
CrosshairInAirFactor -
CrosshairAimFactor +
CrosshairShootingFactor;
HUD->SetHUDPackage(HUDPackage);
}
}
}
void UCombatComponent::TickComponent(...)
{
...
if(Character && Character->IsLocallyControlled)
{
// 本地控制才需要绘制准心
SetHUDCrosshairs(DeltaTime);
}
}
void UCombatComponent::FireButtonPressed(bool bPressed)
{
...
if(EquippedWeapon)
{
CrosshairShootingFactor = 0.75f; // 射击就固定增加扩散
// CrosshairShootingFactor += 0.2f; // 射击就累加扩散
}
}
void UCombatComponent::TraceUnderCrosshairs(FHitResult& TraceHitResult)
{
...
if(TranceHitResult.GetActor() && TraceHitResult.GetActor()->Implements<InteractWithCrosshairsInterface>()) // 目标实现了准心变化接口
{
HUDPackage.CrosshairsColor = FLinearColor:Red;
}
else
{
HUDPackage.CrosshairsColor = FLinearColor:White;
}
}
准星颜色变化
- 创建接口
InteractWithCrosshairsInterface:Unreal Interface
虚幻接口包含两个类

- 人物阻挡可见性
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block); // 人物可以遮挡准心,记得蓝图中检查
}
靠近墙壁,隐藏人物
BlasterCharacter.h
private:
void HideCameraIfCharacterClose();
UPROPERTY(EditAnywhere)
float CameraThreshold = 200.f;
BlasterCharacter.cpp
void ABlasterCharacter::Tick(float DeltaTime)
{
...
HideCameraIfCharacterClose();
}
void ABlasterCharacter::HideCameraIfCharacterClose()
{
// 只在隐藏本地角色
if(!IsLocallyControlled()) return;
// 如果摄像机和人物离太近了
if((FollowCamera->GetComponentLocation() - GetActorLocation()).Size() < CameraThreshold)
{
GetMesh()->SetVisibility(false);
if(Combat && Combat->EquippedWeapon && Combat->EquippedWeapon->GetWeaponMesh())
{
// 对拥有者不可见
Combat->EquippedWeapon->GetWeaponMesh()->bOwnerNoSee = true;
}
}
else
{
GetMesh()->SetVisibility(true);
if(Combat && Combat->EquippedWeapon && Combat->EquippedWeapon->GetWeaponMesh())
{
// 对拥有者不可见
Combat->EquippedWeapon->GetWeaponMesh()->bOwnerNoSee = false;
}
}
}
受击动画
- 处理受击动画,复制一份并改为
Additive,四种受击动画都作此处理


- 创建
HitReact蒙太奇,把四种受击动画都拖进去,设置Preview Base Pose,根据受击的方向创建Montage Section,




- 创建
HitReactSlot

- 触发蒙太奇
BlasterCharacter.h
private:
UPROPERTY(EditAnywhere, Category=Combat)
UAnimMontage* HitReactMontage;
protected:
void PlayHitReactMontage();
public:
UFUNCTION(NetMulticast, Unreliable)
void Multicast_Hit();
BlasterCharacter.cpp
void ABlasterCharacter::PlayHitReactMontage()
{
UAnimInstance AnimInstance = GetMesh()->GetAnimInstance();
if(AnimInstance && HitReactMontage)
{
AnimInstance->Montage_Play(HitReactMontage);
AnimInstance->Montage_JumpToSection(FName("FromFront"));
}
}
void ABlasterCharacter::Multicast_Hit_Implementation()
{
PlayHitReactMontage();
}
Projectile.cpp
// 只在服务器绑定
void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpluse, const FHitResult& HitResult)
{
ABlasterCharacter Character = Cast<ABlasterCharacter>(OtherActor);
if(Character)
{
Character->Multicast_Hit();
}
Destroy();
}
击中胶囊体不够准确,希望击中骨骼网格
Projectile.cpp
AProjectile::AProjectile()
{
// // ECC_Pawn表示会打中胶囊体
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
}
想要打中人物,而不打中胶囊体,就需要自定义碰撞通道

Blaster.h
#define ECC_SkeletalMesh ECollisionChannel::ECC_GameTraceChannel1
Projectile.cpp
AProjectile::AProjectile()
{
...
// ECollisionChannel::ECC_Pawn, 蓝图中记得取消碰撞
CollisionBox->SetCollisionResponseToChannel(ECC_SkeletalMesh, ECollisionResponse::ECR_Block);
...
}
BlasterCharacter.cpp
ABlasterCharacter::ABlasterCharacter()
{
...
GetMesh()->SetCollisionObjectType(ECC_SkeletalMesh);
}
代理角色旋转有误,颤动
P81
游戏框架




游戏实现二
人物血量
为什么将血量放到Character中,而不是放在Player State中

Player State网络更新稍慢,没有在Character直接处理快,血量往往要求足够实时
- 添加血量
BlasterCharacter.h
protected:
UFUNCTION()
void ReceiveDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, class AController* InstigatorController, AActor* DamageCauser);
void UpdateHUDHealth();
private:
UPROPERTY(EditAnywhere, Category = "Player Stats")
float MaxHealth = 100.f;
UPROPERTY(ReplicatedUsing = OnRep_Health, VisibleAnywhere, Category = "Player Stats")
float Health = 100.f;
UFUNCTION()
void OnRep_Health();
class ABlasterPlayerController* BlasterPlayerController;
BlasterCharacter.cpp
void ABlasterCharacter::BeginPlay()
{
...
UpdateHUDHealth();
if(HasAuthority())
{
OnTakeDamage.AddDynamic(this, &ABlasterCharacter::ReceiveDamage);
}
}
void ABlasterCharacter::OnRep_Health()
{
PlayHitReactMontage();
UpdateHUDHealth();
}
void ABlasterCharacter::UpdateHUDHealth()
{
BlasterPlayerController = BlasterPlayerController == nullptr ? Cast<ABlasterPlayerController>(Controller) : BlasterPlayerController;
if(BlasterPlayerController)
{
BlasterPlayerController->SetHUDHealth(Health, MaxHealth);
}
}
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
...
DOREPLIFETIME(ABlasterCharacter, Health);
}
void ABlasterCharacter::ReceiveDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, class AController* InstigatorController, AActor* DamageCauser)
{
Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
OnRep_Health(); // 更新HUD按理说服务器不需要更新的
if(Health == 0.f)
{
// 触发淘汰
ABlasterGameMode* BlasterGameMode = GetWorld()->GetAuthGameMode<ABlasterGameMode>();
if(BlasterGameMode)
{
BlasterPlayerController = BlasterPlayerController == nullptr ? Cast<ABlasterPlayerController>(Controller) : BlasterPlayerController;
ABlasterPlayerController* AttackerController = Cast<ABlasterPlayerController>(InstigatorController);
// 核心代码
BlaseterGameMode->PlayerElimminated(this, BlasterPlayerController, AttackerController);
}
}
}
- 创建
WBP_CharacterOverlay:Widget Blueprint

- 创建
CharacterOverlay:User Widget,并设置为WBP_CharacterOverlap的父类
CharacterOverlay.h
public
UPROPERTY(meta = (BindWidget))
class UProgressBar* HealthBar;
UPROPERTY(meta = (BindWidget))
class UTextBlock* HealthText;
BlasterHUD.h
public:
UPROPERTY(EditAnywhere, Category = "Player Stats")
TSubclassOf<class UUserWidget> CharacterOverlayClass; // 蓝图中指定为WBP_CharacterOverlay
class UCharacterOverlay* CharacterOverlay;
protected:
virtual void BeginPlay() override;
void AddCharacterOverlay();
BlasterHUD.cpp
void ABlasterHUD::BeginPlay()
{
Super::BeginPlay();
AddCharacterOverlay();
}
void ABlasterHUD::AddCharacterOverlay()
{
APlayerController* PlayerController = GetOwningPlayerController();
if(PlayerController && CharacterOverlayClass)
{
CharacterOverlay = CreateWidget<UCharacterOverlay>(PlayerController, CharacterOverlayClass);
CharacterOverlay->AddToViewport();
}
}
- 更新血量,更新``HUD
,最好通过PlayerController`来控制
BlasterPlayerController.h
public:
void SetHUDHealth(float Health, float MaxHealth);
protected:
virtual void BeginPlay() override
private:
class ABlasterHUD* BlasterHUD;
BlasterPlayerController.cpp
void ABlasterPlayerController::BeginPlay()
{
Super::BeginPlay();
BlasterHUD = Cast<ABlasterHUD>(GetHUD());
}
void ABlasterPlayerController::SetHUDHealth(float Health, float MaxHealth)
{
// 安全检查
BlasterHUD = BlasterHUD == nullptr ? Cast<ABlasterHUD>(GetHUD()) : BlasterHUD;
bool bHUDValid = BlasterHUD &&
BlasterHUD->CharacterOverlay &&
BlasterHUD->CharacterOverlay->HealthBar &&
BlasterHUD->CharacterOverlay->HealthText;
if(bHUDValid)
{
const float HealthPercent = Health / MaxHealth;
BlasterHUD->CharacterOverlay->HealthBar->SetPercent(HealthPercent);
FString HealthText = FString::Printf(TEXT("%d/%d"), FMath::CeilToInt(Health), FMath::CeilToInt(MaxHealth));
BlasterHUD->CharacterOverlay->HealthText->SetText(FText::FromString(HealthText));
}
}
游戏模式
- 删除原始游戏模式,并重新生成项目代码

玩家死亡
- 创建
BlasterGameMode:GameMode
BlasterGameMode.h
public:
// 淘汰玩家
virtual void PlayerEliminated(class ABlasterCharacter* ElimmedCharacter, class ABlasterController* VictimController, ABlasterController* AttackerController);
BlasterGameMode.cpp
void ABlasterGameMode::PlayerEliminated(class ABlasterCharacter* ElimmedCharacter, class ABlasterController* VictimController, ABlasterController* AttackerController)
{
if(ElimmedCharacter)
{
ElimmedCharacter->Elim();
}
}
- 玩家实现自己的淘汰方式
BlasterCharacter.h
private:
bool bElimmed = false;
public:
void Elim();
UFUNCTION(NetMulticast, Reliable)
void Multicast_Elim();
void PlayElimMontage();
FORCEINLINE bool IsElimmed() const { return bElimmed; }
BlasterCharacter.cpp
// 只在服务器调用
void ABlasterCharacter::Elim()
{
// 丢弃武器
if(Combat && Combat->EquippedWeapon)
{
Combat->EquippedWeapn->Dropped();
}
// 玩家死亡
Multicast_Elim();
}
void ABlasterCharacter::Multicast_Elim_Implementation()
{
bElimmed = true;
PlayElimMontage();
// Disable character movement
GetCharacterMovement()->DisableMovement(); // 不能移动,但能原地旋转
GetCharacterMovement()->StopMovementImmediately(); // 什么移动都不可以
if(BlasterPlayerController)
{
DisableInput(BlasterPlayerController); // 禁止输入
}
// Disable collision
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision
// Spawn elim bot
if(ElimBotEffect)
{
if(ElimBotSound)
{
UGameplayStatics::SpawnSoundAtLocation(
this,
ElimBotSound,
GetActorLocation()
);
}
FVector ElimBotSpawnPoint(GetActorLocation.X, GetActorLocation.Y, GetActorLocation.Z + 200.f);
ElimBotComponent = UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ElimBotEffect,
ElimBotSpawnPoint,
GetActorRotation()
);
}
}
void ABlasterCharacter::PlayElimMontage()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if(AnimInstance && ElimMontage)
{
AnimInstance->Montage_Play(ElimMontage);
}
}
- 创建
BP_BlasterGameMode:GameMode,指定父类BlasterGameMode

-
给
BlasterMap->World Settings->BP_BlasterGameMode -
玩家死亡动画
- 复制
EpikCharacter_FlySmooth->命名为ElimAnimation - 调整盆骨
pelvis旋转,朝上

- 添加上升的
Key,并烘焙到一个新的动画Elim中

- 制作蒙太奇
Elim,添加ElimSlot,拖入Elim动画 - 把
ElimSlot添加到动画蓝图中 BlasterAnimInstance中获取数据
BlasterAnimInstance.h
private:
bool IsElimmed;
BlasterAnimInstance.cpp
void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
...
bElimmed = BlasterCharacter->IsElimmed();
}
Bot
- 资源商店添加
Paragon:Dekker,使用其中的P_Bot_Cage特效
BlasterCharacter.h
protected:
virtual void Destroyed() override;
private:
UPROPERTY(EditAnywhere)
UParticleSystem* ElimBotEffect;
UPROPERTY(VisibleAnywhere)
UParticleSystemComponent* ElimBotComponent;
UPROPERTY(EditAnywhere)
USoundCue* ElimBotSound;
BlasterCharacter.cpp
void ABlasterCharcter::Destroyed()
{
Super::Destroyed();
if(ElimBotComponent)
{
ElimBotComponent->DestroyComponent();
}
}
人物溶解
- 创建材质
M_DissolveEffect,随便找一个Noise纹理

- 接着创建材质实例

-
Dissolve:溶解程度 -
Glow:发光亮度
P91-P93
丢弃武器
Weapon.h
public:
void Dropped();
Weapon.cpp
void OnRep_WeaponState()
{
// 根据当前的武器状态,调整检测和提示显示
switch(WeaponState)
{
case EWeaponState::EWS_Equipped:
ShowPickupWidget(false);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
WeaponMesh->SetSimulatePhysics(false);
WeaponMesh->SetEnableGravity(false);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
break;
case EWeaponState::EWS_Dropped:
ShowPickupWidget(true);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
WeaponMesh->SetSimulatePhysics(true);
WeaponMesh->SetEnableGravity(true);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
break;
}
}
void AWeapon::SetWeaponState(EWeaponState State)
{
WeaponState = State;
// 这个函数只在服务器上调用,需要手动调用`OnRep`
OnRep_WeaponState();
}
void AWeapon::Dropped()
{
SetWeaponState(EWeaponState::EWS_Dropped);
FDetachmentTransformRules DetachRules(EDetachmentRule::KeepWorld, true);
WeaponMesh->DetachFromComponent(DetachRules);
SetOwner(nullptr);
}
BlasterCharacter.cpp
void ABlasterCharacter::Elim()
{
}
玩家重生
BlasterCharacter.h
private:
FTimerHandler ElimHandler;
float ElimDelay = 3.f;
void ElimTimerFinished();
BlasterCharacter.cpp
// 在GameMode中调用,即只在服务器执行
void ABlasterCharacter::Elim()
{
Multicast_Elim();
GetWorldTimerManager().SetTimer(
ElimTimer,
this,
&ABlasterCharacter::ElimTimerFinished,
ElimDelay
); // 只需要执行一次
}
void ABlasterCharacter::ElimTimerFinished()
{
ABlasterGameMode* BlasterGameMode = GetWorld()->GetAuthGameMode<ABlasterGameMode>();
if(BlasterGameMode)
{
BlasterGameMode->RequestRespawn(this, Controller);
}
}
BlasterGameMode.h
public:
virtual void RequestRespawn(ACharacter* ElimmedCharacter, AController* ElimmedController);
BlasterGameMode.cpp
void ABlasterGameMode::RequestRespawn(ACharacter* ElimmedCharacter, AController* ElimmedController)
{
if(ElimmedCharacter)
{
ElimmedCharacter->Reset(); // 释放控制器等,看源码
ElimmedCharacter->Destroy();
}
if(ElimmedController)
{
// 随机选一个PlayerStart重生
TArray<AActor*> PlayerStarts;
UGameplayStatics::GetAllActorsOfClass(this, APlayerStart::StaticClass(), PlayerStates);
int32 Selection = FMath::RandRange(0, PlayerStarts.Num()-1);
RestartPlayerAtPlayerStart(ElimmedController, PlayerStarts[Selection]);
}
}
bug: 有时不会重生,报错说spawn at location xx error
这是因为该出生点有人物了,发生碰撞,所以没有生成,调整BP_BlasterCharacter的生成规则

或者在代码中调整

重生后重置人物属性(OnPossess)
ABlasterPlayerController.h
public:
virtual void OnPossess(APawn* InPawn) override;
ABlasterPlayerController.cpp
void ABlasterPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// OnPosses 和 Character->BeginPlay 都需要设置
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(InPawn);
if(BlasterCharacter)
{
SetHUDHealth(BlasterCharacter->GetHealth(), BlasterCharacter->GetMaxHealth());
}
}
玩家状态
即使玩家死亡,也会依然存在,内置了一些属性
玩家分数
- 创建
BlasterPlayerState:PlayerState,内置了Score,并实现了Replicated
BlasterPlayerState.h
public:
UFCTION(Server, Reliable)
void AddToScore(float ScoreAmount);
virtual void OnRep_Score() override;
private:
class ABlasterCharacter Character;
class ABlasterPlayerController Controller;
BlasterPlayerState.cpp
void BlasterPlayerState::AddToScore(float ScoreAmount)
{
// 在服务器上更改分数
SetScore(Score + ScoreAmount);
OnRep_Score();
}
// 同步到客户端,同时更新HUD
void BlasterPlayerState::OnRep_Score()
{
Super::OnRep_Score();
Character = Character == nullptr ? Cast<ABlasterCharacter>(GetPawn()) : Character;
if(Character)
{
Controller = Controller == nullptr ? Cast<ABlasterPlayerController>(Character->Controller) : Controller;
if(Controller)
{
Controller->SetHUDScore(Score);
}
}
}
HUD-WBP_CharacterOveraly添加Score,并绑定控件

CharacterOverlay.h
public:
UPROPERTY(meta = (BindWidget))
UTextBlock* ScoreAmount;
- 通过
PlayerController控制HUD,并更新UI
BlasterPlayerController.h
public:
void SetHUDScore(float Score);
BlasterPlayerController.cpp
void BlasterPlayerController::SetHUDScore(float Score)
{
BlasterHUD = BlasterHUD == nullptr ? Cast<ABlasterHUD>(GetHUD()) : BlasterHUD;
bool bHUDValid = BlaterHUD &&
BlasterHUD->CharacterOverlay &&
BlasterHUD->CharacterOverlay->ScoreAmount;
if(bHUDValid)
{
FString ScoreText = FString::Printf(TEXT("%d"), FMath::FloorToInt(Score));
BlasterHUD->CharacterOverlay->ScoreAmount->SetText(FText::FromString(ScoreText));
}
}
- 击杀得分
BlasetrGameMode.cpp
void ABlasterGameMode::PlayerEliminated(class ABlasterCharacter* ElimmedCharacter, class ABlasterPlayerController* VictimController,...)
{
ABlasterPlayerState* AttackerPlayerState = AttackerController ? Cast<ABlasterPlayerState>(AttackerController->PlayerState) : nullptr;
ABlasterPlayerState* VictimPlayerState = VictimController ? Cast<ABlasterPlayerState>(VictimController->PlayerState) : nullptr;
if(AttackerPlayerState && AttackerPlayerState != VictimPlayerState)
{
AttackerPlayerState->AddToScore(1.0f);
}
...
}
- 接着把
PlayerState和角色绑定到一起,创建BP_BlasterPlayerState:BlasterPlayerState - 将
BP_BlasterGameMode->Player State Class->BP_BlasterPlayerState
初始化分数
BlasterCharacter.h
public:
class ABlasterPlayerState* BlasterPlayerState;
void PollInit();
BlasterCharacter.cpp
void ABlasterCharacter::PollInit()
{
if(BlasterPlayerState == nullptr)
{
BlasterPlayerState = GetPlayerState<ABlasterPlayerState>();
if(BlasterPlayerState)
{
BlasterPlayerState->AddToScore(0.f);
}
}
}
void ABlasterCharacter::Tick(float DeltaTime)
{
...
PollInit(); // 每帧检查
}
失败数
- 修改
WBP_CharacterOverlay,添加Defeats

CharacterOverlay中控件绑定
CharacterOverlay.h
public:
UPROPERTY(meta = (BindWidget))
UTextBlock* DefeatsAmount;
BlasterPlayerController更新头显
BlasterPlayerController.h
public:
void SetHUDDefeats(int32 Defeats);
BlasterPlayerController.cpp
void BlasterPlayerController::SetHUDDefeats(int32 Defeats)
{
BlasterHUD = BlasterHUD == nullptr ? Cast<ABlasterHUD>(GetHUD()) : BlasterHUD;
bool bHUDValid = BlaterHUD &&
BlasterHUD->CharacterOverlay &&
BlasterHUD->CharacterOverlay->DefeatsAmount;
if(bHUDValid)
{
FString DefeatsText = FString::Printf(TEXT("%d"), Defeats);
BlasterHUD->CharacterOverlay->DefeatsAmount->SetText(FText::FromString(DefeatsText));
}
}
- 添加玩家状态
BlasterPlayerState.h
private:
void AddToDefeats(int32 DefeatsAmount);
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UFUNCTION()
virtual void OnRep_Defeats() override;
UPROPERTY(ReplicatedUsing = OnRep_Defeats)
int32 Defeats;
public:
void SetDefeats(int32 NewDefeats) { Defeats = NewDefeats; }
BlasterPlayerState.cpp
void ABlasterPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ABlasterPlayerState, Defeats)
}
void ABlasterPlayerState::AddToDefeats(int32 DefeatsAmount)
{
SetDefeats(DefeatsAmount);
OnRep_Defeats();
}
void ABlasterPlayerState::OnRep_Defeats()
{
Super::OnRep_Defeats();
Character = Character == nullptr ? Cast<ABlasterCharacter>(GetPawn()) : Character;
if(Character)
{
Controller = Controller == nullptr ? Cast<ABlasterPlayerController>(Character->Controller) : Controller;
if(Controller)
{
Controller->SetHUDDefeats(Defeats);
}
}
}
- 在玩家阵亡中,添加失败数逻辑
BlasterGameMode.cpp
void ABlasterGameMode::PlayerEliminated(...)
{
...
if(VictimPlayerState)
{
VictimPlayerState->AddToDefeats(1);
}
}
PollInit中初始化
BlasterCharacter.cpp
void ABlasterCharacter::PollInit()
{
...
BlasterPlayerState->AddToDefeats(0);
}
bug: 所有指针类型,需要初始化为nullptr
if(Character),判断的可能是NULL,初始化为0,并不一定等于nullptr
所以要么初始化PlayerCharacter* Character = nullptr;
要么加上注解UPROPERTY,也会初始化为nullptr
浙公网安备 33010602011771号