UE4本地多人分屏玩不爽?手把手教你用C++自定义每个玩家的屏幕区域(附源码)
UE4分屏革命:用C++打造完全自定义的多人视口布局
当四个玩家挤在沙发上争夺同一个屏幕时,为什么总要忍受千篇一律的均等分屏?在《胡闹厨房》的混乱厨房里,主厨可能需要更大的视野;在《FIFA》的球场上,观众席或许只需一个小窗。本文将带你突破UE4默认分屏限制,实现真正的视口自由。
1. 理解UE4分屏系统的底层逻辑
引擎默认的分屏实现藏在 GameViewportClient.cpp 这个关键文件中。打开源码,你会发现一个名为 FPerPlayerSplitscreenData 的结构体,它就像分屏系统的DNA:
struct FPerPlayerSplitscreenData {
float SizeX; // 视口宽度比例 (0.0-1.0)
float SizeY; // 视口高度比例
float OriginX; // 视口左下角X坐标
float OriginY; // 视口左下角Y坐标
};
默认配置中,UE4通过 ESplitScreenType 枚举预设了几种固定布局。比如四人游戏时, FourPlayer_Grid 会将屏幕均分为四等份。这种"一刀切"的设计虽然简单,却严重限制了创意表达。
关键发现 :通过修改 SplitscreenInfo 数组中的数据,我们可以实时改变每个玩家的视口参数。但直接修改这些值存在风险:
- 可能破坏视口间的相对关系
- 未考虑UI适配问题
- 缺乏对动态分辨率变化的响应
2. 构建安全的视口控制系统
我们需要创建一个更健壮的解决方案。首先定义蓝图友好的数据结构:
USTRUCT(BlueprintType)
struct FCustomViewportConfig {
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category="Viewport")
float RelativeWidth = 1.0f;
UPROPERTY(EditAnywhere, Category="Viewport")
float RelativeHeight = 1.0f;
UPROPERTY(EditAnywhere, Category="Viewport")
FVector2D AnchorPoint = FVector2D(0,0);
// 包含边界检查的setter方法
void SetDimensions(float Width, float Height) {
RelativeWidth = FMath::Clamp(Width, 0.1f, 1.0f);
RelativeHeight = FMath::Clamp(Height, 0.1f, 1.0f);
}
};
然后在GameMode中实现核心控制逻辑:
void AMyGameMode::ApplyViewportConfig(const TArray<FCustomViewportConfig>& Configs) {
if (!GEngine || !GEngine->GameViewport) return;
ESplitScreenType::Type CurrentLayout = GetCurrentSplitType();
// 验证配置有效性
if (Configs.Num() != GetNumPlayers() ||
Configs.Num() > MAX_SUPPORTED_PLAYERS) {
UE_LOG(LogTemp, Warning, TEXT("Invalid viewport config count!"));
return;
}
for (int32 i = 0; i < Configs.Num(); ++i) {
FPerPlayerSplitscreenData& PlayerData =
GEngine->GameViewport->SplitscreenInfo[CurrentLayout].PlayerData[i];
PlayerData.SizeX = Configs[i].RelativeWidth;
PlayerData.SizeY = Configs[i].RelativeHeight;
PlayerData.OriginX = Configs[i].AnchorPoint.X;
PlayerData.OriginY = Configs[i].AnchorPoint.Y;
}
// 强制刷新视口
GEngine->GameViewport->LayoutPlayers();
}
重要提示:修改视口参数后必须调用LayoutPlayers(),否则更改不会立即生效
3. 高级应用:动态视口与特效结合
真正的魔法始于将视口控制与其他系统结合。以下是三个实战案例:
案例一:焦点玩家放大效果
// 在Tick中动态调整视口
void AMyGameMode::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
if (bIsFocusModeActive) {
TArray<FCustomViewportConfig> NewConfigs;
// 主玩家获得75%宽度
NewConfigs.Add(FCustomViewportConfig{0.75f, 1.0f, FVector2D(0,0)});
// 其他玩家共享剩余空间
for (int i = 1; i < GetNumPlayers(); ++i) {
float Width = 0.25f / (GetNumPlayers() - 1);
NewConfigs.Add(FCustomViewportConfig{
Width, 1.0f,
FVector2D(0.75f + (i-1)*Width, 0)
});
}
ApplyViewportConfig(NewConfigs);
}
}
案例二:画中画观察模式
通过嵌套视口实现监控摄像头效果:
- 为主玩家创建全屏视口
- 在角落保留小窗口显示观察目标
- 使用RenderTexture实现画中画渲染
案例三:非矩形视口
虽然UE4原生不支持,但可以通过遮罩材质模拟特殊形状:
// 在PlayerController中
void AMyPlayerController::SetupCustomViewport() {
if (GetLocalPlayer()) {
ULocalPlayer* LocalPlayer = GetLocalPlayer();
// 创建圆形遮罩材质实例
UMaterialInstanceDynamic* MaskMaterial = UMaterialInstanceDynamic::Create(
CircleMaskMaterial, this);
// 应用到玩家视口
LocalPlayer->ViewportClient->SetViewportOverlayMaterial(
LocalPlayer->ViewportIndex, MaskMaterial);
}
}
4. 避坑指南:分屏开发的七个致命陷阱
-
UI适配问题
使用GetViewportSize和GetViewportScale时,必须考虑分屏后的实际显示区域:FVector2D AMyHUD::GetAdjustedPosition(const FVector2D& ScreenPosition) { FVector2D ViewportSize; GEngine->GameViewport->GetViewportSize(ViewportSize); float ViewportScale = GetLocalPlayer()->ViewportClient->GetDPIScale(); // 转换为当前视口空间坐标 return (ScreenPosition - ViewportOrigin) * ViewportScale; } -
摄像机比例失调
修改视口大小时,必须同步调整摄像机宽高比:视口比例 推荐摄像机设置 宽屏(16:9) FOV 90°, AspectRatio 1.777 方屏(1:1) FOV 75°, AspectRatio 1.0 竖屏(9:16) FOV 60°, AspectRatio 0.5625 -
输入映射混乱
每个玩家的输入必须限定在其视口区域:bool AMyPlayerController::IsInputInViewport(const FVector2D& ScreenPos) { FVector2D ViewportOrigin, ViewportSize; GetViewportDimensions(ViewportOrigin, ViewportSize); return ScreenPos.X >= ViewportOrigin.X && ScreenPos.Y >= ViewportOrigin.Y && ScreenPos.X <= (ViewportOrigin.X + ViewportSize.X) && ScreenPos.Y <= (ViewportOrigin.Y + ViewportSize.Y); } -
性能优化策略
非对称分屏时,不同视口可采用不同渲染质量:- 小视口:降低分辨率比例(0.7-0.8)
- 大视口:保持原生分辨率
- 不可见视口:暂停渲染
-
动态布局切换
使用状态模式管理不同布局配置:UENUM(BlueprintType) enum class EViewportLayout : uint8 { DefaultSplit, FocusMode, PictureInPicture, Custom }; void AMyGameMode::SetActiveLayout(EViewportLayout NewLayout) { CurrentLayout = NewLayout; switch (NewLayout) { case EViewportLayout::FocusMode: ApplyFocusModeConfig(); break; // 其他布局处理... } } -
多显示器支持
通过识别DisplayID将不同玩家分配到不同物理屏幕:void AMyGameMode::AssignToDisplay(int32 PlayerIndex, int32 DisplayID) { FDisplayMetrics Metrics; FDisplayMetrics::GetDisplayMetrics(Metrics); if (DisplayID < Metrics.MonitorInfo.Num()) { const FMonitorInfo& Info = Metrics.MonitorInfo[DisplayID]; // 设置视口为显示器物理坐标... } } -
回退机制
任何自定义分屏系统都应保留恢复默认的路径:void AMyGameMode::ResetToDefaultSplit() { if (GEngine && GEngine->GameViewport) { GEngine->GameViewport->SetForceDisableSplitscreen(false); GEngine->GameViewport->LayoutPlayers(); } }
5. 源码剖析:视口布局的刷新机制
理解 UGameViewportClient 的工作流程至关重要。当调用 LayoutPlayers() 时,引擎内部会:
- 清除现有视口
- 根据当前
SplitscreenInfo重新计算布局 - 为每个玩家创建新的
FSceneView - 触发
OnViewportCreated事件
关键覆盖点 :你可以继承 UGameViewportClient 来修改这一过程:
void UMyGameViewportClient::LayoutPlayers() {
if (bUseCustomLayout) {
// 完全自定义布局逻辑
CalculateCustomViewports();
} else {
// 回退到原生实现
Super::LayoutPlayers();
}
// 通知所有监听者
OnViewportLayoutChanged.Broadcast();
}
对于需要每帧调整的高级应用,可以重写 Tick() 函数:
void UMyGameViewportClient::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
if (bDynamicViewports) {
UpdateDynamicViewports(DeltaTime);
}
}
6. 蓝图集成:设计师友好的控制接口
为了让关卡设计师能自由实验不同布局,我们暴露以下蓝图函数:
Set Player Viewport Size- 调整单个玩家视口尺寸Swap Viewport Positions- 动态交换两个玩家视口位置Enable Focus Mode- 激活主玩家放大模式Save Layout Preset- 将当前布局保存为可复用的资产
示例蓝图实现 :
通过 GameplayTag 系统,可以实现条件触发的布局切换:
void AMyGameMode::OnGameplayTagAdded(FGameplayTag Tag) {
if (Tag.MatchesTag(FGameplayTag::RequestGameplayTag("Viewport.Layout.Focus"))) {
ActivateFocusMode();
}
}
7. 性能监控与调试技巧
自定义分屏系统需要特别的调试手段:
视口调试命令 :
| 命令 | 功能 |
|---|---|
ShowDebug Viewports |
显示视口边界和玩家索引 |
DumpViewportInfo |
输出当前布局参数到日志 |
TestViewportResize |
触发动态尺寸测试 |
统计信息监控 :
// 在HUD上显示关键指标
void AMyHUD::DrawStats() {
if (GEngine && GEngine->GameViewport) {
for (int32 i = 0; i < GEngine->GameViewport->SplitscreenInfo.Num(); ++i) {
const FPerPlayerSplitscreenData& Data =
GEngine->GameViewport->SplitscreenInfo[i].PlayerData[0];
FString Info = FString::Printf(TEXT("P%d: %.1f x %.1f @ (%.1f,%.1f)"),
i, Data.SizeX, Data.SizeY, Data.OriginX, Data.OriginY);
DrawText(Info, FVector2D(10, 100 + i*20), GEngine->GetSmallFont());
}
}
}
GPU负载均衡技巧 :
- 将不同视口的渲染分散到不同帧
- 根据视口大小动态调整渲染分辨率
- 对不可见区域使用简化的渲染路径
// 在自定义ViewportClient中
void UMyGameViewportClient::BeginRenderViewport() {
if (ShouldSkipRenderThisFrame()) {
return; // 跳过小视口的偶帧渲染
}
Super::BeginRenderViewport();
}
更多推荐


所有评论(0)