diff --git a/Docs/Multiplayer.md b/Docs/Multiplayer.md index ef82976..72d8580 100644 --- a/Docs/Multiplayer.md +++ b/Docs/Multiplayer.md @@ -1,16 +1,9 @@ # Multiplayer, Steam & Server Admin -## In-Game Menu (`M`) +## Start Menu -Opens automatically on start and again with **M**: - -- **Map** selection -- **Backend**: Auto / Steam / LAN -- **Host Port** + Max Players -- **Find Steam** / **Find LAN** -- **Join selected session** -- **Connect by IP:Port** -- **Play Solo** +Map / Steam / LAN / IP:Port selection appears **only at game start**. +It is not bound to a key in-game (`M` is reserved for the world map). ## Steam diff --git a/README.md b/README.md index 505e33f..29554bb 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ A life-simulation action RPG remake inspired by Fantasy Life, built in **Unreal | Interact / Craft | E | X / Square | | Attack | LMB | RT | | Toggle camera (Top-Down / FP) | C | Menu | -| Multiplayer menu | M | — | | Pause | Esc | Start | ### Pause menu shortcuts diff --git a/Source/VocationLife/Private/VocationGameInstance.cpp b/Source/VocationLife/Private/VocationGameInstance.cpp index ec0fbb6..3962fc1 100644 --- a/Source/VocationLife/Private/VocationGameInstance.cpp +++ b/Source/VocationLife/Private/VocationGameInstance.cpp @@ -21,6 +21,11 @@ void UVocationGameInstance::Init() OnDataReady.Broadcast(); } +void UVocationGameInstance::MarkGameplayStarted() +{ + bHasEnteredGameplay = true; +} + void UVocationGameInstance::InitializeDefaultData() { ItemDefinitions.Empty(); diff --git a/Source/VocationLife/Private/VocationHUD.cpp b/Source/VocationLife/Private/VocationHUD.cpp index 56fcb68..f5fe288 100644 --- a/Source/VocationLife/Private/VocationHUD.cpp +++ b/Source/VocationLife/Private/VocationHUD.cpp @@ -76,7 +76,7 @@ void AVocationHUD::DrawMainHUD() } else { - const FString Controls = TEXT("WASD Move | C Camera | M Multiplayer | E Interact | LMB Attack | I Save | Esc Pause"); + const FString Controls = TEXT("WASD Move | C Camera | E Interact | LMB Attack | I Save | Esc Pause"); Canvas->DrawColor = FColor(200, 200, 200); Canvas->DrawText(GEngine->GetSmallFont(), Controls, 40.f * Scale, Canvas->ClipY - 60.f * Scale, Scale, Scale); } diff --git a/Source/VocationLife/Private/VocationMainMenuWidget.cpp b/Source/VocationLife/Private/VocationMainMenuWidget.cpp index 3184551..44f78a8 100644 --- a/Source/VocationLife/Private/VocationMainMenuWidget.cpp +++ b/Source/VocationLife/Private/VocationMainMenuWidget.cpp @@ -2,6 +2,7 @@ #include "VocationMainMenuWidget.h" #include "VocationSessionSubsystem.h" +#include "VocationGameInstance.h" #include "Components/Button.h" #include "Components/ComboBoxString.h" #include "Components/EditableTextBox.h" @@ -72,7 +73,7 @@ void UVocationMainMenuWidget::BuildUI() } }; - AddToColumn(MakeLabel(WidgetTree, TEXT("Title"), TEXT("VocationLife Multiplayer"))); + AddToColumn(MakeLabel(WidgetTree, TEXT("Title"), TEXT("VocationLife — Start Menu"))); AddToColumn(MakeLabel(WidgetTree, TEXT("MapLabel"), TEXT("Map"))); MapCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("MapCombo")); @@ -182,6 +183,22 @@ void UVocationMainMenuWidget::SetStatus(const FString& Message) } } +void UVocationMainMenuWidget::LeaveStartMenu() +{ + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->MarkGameplayStarted(); + } + + if (APlayerController* PC = GetOwningPlayer()) + { + PC->bShowMouseCursor = false; + PC->SetInputMode(FInputModeGameOnly()); + } + + RemoveFromParent(); +} + void UVocationMainMenuWidget::OnHostClicked() { UVocationSessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr; @@ -202,8 +219,8 @@ void UVocationMainMenuWidget::OnHostClicked() if (BackendIndex == 2) Backend = EVocationNetBackend::LAN; SetStatus(TEXT("Hosting...")); + LeaveStartMenu(); Sessions->HostSession(MaxPlayers, Name, MapPath, Port, Backend, Backend == EVocationNetBackend::LAN); - RemoveFromParent(); } void UVocationMainMenuWidget::OnFindSteamClicked() @@ -235,6 +252,7 @@ void UVocationMainMenuWidget::OnJoinSelectedClicked() return; } SetStatus(TEXT("Joining session...")); + LeaveStartMenu(); Sessions->JoinSessionByIndex(Index); } } @@ -246,8 +264,8 @@ void UVocationMainMenuWidget::OnConnectIpClicked() const FString IP = IpBox ? IpBox->GetText().ToString() : TEXT("127.0.0.1"); const int32 Port = JoinPortBox ? FCString::Atoi(*JoinPortBox->GetText().ToString()) : 7777; SetStatus(FString::Printf(TEXT("Connecting to %s:%d ..."), *IP, Port)); + LeaveStartMenu(); Sessions->ConnectByIP(IP, Port); - RemoveFromParent(); } } @@ -257,8 +275,8 @@ void UVocationMainMenuWidget::OnSoloClicked() { const int32 MapIndex = MapCombo ? MapCombo->GetSelectedIndex() : 0; const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry"); + LeaveStartMenu(); Sessions->TravelToMap(MapPath, false, 7777); - RemoveFromParent(); } } @@ -295,6 +313,6 @@ void UVocationMainMenuWidget::OnSessionJoined(bool bSuccess) SetStatus(bSuccess ? TEXT("Join/host ok") : TEXT("Join/host failed")); if (bSuccess) { - RemoveFromParent(); + LeaveStartMenu(); } } diff --git a/Source/VocationLife/Private/VocationPlayerController.cpp b/Source/VocationLife/Private/VocationPlayerController.cpp index ac23f91..bf67fee 100644 --- a/Source/VocationLife/Private/VocationPlayerController.cpp +++ b/Source/VocationLife/Private/VocationPlayerController.cpp @@ -44,10 +44,10 @@ void AVocationPlayerController::BeginPlay() GI->SetGraphicsPreset(GI->GetGraphicsPreset(), GI->IsRayTracingEnabled()); } - // Show multiplayer menu on first load (local player only). + // Start menu only once per game session — not reopenable in-game (M reserved for map). if (IsLocalController() && !IsRunningDedicatedServer()) { - ToggleMultiplayerMenu(); + ShowStartMenuIfNeeded(); } } @@ -75,7 +75,6 @@ void AVocationPlayerController::SetupInputComponent() InputComponent->BindKey(EKeys::F3, IE_Pressed, this, &AVocationPlayerController::ApplyHighGraphics).bConsumeInput = false; InputComponent->BindKey(EKeys::F4, IE_Pressed, this, &AVocationPlayerController::ApplyUltraGraphics).bConsumeInput = false; InputComponent->BindKey(EKeys::F5, IE_Pressed, this, &AVocationPlayerController::ApplyRayTracingGraphics).bConsumeInput = false; - InputComponent->BindKey(EKeys::M, IE_Pressed, this, &AVocationPlayerController::ToggleMultiplayerMenu).bConsumeInput = false; InputComponent->BindKey(EKeys::H, IE_Pressed, this, &AVocationPlayerController::HostCoopGame).bConsumeInput = false; InputComponent->BindKey(EKeys::J, IE_Pressed, this, &AVocationPlayerController::JoinFirstFoundSession).bConsumeInput = false; InputComponent->BindKey(EKeys::S, IE_Pressed, this, &AVocationPlayerController::QuickSave).bConsumeInput = false; @@ -118,13 +117,16 @@ void AVocationPlayerController::ClearInteractionPrompt() } } -void AVocationPlayerController::ToggleMultiplayerMenu() +void AVocationPlayerController::ShowStartMenuIfNeeded() { + UVocationGameInstance* GI = GetGameInstance(); + if (!GI || GI->HasGameplayStarted()) + { + return; + } + if (MultiplayerMenu && MultiplayerMenu->IsInViewport()) { - MultiplayerMenu->RemoveFromParent(); - bShowMouseCursor = false; - SetInputMode(FInputModeGameOnly()); return; } @@ -133,9 +135,8 @@ void AVocationPlayerController::ToggleMultiplayerMenu() { MultiplayerMenu->AddToViewport(100); bShowMouseCursor = true; - FInputModeGameAndUI Mode; + FInputModeUIOnly Mode; Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); - Mode.SetHideCursorDuringCapture(false); SetInputMode(Mode); } } @@ -144,6 +145,10 @@ void AVocationPlayerController::HostCoopGame() { if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem()) { + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->MarkGameplayStarted(); + } Sessions->HostSession(4, TEXT("VocationLife"), TEXT("/Engine/Maps/Entry"), 7777, EVocationNetBackend::Auto, false); } } diff --git a/Source/VocationLife/Public/VocationGameInstance.h b/Source/VocationLife/Public/VocationGameInstance.h index def188b..7d0052c 100644 --- a/Source/VocationLife/Public/VocationGameInstance.h +++ b/Source/VocationLife/Public/VocationGameInstance.h @@ -48,6 +48,13 @@ public: UFUNCTION(BlueprintCallable, Category = "VocationLife|Graphics") bool IsRayTracingEnabled() const { return bRayTracingEnabled; } + /** Once true, the start/multiplayer menu will not open again this session. */ + UFUNCTION(BlueprintCallable, Category = "VocationLife|UI") + void MarkGameplayStarted(); + + UFUNCTION(BlueprintCallable, Category = "VocationLife|UI") + bool HasGameplayStarted() const { return bHasEnteredGameplay; } + UPROPERTY(BlueprintAssignable, Category = "VocationLife") FOnVocationDataReady OnDataReady; @@ -68,4 +75,7 @@ protected: UPROPERTY() bool bRayTracingEnabled = false; + + UPROPERTY() + bool bHasEnteredGameplay = false; }; diff --git a/Source/VocationLife/Public/VocationMainMenuWidget.h b/Source/VocationLife/Public/VocationMainMenuWidget.h index 51a7c0b..9b1af31 100644 --- a/Source/VocationLife/Public/VocationMainMenuWidget.h +++ b/Source/VocationLife/Public/VocationMainMenuWidget.h @@ -55,6 +55,8 @@ protected: UFUNCTION() void OnSessionJoined(bool bSuccess); + void LeaveStartMenu(); + UPROPERTY() TObjectPtr MapCombo; diff --git a/Source/VocationLife/Public/VocationPlayerController.h b/Source/VocationLife/Public/VocationPlayerController.h index 5dd2e48..d59782a 100644 --- a/Source/VocationLife/Public/VocationPlayerController.h +++ b/Source/VocationLife/Public/VocationPlayerController.h @@ -33,7 +33,7 @@ public: void ClearInteractionPrompt(); UFUNCTION(BlueprintCallable, Category = "VocationLife|UI") - void ToggleMultiplayerMenu(); + void ShowStartMenuIfNeeded(); UFUNCTION(BlueprintCallable, Category = "VocationLife|Session") void HostCoopGame();