Compare commits
20 Commits
721c9bb5c3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eca4fc94b | |||
| 4a98c6402b | |||
| f6efc00c73 | |||
| 34280de44a | |||
| ac91e4e8f9 | |||
| c7ec3e9653 | |||
| d9ed016297 | |||
| 67ac3ffab7 | |||
| 8f7476d2db | |||
| b9ec67f9ff | |||
| 2a53d12e68 | |||
| 49cb480e15 | |||
| 7f2600f0b8 | |||
| c520f3ccd9 | |||
| 2f8633d23b | |||
| 98de4dff6f | |||
| b51f063d52 | |||
| 93b9c7d927 | |||
| 050ea4eb2f | |||
| 9877c1b6de |
@@ -1,4 +1,4 @@
|
|||||||
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.5, Phases 1-4 done, outline material pending
|
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.7 (migrated from 5.5 in April 2026), Phases 1-4 done, outline material pending
|
||||||
- [Underscore naming](feedback_naming.md) — Always use PS_Editor_ prefix with underscores in file/class names
|
- [Underscore naming](feedback_naming.md) — Always use PS_Editor_ prefix with underscores in file/class names
|
||||||
- [UE5 build process](reference_build.md) — CLI build command for the project (Build.bat path, target name, flags)
|
- [UE5 build process](reference_build.md) — CLI build command for PROSERVE_UE_5_7 (5.5 host deprecated), Build.bat path, target name, junction check
|
||||||
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed
|
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: PS_Editor plugin project
|
name: PS_Editor plugin project
|
||||||
description: Runtime editor plugin for UE 5.5 - object placement, scene editing, JSON save/load in packaged builds
|
description: Runtime editor plugin for UE 5.7 - object placement, scene editing, JSON save/load in packaged builds
|
||||||
type: project
|
type: project
|
||||||
---
|
---
|
||||||
|
|
||||||
PS_Editor is a runtime editor plugin for Unreal Engine 5.5 for the PROSERVE project.
|
PS_Editor is a runtime editor plugin for Unreal Engine 5.7 for the PROSERVE project. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026 and the plugin follows. Build details in reference_build.md.
|
||||||
|
|
||||||
Key decisions:
|
Key decisions:
|
||||||
- UI: UMG driven by C++ (Slate)
|
- UI: UMG driven by C++ (Slate)
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
---
|
---
|
||||||
name: UE5 build process
|
name: UE5 build process
|
||||||
description: How to build the PS_ProserveEditor UE 5.5 project from command line on Windows
|
description: How to build the PROSERVE host project (currently UE 5.7) from command line on Windows
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
UE 5.5 is installed at `C:\Program Files\Epic Games\UE_5.5` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5`).
|
The PS_Editor plugin is developed in `E:\ASTERION\GIT\PS_Editor\Unreal\Plugins\PS_Editor` and consumed by the PROSERVE host project via an NTFS junction. Always build the host project, not the plugin in isolation.
|
||||||
|
|
||||||
|
**Current host project (April 2026):** `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` (Unreal Engine 5.7)
|
||||||
|
|
||||||
|
The previous 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not use unless explicitly asked.
|
||||||
|
|
||||||
|
UE 5.7 is installed at `C:\Program Files\Epic Games\UE_5.7` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7`).
|
||||||
|
|
||||||
Build command (Editor target, Development):
|
Build command (Editor target, Development):
|
||||||
```
|
```
|
||||||
"C:/Program Files/Epic Games/UE_5.5/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="C:/ASTERION/GIT/PS_ProserveEditor/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.uproject" -WaitMutex -FromMsBuild
|
||||||
```
|
```
|
||||||
|
|
||||||
**How to apply:** Use this command to compile after code changes. Timeout should be set to 600000ms (10 min) for safety. The target name is `PS_ProserveEditorEditor` (Editor suffix) for editor builds.
|
**How to apply:** Use this command to compile after code changes. Timeout should be set to 600000ms (10 min) for safety. The target name is `PROSERVE_UE_5_7Editor` (Editor suffix) for editor builds.
|
||||||
|
|
||||||
**When to use CLI vs Live Coding:**
|
**When to use CLI vs Live Coding:**
|
||||||
- Live Coding (Ctrl+Alt+F11): .cpp only changes (no new UCLASS, USTRUCT, UENUM, UPROPERTY in headers)
|
- Live Coding (Ctrl+Alt+F11): .cpp only changes (no new UCLASS, USTRUCT, UENUM, UPROPERTY in headers)
|
||||||
- Full CLI build (UE must be closed): new files, header changes with new reflected types, Build.cs changes
|
- Full CLI build (UE must be closed): new files, header changes with new reflected types, Build.cs changes
|
||||||
|
|
||||||
|
**Plugin junction verification:**
|
||||||
|
```
|
||||||
|
ls -la E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/Plugins/ | grep PS_Editor
|
||||||
|
```
|
||||||
|
Should show a symlink pointing to `/e/ASTERION/GIT/PS_Editor/Unreal/Plugins/PS_Editor`. If broken, the host project will build against a stale or missing plugin copy.
|
||||||
|
|||||||
48
CLAUDE.md
48
CLAUDE.md
@@ -1,7 +1,7 @@
|
|||||||
# PS_ProserveEditor - Project Guidelines
|
# PS_ProserveEditor - Project Guidelines
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
Unreal Engine 5.5 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load.
|
Unreal Engine 5.7 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
||||||
@@ -57,6 +57,29 @@ if (UPS_Editor_SceneLoader::IsInEditorMode())
|
|||||||
// editor-only behaviour
|
// editor-only behaviour
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Known PIE-only AI quirk — use Standalone for AI testing
|
||||||
|
|
||||||
|
When the editor's "Play" button calls `UGameplayStatics::OpenLevel(BaseLevel)` from a PIE
|
||||||
|
session, the BaseLevel's `RecastNavMesh` actor ends up pending-kill at registration time
|
||||||
|
(UE logs `RegistrationFailed_DataPendingKill` ×2 and silently spawns an empty
|
||||||
|
`AbstractNavData-Default` as fallback). All `FindPathAsync` / `ProjectPointToNavigation`
|
||||||
|
queries return empty after that — pathfinding looks broken with no error.
|
||||||
|
|
||||||
|
This is **PIE-specific**:
|
||||||
|
- Standalone Game (Window → Standalone) — works correctly
|
||||||
|
- Packaged builds — work correctly
|
||||||
|
- PIE direct on BaseLevel (no editor map / OpenLevel involved) — works correctly
|
||||||
|
- PIE on editor map → click Play (OpenLevel transition inside PIE) — broken
|
||||||
|
|
||||||
|
Multiple workaround attempts have all failed (cascading re-registrations through
|
||||||
|
`RegisterNavData` / `OnInitializeActors` / `AddNavigationSystemToWorld` / `Build` /
|
||||||
|
`SpawnMissingNavigationData`; transition map; duplicated sublevel suffixed `_default`).
|
||||||
|
The cause is internal to PIE's OpenLevel handling and cannot be fixed from the plugin
|
||||||
|
side without engine modifications.
|
||||||
|
|
||||||
|
**Workflow:** for AI iteration, run **Standalone Game** instead of PIE. It exercises the
|
||||||
|
exact same code path as packaged builds.
|
||||||
or gate on `bIsCurrentlyLoading` to skip BP init during JSON restore.
|
or gate on `bIsCurrentlyLoading` to skip BP init during JSON restore.
|
||||||
|
|
||||||
## UI Modes — IMPORTANT for widget changes
|
## UI Modes — IMPORTANT for widget changes
|
||||||
@@ -83,15 +106,28 @@ Symptom that the wrong widget was edited: logs you added don't appear in the Out
|
|||||||
- When fixing a bug, fix the root cause, not the symptom.
|
- When fixing a bug, fix the root cause, not the symptom.
|
||||||
- If something requires error handling or validation to work reliably, include it without asking.
|
- If something requires error handling or validation to work reliably, include it without asking.
|
||||||
|
|
||||||
|
## Workflow — CRITICAL
|
||||||
|
- **Do not make behavior / logic changes without first explaining the plan and waiting for the
|
||||||
|
user's go-ahead.** Explain what the change will be, why it fixes the issue, and what side
|
||||||
|
effects it could have. Only after the user approves (or asks to proceed), apply the edit.
|
||||||
|
- Diagnostic-only additions (temporary `UE_LOG`, read-only inspections) are fine without
|
||||||
|
approval because they can't regress the project.
|
||||||
|
- Refactors, algorithm changes, comportement changes, new defaults, removed features — all
|
||||||
|
require approval first, even when they seem obvious.
|
||||||
|
- When revealing new data (e.g. logs) suggests a fix, STATE the proposed fix in plain words
|
||||||
|
and wait. Don't pre-emptively write + apply the code change in the same turn.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
- Engine: Unreal Engine 5.5
|
- Engine: Unreal Engine 5.7 (host project migrated from 5.5 in April 2026)
|
||||||
- Plugin location: `Unreal/Plugins/PS_Editor/`
|
- Plugin location: `Unreal/Plugins/PS_Editor/` (consumed via NTFS junction from the PROSERVE host project's `Plugins/` directory)
|
||||||
- Game module: `PS_ProserveEditor` (in `Unreal/Source/PS_ProserveEditor/`)
|
- Host project: `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` — always build this, not the plugin in isolation
|
||||||
|
- Game module: `PROSERVE_UE_5_7` (in the host project's `Source/`)
|
||||||
- CLI build command (Editor, Development):
|
- CLI build command (Editor, Development):
|
||||||
```
|
```
|
||||||
"<UE_INSTALL>/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="<REPO>/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.uproject" -WaitMutex -FromMsBuild
|
||||||
```
|
```
|
||||||
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5` > `InstalledDirectory`
|
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7` > `InstalledDirectory`
|
||||||
|
- The 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not build against it unless explicitly asked.
|
||||||
|
|
||||||
## Project Memory
|
## Project Memory
|
||||||
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
||||||
|
|||||||
BIN
Unreal/Content/Cubeezrzea.uasset
Normal file
BIN
Unreal/Content/Cubeezrzea.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/PS_MasterCatalog.uasset
Normal file
BIN
Unreal/Content/PS_MasterCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/PS_PropsCatalog.uasset
Normal file
BIN
Unreal/Content/PS_PropsCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/PS_SplineEnnemy.uasset
Normal file
BIN
Unreal/Content/PS_SplineEnnemy.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/PS_ToolsCatalog.uasset
Normal file
BIN
Unreal/Content/PS_ToolsCatalog.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -20,11 +20,11 @@ public:
|
|||||||
#if WITH_EDITORONLY_DATA
|
#if WITH_EDITORONLY_DATA
|
||||||
protected:
|
protected:
|
||||||
/** Icon visible in the editor viewport. Override the sprite in a derived Blueprint if needed. */
|
/** Icon visible in the editor viewport. Override the sprite in a derived Blueprint if needed. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TObjectPtr<class UBillboardComponent> SpriteComponent;
|
TObjectPtr<class UBillboardComponent> SpriteComponent;
|
||||||
|
|
||||||
/** Arrow showing the view direction in the editor viewport. */
|
/** Arrow showing the view direction in the editor viewport. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TObjectPtr<class UArrowComponent> ArrowComponent;
|
TObjectPtr<class UArrowComponent> ArrowComponent;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
|
#include "PS_Editor_SceneLoader.h"
|
||||||
|
#include "PS_Editor_SceneData.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "JsonObjectConverter.h"
|
||||||
|
|
||||||
void UPS_Editor_GameInstanceSubsystem::SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel)
|
void UPS_Editor_GameInstanceSubsystem::SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel, uint8 ScenarioType)
|
||||||
{
|
{
|
||||||
PendingScenarioName = ScenarioName;
|
PendingScenarioName = ScenarioName;
|
||||||
PendingBaseLevel = BaseLevel;
|
PendingBaseLevel = BaseLevel;
|
||||||
|
PendingScenarioType = ScenarioType;
|
||||||
LastEditedScenarioName = ScenarioName;
|
LastEditedScenarioName = ScenarioName;
|
||||||
LastEditedBaseLevel = BaseLevel;
|
LastEditedBaseLevel = BaseLevel;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scenario set: '%s' (base level: '%s')"), *ScenarioName, *BaseLevel);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scenario set: '%s' (base level: '%s', type: %u)"),
|
||||||
|
*ScenarioName, *BaseLevel, ScenarioType);
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScenario()
|
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScenario()
|
||||||
@@ -45,3 +51,44 @@ void UPS_Editor_GameInstanceSubsystem::ReturnToEditor(const UObject* WorldContex
|
|||||||
|
|
||||||
OpenEditor(WorldContextObject);
|
OpenEditor(WorldContextObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_GameInstanceSubsystem::ReadScenarioMetadata(const FString& ScenarioName, uint8& OutScenarioType, FString& OutBaseLevel) const
|
||||||
|
{
|
||||||
|
OutScenarioType = 0;
|
||||||
|
OutBaseLevel.Empty();
|
||||||
|
|
||||||
|
if (ScenarioName.IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString FilePath = UPS_Editor_SceneLoader::GetSceneFilePath(ScenarioName);
|
||||||
|
FString JsonString;
|
||||||
|
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ReadScenarioMetadata — file not found: %s"), *FilePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the full SceneData. We could parse just the metadata fields with a custom
|
||||||
|
// reader, but FJsonObjectConverter is fast enough for the scenario sizes we deal with
|
||||||
|
// (tens of actors max), and reusing the same struct keeps version handling consistent.
|
||||||
|
FPS_Editor_SceneData SceneData;
|
||||||
|
if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ReadScenarioMetadata — failed to parse JSON for '%s'"), *ScenarioName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutScenarioType = SceneData.ScenarioType;
|
||||||
|
OutBaseLevel = SceneData.LevelName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 UPS_Editor_GameInstanceSubsystem::GetScenarioTypeFromFile(const FString& ScenarioName) const
|
||||||
|
{
|
||||||
|
uint8 Type = 0;
|
||||||
|
FString DummyLevel;
|
||||||
|
ReadScenarioMetadata(ScenarioName, Type, DummyLevel);
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,41 +18,68 @@ class PS_EDITOR_API UPS_Editor_GameInstanceSubsystem : public UGameInstanceSubsy
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/** Scenario name to load on the next level. Set by PROSERVE or Play button, consumed on load. */
|
/** Scenario name to load on the next level. Set by PROSERVE or Play button, consumed on load. */
|
||||||
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString PendingScenarioName;
|
FString PendingScenarioName;
|
||||||
|
|
||||||
/** Base level name associated with the pending scenario. */
|
/** Base level name associated with the pending scenario. */
|
||||||
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString PendingBaseLevel;
|
FString PendingBaseLevel;
|
||||||
|
|
||||||
/** Last scenario/base level edited. Persists after consume — used by ReturnToEditor. */
|
/** Last scenario/base level edited. Persists after consume — used by ReturnToEditor. */
|
||||||
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadOnly, Category = "PS_Editor")
|
||||||
FString LastEditedScenarioName;
|
FString LastEditedScenarioName;
|
||||||
|
|
||||||
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadOnly, Category = "PS_Editor")
|
||||||
FString LastEditedBaseLevel;
|
FString LastEditedBaseLevel;
|
||||||
|
|
||||||
/** Name of the editor map to return to. Set this once at startup. */
|
/** Name of the editor map to return to. Set this once at startup. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString EditorMapName;
|
FString EditorMapName;
|
||||||
|
|
||||||
/** True when the Play button was pressed (gameplay mode). False when just loading/editing a scenario. */
|
/** True when the Play button was pressed (gameplay mode). False when just loading/editing a scenario. */
|
||||||
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
bool bIsPlayMode = false;
|
bool bIsPlayMode = false;
|
||||||
|
|
||||||
/** Set pending scenario + base level (for opening the editor or gameplay). */
|
/** Pending scenario type as uint8 — index into UPS_Editor_MasterCatalog::ScenarioTypeEnum.
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
* Set by the Play button alongside PendingScenarioName. The gameplay GameMode reads it
|
||||||
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
* (Byte to Enum) and routes its scenario-type-specific logic. Default 0 = first enum entry. */
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
|
uint8 PendingScenarioType = 0;
|
||||||
|
|
||||||
|
/** Set pending scenario + base level + type (for opening the editor or gameplay). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
|
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel, uint8 ScenarioType = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-parse a scenario file from disk (without spawning anything) to extract its
|
||||||
|
* persisted metadata: scenario type and base level. Use this from PROSERVE GameMode
|
||||||
|
* BeginPlay (or earlier) to know what type a scenario is BEFORE deciding to load it,
|
||||||
|
* when the standard Play-button flow hasn't populated PendingScenarioType.
|
||||||
|
*
|
||||||
|
* @param ScenarioName Name of the scenario file (without extension).
|
||||||
|
* @param OutScenarioType Set to the scenario's saved type (uint8 index into MasterCatalog::ScenarioTypeEnum). 0 on failure.
|
||||||
|
* @param OutBaseLevel Set to the scenario's saved base level name. Empty on failure.
|
||||||
|
* @return True if the file was found and parsed successfully.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
|
bool ReadScenarioMetadata(const FString& ScenarioName, uint8& OutScenarioType, FString& OutBaseLevel) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: read just the scenario type from a file. Returns 0 if the file isn't
|
||||||
|
* found / can't be parsed (which is also the default for a saved-but-untyped scenario).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
|
uint8 GetScenarioTypeFromFile(const FString& ScenarioName) const;
|
||||||
|
|
||||||
/** Consume the pending scenario (reads and clears). */
|
/** Consume the pending scenario (reads and clears). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
FString ConsumePendingScenario();
|
FString ConsumePendingScenario();
|
||||||
|
|
||||||
/** Open the editor with the current pending scenario + base level. */
|
/** Open the editor with the current pending scenario + base level. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
void OpenEditor(const UObject* WorldContextObject);
|
void OpenEditor(const UObject* WorldContextObject);
|
||||||
|
|
||||||
/** Return to the editor, keeping the current base level and scenario. */
|
/** Return to the editor, keeping the current base level and scenario. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
void ReturnToEditor(const UObject* WorldContextObject);
|
void ReturnToEditor(const UObject* WorldContextObject);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ public:
|
|||||||
APS_Editor_GameMode();
|
APS_Editor_GameMode();
|
||||||
|
|
||||||
/** Whether the editor is currently in edit mode (vs play/preview mode). */
|
/** Whether the editor is currently in edit mode (vs play/preview mode). */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
bool bIsEditMode = true;
|
bool bIsEditMode = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include "Camera/CameraComponent.h"
|
#include "Camera/CameraComponent.h"
|
||||||
#include "GameFramework/SpringArmComponent.h"
|
#include "GameFramework/SpringArmComponent.h"
|
||||||
#include "Components/PostProcessComponent.h"
|
#include "Components/PostProcessComponent.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
#include "EnhancedInputComponent.h"
|
#include "EnhancedInputComponent.h"
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "InputAction.h"
|
#include "InputAction.h"
|
||||||
@@ -10,6 +12,9 @@
|
|||||||
#include "InputTriggers.h"
|
#include "InputTriggers.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_GroundSnap.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -43,24 +48,27 @@ APS_Editor_Pawn::APS_Editor_Pawn()
|
|||||||
OutlinePostProcess = CreateDefaultSubobject<UPostProcessComponent>(TEXT("OutlinePostProcess"));
|
OutlinePostProcess = CreateDefaultSubobject<UPostProcessComponent>(TEXT("OutlinePostProcess"));
|
||||||
OutlinePostProcess->SetupAttachment(RootScene);
|
OutlinePostProcess->SetupAttachment(RootScene);
|
||||||
OutlinePostProcess->bUnbound = true; // Apply to entire scene
|
OutlinePostProcess->bUnbound = true; // Apply to entire scene
|
||||||
|
|
||||||
|
// Hard-ref the outline material at CDO-construction time so the cooker follows it into
|
||||||
|
// packaged builds. Replaces the old runtime LoadObject<> soft-string path.
|
||||||
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> OutlineMatFinder(
|
||||||
|
TEXT("/PS_Editor/M_PS_Editor_SelectionOutline.M_PS_Editor_SelectionOutline"));
|
||||||
|
if (OutlineMatFinder.Succeeded())
|
||||||
|
{
|
||||||
|
OutlineMaterial = OutlineMatFinder.Object;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::BeginPlay()
|
void APS_Editor_Pawn::BeginPlay()
|
||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
// Load outline post-process material
|
// OutlineMaterial is resolved in the ctor via ConstructorHelpers (hard cook ref).
|
||||||
UMaterialInterface* OutlineMat = LoadObject<UMaterialInterface>(
|
if (OutlineMaterial && OutlinePostProcess)
|
||||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_SelectionOutline.M_PS_Editor_SelectionOutline"));
|
|
||||||
if (OutlineMat && OutlinePostProcess)
|
|
||||||
{
|
{
|
||||||
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMat));
|
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMaterial));
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material loaded"));
|
|
||||||
}
|
}
|
||||||
|
else if (!OutlineMaterial)
|
||||||
// TODO: Spline occluded PP material — disabled for now, depth comparison unreliable
|
|
||||||
// Consider translucent second-pass approach instead of post-process
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. "
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. "
|
||||||
"Create a Post Process material in Plugins/PS_Editor/Content/"));
|
"Create a Post Process material in Plugins/PS_Editor/Content/"));
|
||||||
@@ -71,6 +79,26 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
|
|
||||||
|
// Focus animation: smoothly interpolate camera location/rotation toward the framed
|
||||||
|
// selection. Uses ease-in-out (smoothstep) so the camera doesn't snap at the boundaries.
|
||||||
|
if (bIsFocusing)
|
||||||
|
{
|
||||||
|
FocusElapsed = FMath::Min(FocusElapsed + DeltaTime, FocusDuration);
|
||||||
|
const float Linear = FocusElapsed / FocusDuration;
|
||||||
|
const float Alpha = Linear * Linear * (3.0f - 2.0f * Linear); // smoothstep
|
||||||
|
const FVector NewLoc = FMath::Lerp(FocusStartLocation, FocusTargetLocation, Alpha);
|
||||||
|
const FRotator NewRot = FMath::Lerp(FocusStartRotation, FocusTargetRotation, Alpha);
|
||||||
|
SetActorLocation(NewLoc);
|
||||||
|
SetActorRotation(NewRot);
|
||||||
|
if (FocusElapsed >= FocusDuration)
|
||||||
|
{
|
||||||
|
bIsFocusing = false;
|
||||||
|
// Sync orbit state so a subsequent Alt+drag pivots around the new focal point.
|
||||||
|
OrbitYaw = NewRot.Yaw;
|
||||||
|
OrbitPitch = NewRot.Pitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
||||||
@@ -223,6 +251,16 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_Cancel = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
|
IA_Cancel = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
|
||||||
IA_Cancel->ValueType = EInputActionValueType::Boolean;
|
IA_Cancel->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape);
|
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape);
|
||||||
|
|
||||||
|
// Focus selection (Maya/Blender style)
|
||||||
|
IA_Focus = NewObject<UInputAction>(this, TEXT("IA_Focus"));
|
||||||
|
IA_Focus->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Focus, EKeys::F);
|
||||||
|
|
||||||
|
// Toggle navmesh visualization (mimics UE editor's P key)
|
||||||
|
IA_ToggleNavMesh = NewObject<UInputAction>(this, TEXT("IA_ToggleNavMesh"));
|
||||||
|
IA_ToggleNavMesh->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_ToggleNavMesh, EKeys::P);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -269,6 +307,8 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
|
EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
|
||||||
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
||||||
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
||||||
|
EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus);
|
||||||
|
EIC->BindAction(IA_ToggleNavMesh, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleToggleNavMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -442,6 +482,21 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle));
|
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle));
|
||||||
const FVector GizmoLoc = GizmoDragPlaneOrigin;
|
const FVector GizmoLoc = GizmoDragPlaneOrigin;
|
||||||
|
|
||||||
|
// ChildMovable rotation: rotate only the active child instead of all selected actors.
|
||||||
|
// We compose the delta quat onto the initial rotation captured at drag start —
|
||||||
|
// reading the live value each frame would feed back through GetChildRotation and
|
||||||
|
// drift if the BP impl clamps axes.
|
||||||
|
if (ActiveSplinePointIndex >= 0
|
||||||
|
&& DraggedSplineActor.IsValid()
|
||||||
|
&& DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())
|
||||||
|
&& SplinePointDragInitialRotations.IsValidIndex(ActiveSplinePointIndex))
|
||||||
|
{
|
||||||
|
const FQuat OrigRot = SplinePointDragInitialRotations[ActiveSplinePointIndex].Quaternion();
|
||||||
|
const FRotator NewRot = (RotationQuat * OrigRot).Rotator();
|
||||||
|
IPS_Editor_ChildMovable::Execute_RotateChild(DraggedSplineActor.Get(), ActiveSplinePointIndex, NewRot);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Selected[i].Get())
|
if (AActor* Actor = Selected[i].Get())
|
||||||
@@ -454,6 +509,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (Mode == EPS_Editor_TransformMode::Scale)
|
else if (Mode == EPS_Editor_TransformMode::Scale)
|
||||||
{
|
{
|
||||||
// Use combined mouse X+Y screen movement to drive scale
|
// Use combined mouse X+Y screen movement to drive scale
|
||||||
@@ -674,6 +730,9 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
ActiveSplinePointIndex = ChildIdx;
|
ActiveSplinePointIndex = ChildIdx;
|
||||||
DraggedSplineActor = ActiveActor;
|
DraggedSplineActor = ActiveActor;
|
||||||
|
// Clear any previous highlight before applying the new one — without this, picking
|
||||||
|
// a different child leaves the prior one stuck in highlight state.
|
||||||
|
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(ActiveActor);
|
||||||
IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx);
|
IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx);
|
||||||
|
|
||||||
if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager())
|
||||||
@@ -753,7 +812,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -769,22 +828,18 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(EdPC3->GetActivePlacementActor()))
|
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(EdPC3->GetActivePlacementActor()))
|
||||||
{
|
{
|
||||||
// Raycast to get world click position
|
// Project the click ray onto ground. Uses the shared helper so that a click
|
||||||
FHitResult AddHit;
|
// pointing at the sky still lands the control point on the ground below the
|
||||||
FCollisionQueryParams AddParams;
|
// camera's forward vector (instead of floating 1000 units ahead).
|
||||||
AddParams.AddIgnoredActor(this);
|
TArray<AActor*> Ignored;
|
||||||
AddParams.AddIgnoredActor(Cast<AActor>(Spline));
|
Ignored.Add(this);
|
||||||
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
if (AActor* SplineActor = Cast<AActor>(Spline)) Ignored.Add(SplineActor);
|
||||||
|
|
||||||
FVector ClickPos;
|
FVector ClickPos = PS_Editor_GroundSnap::ProjectCameraRayToGround(
|
||||||
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Camera, AddParams))
|
GetWorld(), RayOrigin, RayDir, Ignored);
|
||||||
{
|
// Lift the CP slightly above the surface so the tube mesh renders cleanly
|
||||||
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
// (otherwise Z-fighting with the ground).
|
||||||
}
|
ClickPos.Z += 5.0f;
|
||||||
else
|
|
||||||
{
|
|
||||||
ClickPos = RayOrigin + RayDir * 1000.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
Spline->AddPoint(ClickPos);
|
Spline->AddPoint(ClickPos);
|
||||||
@@ -813,7 +868,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,6 +902,7 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
TArray<FVector> NewPoints;
|
TArray<FVector> NewPoints;
|
||||||
|
TArray<FRotator> NewRotations;
|
||||||
FString Desc;
|
FString Desc;
|
||||||
|
|
||||||
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
|
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
|
||||||
@@ -857,7 +913,16 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
||||||
{
|
{
|
||||||
NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
|
NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
|
||||||
Desc = TEXT("Move child element");
|
// Snapshot rotations so the undo system restores both translation and rotation.
|
||||||
|
// The arrays may be empty if the BP didn't implement rotation — that's fine,
|
||||||
|
// the undo Execute/Undo only applies when both old and new are non-empty.
|
||||||
|
NewRotations = IPS_Editor_ChildMovable::Execute_GetAllChildRotations(DraggedSplineActor.Get());
|
||||||
|
bool bIsRotate = false;
|
||||||
|
if (UPS_Editor_SelectionManager* SMDesc = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
bIsRotate = (SMDesc->GetTransformMode() == EPS_Editor_TransformMode::Rotate);
|
||||||
|
}
|
||||||
|
Desc = bIsRotate ? TEXT("Rotate child element") : TEXT("Move child element");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NewPoints.Num() > 0)
|
if (NewPoints.Num() > 0)
|
||||||
@@ -866,6 +931,8 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
Action->SplineActor = DraggedSplineActor.Get();
|
Action->SplineActor = DraggedSplineActor.Get();
|
||||||
Action->OldPoints = SplinePointDragInitialState;
|
Action->OldPoints = SplinePointDragInitialState;
|
||||||
Action->NewPoints = NewPoints;
|
Action->NewPoints = NewPoints;
|
||||||
|
Action->OldRotations = SplinePointDragInitialRotations;
|
||||||
|
Action->NewRotations = NewRotations;
|
||||||
Action->Description = Desc;
|
Action->Description = Desc;
|
||||||
UM->RecordAction(Action);
|
UM->RecordAction(Action);
|
||||||
}
|
}
|
||||||
@@ -992,7 +1059,7 @@ void APS_Editor_Pawn::HandleTranslateMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Translate mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Translate mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1005,7 +1072,7 @@ void APS_Editor_Pawn::HandleRotateMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Rotate);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Rotate);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Rotate mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Rotate mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,11 +1085,17 @@ void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Scale);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Scale);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scale mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scale mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::ClearActiveSubElementDrag()
|
||||||
|
{
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
@@ -1082,7 +1155,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deleted spline point, %d remaining"), Spline->GetNumPoints());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deleted spline point, %d remaining"), Spline->GetNumPoints());
|
||||||
}
|
}
|
||||||
else if (Spline)
|
else if (Spline)
|
||||||
{
|
{
|
||||||
@@ -1094,8 +1167,35 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
// Filter out mandatory actors (UPS_Editor_SpawnableComponent::bIsMandatory) — they're
|
||||||
|
// pinned to the scene by design and the delete is silently refused for them. We pre-filter
|
||||||
|
// here so the confirmation dialog count matches what will actually be deleted, and we
|
||||||
|
// skip the dialog entirely if nothing is deletable.
|
||||||
|
UPS_Editor_SpawnManager* SpawnManager = EditorPC->GetSpawnManager();
|
||||||
|
TArray<TWeakObjectPtr<AActor>> DeletableActors;
|
||||||
|
int32 SkippedMandatory = 0;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
AActor* Actor = Weak.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
if (SpawnManager && !SpawnManager->IsActorDeletable(Actor))
|
||||||
|
{
|
||||||
|
++SkippedMandatory;
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: '%s' is a mandatory actor — delete refused."),
|
||||||
|
*Actor->GetName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DeletableActors.Add(Weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DeletableActors.Num() == 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Delete aborted — selection contained only mandatory actor(s) (%d)."), SkippedMandatory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show in-game confirmation dialog (runtime compatible)
|
// Show in-game confirmation dialog (runtime compatible)
|
||||||
const int32 Count = SM->GetSelectedActors().Num();
|
const int32 Count = DeletableActors.Num();
|
||||||
const FText Message = FText::Format(
|
const FText Message = FText::Format(
|
||||||
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
||||||
FText::AsNumber(Count));
|
FText::AsNumber(Count));
|
||||||
@@ -1108,15 +1208,15 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
// Weak refs to avoid dangling pointers in the callback
|
// Weak refs to avoid dangling pointers in the callback
|
||||||
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC = EditorPC;
|
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC = EditorPC;
|
||||||
|
|
||||||
Dialog->Show(Message, FPS_Editor_OnConfirmResult::CreateLambda([WeakPC](bool bConfirmed)
|
Dialog->Show(Message, FPS_Editor_OnConfirmResult::CreateLambda([WeakPC, DeletableActors](bool bConfirmed)
|
||||||
{
|
{
|
||||||
if (!bConfirmed || !WeakPC.IsValid()) return;
|
if (!bConfirmed || !WeakPC.IsValid()) return;
|
||||||
|
|
||||||
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM) return;
|
||||||
|
|
||||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : DeletableActors)
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
@@ -1188,11 +1288,6 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
|
||||||
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
|
||||||
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("null"),
|
|
||||||
Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("null"));
|
|
||||||
|
|
||||||
// Store state for undo
|
// Store state for undo
|
||||||
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
|
||||||
@@ -1323,6 +1418,92 @@ void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleFocus(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
// Frame the current selection (Maya/Blender F-key behaviour). Computes the combined
|
||||||
|
// world-space bounding box of every selected actor, then animates the camera to a
|
||||||
|
// distance that fits the box in view while keeping the current viewing direction.
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||||
|
if (!EditorPC) return;
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
FBox CombinedBox(EForceInit::ForceInit);
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
const FBox ActorBox = Actor->GetComponentsBoundingBox(true);
|
||||||
|
if (ActorBox.IsValid)
|
||||||
|
{
|
||||||
|
CombinedBox += ActorBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!CombinedBox.IsValid)
|
||||||
|
{
|
||||||
|
// Fallback: use actor pivots if no component reports valid bounds (rare — empty actors)
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
CombinedBox += FBox(Actor->GetActorLocation(), Actor->GetActorLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!CombinedBox.IsValid) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Center = CombinedBox.GetCenter();
|
||||||
|
const FVector Extent = CombinedBox.GetExtent();
|
||||||
|
// Sphere radius enclosing the box; clamp to a minimum so picking a point-like actor
|
||||||
|
// (zero-size bounds) still leaves the camera at a usable distance.
|
||||||
|
const float Radius = FMath::Max(Extent.Size(), 50.0f);
|
||||||
|
|
||||||
|
// Distance to fit a sphere of given radius given the camera's vertical FOV.
|
||||||
|
float FOV = 90.0f;
|
||||||
|
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (PC->PlayerCameraManager)
|
||||||
|
{
|
||||||
|
FOV = PC->PlayerCameraManager->GetFOVAngle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f);
|
||||||
|
const float FitDistance = Radius / FMath::Max(FMath::Tan(HalfFOVRad), 0.01f);
|
||||||
|
const float TargetDistance = FMath::Max(FitDistance * 1.4f, 100.0f); // 40% margin
|
||||||
|
|
||||||
|
// Keep current viewing direction; just slide the camera so Center sits TargetDistance
|
||||||
|
// in front along that direction. Then force the rotation to look squarely at Center —
|
||||||
|
// guards against accumulated rounding so the pivot for the next orbit is exact.
|
||||||
|
const FVector Forward = GetActorForwardVector();
|
||||||
|
FocusStartLocation = GetActorLocation();
|
||||||
|
FocusStartRotation = GetActorRotation();
|
||||||
|
FocusTargetLocation = Center - Forward * TargetDistance;
|
||||||
|
FocusTargetRotation = (Center - FocusTargetLocation).Rotation();
|
||||||
|
|
||||||
|
FocusElapsed = 0.0f;
|
||||||
|
bIsFocusing = true;
|
||||||
|
|
||||||
|
// Update orbit state so Alt+drag immediately after F pivots around the new center.
|
||||||
|
OrbitFocalPoint = Center;
|
||||||
|
OrbitDistance = TargetDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleToggleNavMesh(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
// Mimics UE editor's P key — toggles the runtime navmesh visualization. We route
|
||||||
|
// through SpawnManager's session-only bShowNavMesh flag so the Settings popup
|
||||||
|
// checkbox stays in sync.
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||||
|
if (!EditorPC) return;
|
||||||
|
UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
SM->bShowNavMesh = !SM->bShowNavMesh;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||||
@@ -1515,6 +1696,7 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store initial sub-element state if editing a point/child
|
// Store initial sub-element state if editing a point/child
|
||||||
|
SplinePointDragInitialRotations.Reset();
|
||||||
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
|
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
|
||||||
@@ -1524,6 +1706,9 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
|||||||
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
||||||
{
|
{
|
||||||
SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
|
SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
|
||||||
|
// Snapshot rotations too so the rotate gizmo can compute deltas from a fixed
|
||||||
|
// reference and the undo system can restore them.
|
||||||
|
SplinePointDragInitialRotations = IPS_Editor_ChildMovable::Execute_GetAllChildRotations(DraggedSplineActor.Get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class USpringArmComponent;
|
|||||||
class UPostProcessComponent;
|
class UPostProcessComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
|
class UMaterialInterface;
|
||||||
|
|
||||||
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
||||||
|
|
||||||
@@ -68,6 +69,11 @@ public:
|
|||||||
/** Currently selected spline point index (-1 = none). Used by UI for Delete Point button. */
|
/** Currently selected spline point index (-1 = none). Used by UI for Delete Point button. */
|
||||||
int32 ActiveSplinePointIndex = -1;
|
int32 ActiveSplinePointIndex = -1;
|
||||||
|
|
||||||
|
/** Clear any active sub-element drag state (spline point / ChildMovable index + target actor).
|
||||||
|
* Call this when leaving placement / edit mode so a subsequent gizmo drag on the newly
|
||||||
|
* selected parent actor moves the whole actor instead of the last clicked point. */
|
||||||
|
void ClearActiveSubElementDrag();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
@@ -85,6 +91,12 @@ protected:
|
|||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
TObjectPtr<UPostProcessComponent> OutlinePostProcess;
|
TObjectPtr<UPostProcessComponent> OutlinePostProcess;
|
||||||
|
|
||||||
|
/** Selection outline post-process material (from /PS_Editor/M_PS_Editor_SelectionOutline).
|
||||||
|
* Hard-ref so the cooker picks it up in packaged builds. Resolved via ConstructorHelpers
|
||||||
|
* in the ctor. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInterface> OutlineMaterial;
|
||||||
|
|
||||||
// ---- Camera Settings ----
|
// ---- Camera Settings ----
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
@@ -197,9 +209,18 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_Cancel;
|
TObjectPtr<UInputAction> IA_Cancel;
|
||||||
|
|
||||||
// ---- Spline point drag state ----
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Focus;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_ToggleNavMesh;
|
||||||
|
|
||||||
|
// ---- Spline point / ChildMovable drag state ----
|
||||||
TWeakObjectPtr<AActor> DraggedSplineActor;
|
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||||
TArray<FVector> SplinePointDragInitialState;
|
TArray<FVector> SplinePointDragInitialState;
|
||||||
|
// Initial rotations captured at drag start when dragging a ChildMovable sub-element.
|
||||||
|
// Empty for spline points (which do not have a rotation concept).
|
||||||
|
TArray<FRotator> SplinePointDragInitialRotations;
|
||||||
FVector SplinePointDragPlaneOrigin;
|
FVector SplinePointDragPlaneOrigin;
|
||||||
FVector SplinePointDragPlaneNormal;
|
FVector SplinePointDragPlaneNormal;
|
||||||
|
|
||||||
@@ -241,6 +262,17 @@ private:
|
|||||||
void HandleSplineMode(const FInputActionValue& Value);
|
void HandleSplineMode(const FInputActionValue& Value);
|
||||||
void HandleConfirm(const FInputActionValue& Value);
|
void HandleConfirm(const FInputActionValue& Value);
|
||||||
void HandleCancel(const FInputActionValue& Value);
|
void HandleCancel(const FInputActionValue& Value);
|
||||||
|
void HandleFocus(const FInputActionValue& Value);
|
||||||
|
void HandleToggleNavMesh(const FInputActionValue& Value);
|
||||||
|
|
||||||
|
// ---- Focus animation state (F key, frame selection) ----
|
||||||
|
bool bIsFocusing = false;
|
||||||
|
float FocusElapsed = 0.0f;
|
||||||
|
float FocusDuration = 0.25f;
|
||||||
|
FVector FocusStartLocation = FVector::ZeroVector;
|
||||||
|
FVector FocusTargetLocation = FVector::ZeroVector;
|
||||||
|
FRotator FocusStartRotation = FRotator::ZeroRotator;
|
||||||
|
FRotator FocusTargetRotation = FRotator::ZeroRotator;
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
void CreateInputActions();
|
void CreateInputActions();
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "PS_Editor_SplineEditable.h"
|
#include "PS_Editor_SplineEditable.h"
|
||||||
#include "PS_Editor_PointPlaceable.h"
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
#include "PS_Editor_EditableInterface.h"
|
#include "PS_Editor_EditableInterface.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
#include "Engine/LevelStreamingDynamic.h"
|
#include "Engine/LevelStreamingDynamic.h"
|
||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
@@ -29,6 +31,44 @@
|
|||||||
#include "GameFramework/GameStateBase.h"
|
#include "GameFramework/GameStateBase.h"
|
||||||
#include "GameFramework/PlayerState.h"
|
#include "GameFramework/PlayerState.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** Resolve an editable property path ("Comp.Prop" or "Prop") on Actor to a UObject target +
|
||||||
|
* FProperty pointer. Mirrors PS_Editor_MainWidget::FindPropertyTarget + class lookup so the
|
||||||
|
* PlayerController can snapshot/restore values without depending on the widget. */
|
||||||
|
static bool ResolveEditableProp(AActor* Actor, const FName& Path, UObject*& OutTarget, FProperty*& OutProp)
|
||||||
|
{
|
||||||
|
OutTarget = nullptr;
|
||||||
|
OutProp = nullptr;
|
||||||
|
if (!Actor) return false;
|
||||||
|
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
|
||||||
|
if (CompName.IsEmpty())
|
||||||
|
{
|
||||||
|
OutTarget = Actor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
Actor->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName)
|
||||||
|
{
|
||||||
|
OutTarget = Comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OutTarget) return false;
|
||||||
|
OutProp = OutTarget->GetClass()->FindPropertyByName(*PropName);
|
||||||
|
return OutProp != nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
{
|
{
|
||||||
bShowMouseCursor = true;
|
bShowMouseCursor = true;
|
||||||
@@ -98,8 +138,8 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
|
|
||||||
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
|
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
|
||||||
{
|
{
|
||||||
bool bLoaded = SceneSerializer->LoadScene(Sub->PendingScenarioName, SpawnManager);
|
bool bLoaded = SceneSerializer->LoadScenario(Sub->PendingScenarioName, SpawnManager);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScene('%s') = %s"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScenario('%s') = %s"),
|
||||||
*Sub->PendingScenarioName, bLoaded ? TEXT("OK") : TEXT("FAILED"));
|
*Sub->PendingScenarioName, bLoaded ? TEXT("OK") : TEXT("FAILED"));
|
||||||
}
|
}
|
||||||
Sub->PendingScenarioName.Empty();
|
Sub->PendingScenarioName.Empty();
|
||||||
@@ -170,6 +210,20 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
|
{
|
||||||
|
// Reset the runtime-editor flag whenever this controller's world tears down — covers
|
||||||
|
// OpenLevel from any caller (Play button, scenario loader BP, gameplay code that calls
|
||||||
|
// OpenLevel directly), end of PIE, and seamless world transitions. Without this, the
|
||||||
|
// static would stay 'true' from the previous editor session and IsInEditorMode() would
|
||||||
|
// lie in the next world (which has no PS_Editor PlayerController). The 3 existing eager
|
||||||
|
// resets (CloseEditor + Play buttons) remain as defense-in-depth so any check between
|
||||||
|
// the click and the actual world tear-down also returns false.
|
||||||
|
UPS_Editor_SceneLoader::bIsEditorModeActive = false;
|
||||||
|
|
||||||
|
Super::EndPlay(EndPlayReason);
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::Tick(float DeltaTime)
|
void APS_Editor_PlayerController::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
@@ -186,6 +240,17 @@ void APS_Editor_PlayerController::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
SpawnManager->TickEditorMode(DeltaTime);
|
SpawnManager->TickEditorMode(DeltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the gizmo glued to its selection every frame while simulating — otherwise
|
||||||
|
// animated actors (AI walking, timeline-driven properties, ragdoll) leave the gizmo
|
||||||
|
// behind at the pose it had when the actor was last selected / dragged. Outside of
|
||||||
|
// simulation the selection doesn't move on its own so the normal mutation-driven
|
||||||
|
// UpdateGizmo calls (click / drag / undo) are enough — and skipping this avoids any
|
||||||
|
// feedback with the pawn's in-progress gizmo drag.
|
||||||
|
if (bSimulating && SelectionManager && SelectionManager->IsAnythingSelected())
|
||||||
|
{
|
||||||
|
SelectionManager->UpdateGizmo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||||
@@ -264,6 +329,43 @@ void APS_Editor_PlayerController::StartSimulation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot editable property values so we can restore them on Stop. Game logic may mutate
|
||||||
|
// editable properties during simulate (e.g. WalkSpeed driven by gameplay code) — restoring
|
||||||
|
// on Stop matches the convention used for transforms. Skip properties already tracked by the
|
||||||
|
// timeline: the timeline's RestoreBaseline owns those, double-restoring would conflict.
|
||||||
|
SimulationSavedEditableProps.Empty();
|
||||||
|
UPS_Editor_TimelineSubsystem* TimelineSS = GetWorld() ? GetWorld()->GetSubsystem<UPS_Editor_TimelineSubsystem>() : nullptr;
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
||||||
|
{
|
||||||
|
AActor* Actor = Weak.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
|
if (!EditComp || EditComp->EditableProperties.Num() == 0) continue;
|
||||||
|
|
||||||
|
TMap<FName, FString> ActorMap;
|
||||||
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
if (TimelineSS && TimelineSS->IsPropertyTracked(Actor, Entry.Path))
|
||||||
|
{
|
||||||
|
continue; // timeline owns this one
|
||||||
|
}
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
if (!ResolveEditableProp(Actor, Entry.Path, Target, Prop)) continue;
|
||||||
|
|
||||||
|
FString Exported;
|
||||||
|
Prop->ExportText_InContainer(0, Exported, Target, Target, nullptr, PPF_None);
|
||||||
|
ActorMap.Add(Entry.Path, Exported);
|
||||||
|
}
|
||||||
|
if (ActorMap.Num() > 0)
|
||||||
|
{
|
||||||
|
SimulationSavedEditableProps.Add(Actor, MoveTemp(ActorMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear selection
|
// Clear selection
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
{
|
{
|
||||||
@@ -378,18 +480,23 @@ void APS_Editor_PlayerController::SetSimulationPaused(bool bPaused)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::RequestStopSimulation()
|
void APS_Editor_PlayerController::RequestStopSimulation(bool bImmediate)
|
||||||
{
|
{
|
||||||
if (!bSimulating) return;
|
if (!bSimulating) return;
|
||||||
|
|
||||||
// If a request is already scheduled, do nothing (debounce double-clicks on Stop).
|
// If a request is already scheduled, do nothing (debounce double-clicks on Stop).
|
||||||
|
// Skip the debounce when bImmediate (Restart wants to flush right now).
|
||||||
|
if (!bImmediate)
|
||||||
|
{
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (PendingStopSimulationHandle.IsValid()) return;
|
if (PendingStopSimulationHandle.IsValid()) return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fire OnEditorBeforeSimulateStop immediately on every actor so BP handlers can react
|
// Fire OnEditorBeforeSimulateStop immediately on every actor so BP handlers can react
|
||||||
// while the BT is still live. Then schedule the real cut for ~500ms later.
|
// while the BT is still live. Then schedule the real cut (deferred) or run it now
|
||||||
|
// (immediate, used by timeline Restart).
|
||||||
if (SpawnManager)
|
if (SpawnManager)
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
||||||
@@ -402,6 +509,21 @@ void APS_Editor_PlayerController::RequestStopSimulation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bImmediate)
|
||||||
|
{
|
||||||
|
// Cancel any pending deferred stop so it doesn't fire again later.
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (PendingStopSimulationHandle.IsValid())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().ClearTimer(PendingStopSimulationHandle);
|
||||||
|
PendingStopSimulationHandle.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StopSimulation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
TWeakObjectPtr<APS_Editor_PlayerController> WeakThis(this);
|
TWeakObjectPtr<APS_Editor_PlayerController> WeakThis(this);
|
||||||
@@ -491,7 +613,49 @@ void APS_Editor_PlayerController::StopSimulation()
|
|||||||
}
|
}
|
||||||
SimulationSavedTransforms.Empty();
|
SimulationSavedTransforms.Empty();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored"));
|
// Restore editable properties that game logic may have mutated during simulate. The timeline's
|
||||||
|
// own RestoreBaseline already covers the animated ones (we skipped them at snapshot time), so
|
||||||
|
// this loop only touches properties NOT owned by the timeline — no double-write. Mirrors the
|
||||||
|
// canonical UI write path (ImportText + MarkRenderStateDirty + RepNotify + OnEditorPropertyChanged)
|
||||||
|
// so actors reinit consistently regardless of where the value change originated.
|
||||||
|
int32 RestoredCount = 0;
|
||||||
|
for (auto& ActorPair : SimulationSavedEditableProps)
|
||||||
|
{
|
||||||
|
AActor* Actor = ActorPair.Key.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
|
for (auto& PropPair : ActorPair.Value)
|
||||||
|
{
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
if (!ResolveEditableProp(Actor, PropPair.Key, Target, Prop)) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(*PropPair.Value, Target, Target, PPF_None);
|
||||||
|
|
||||||
|
if (UActorComponent* Comp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
Comp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepNotify mirror — ImportText bypasses it, same as the UI write path.
|
||||||
|
if (!Prop->RepNotifyFunc.IsNone())
|
||||||
|
{
|
||||||
|
if (UFunction* RepFunc = Target->FindFunction(Prop->RepNotifyFunc))
|
||||||
|
{
|
||||||
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bImplementsInterface)
|
||||||
|
{
|
||||||
|
IPS_Editor_EditableInterface::Execute_OnEditorPropertyChanged(Actor, PropPair.Key.ToString());
|
||||||
|
}
|
||||||
|
++RestoredCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SimulationSavedEditableProps.Empty();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored (%d editable properties also restored)"), RestoredCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
||||||
@@ -505,7 +669,7 @@ void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActo
|
|||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
|
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
|
||||||
@@ -532,7 +696,7 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
|
|||||||
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true);
|
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::FinishPointPlacement()
|
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||||
@@ -554,13 +718,21 @@ void APS_Editor_PlayerController::FinishPointPlacement()
|
|||||||
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(PlacementActor);
|
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(PlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the pawn's sub-element drag state (last clicked spline point / child index).
|
||||||
|
// Without this, grabbing the gizmo right after Done would move the last clicked point
|
||||||
|
// instead of the whole actor, because the drag logic keys on ActiveSplinePointIndex.
|
||||||
|
if (APS_Editor_Pawn* EditorPawn = Cast<APS_Editor_Pawn>(GetPawn()))
|
||||||
|
{
|
||||||
|
EditorPawn->ClearActiveSubElementDrag();
|
||||||
|
}
|
||||||
|
|
||||||
if (PlacementActor && SelectionManager)
|
if (PlacementActor && SelectionManager)
|
||||||
{
|
{
|
||||||
SelectionManager->ClearSelection();
|
SelectionManager->ClearSelection();
|
||||||
SelectionManager->SelectActor(PlacementActor);
|
SelectionManager->SelectActor(PlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Exited placement mode"));
|
||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
@@ -601,7 +773,7 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
CurrentSublevel->SetShouldBeVisible(false);
|
CurrentSublevel->SetShouldBeVisible(false);
|
||||||
CurrentSublevel->SetIsRequestingUnloadAndRemoval(true);
|
CurrentSublevel->SetIsRequestingUnloadAndRemoval(true);
|
||||||
CurrentSublevel = nullptr;
|
CurrentSublevel = nullptr;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Unloaded previous sublevel"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild spawn catalog: global + base-level-specific catalogs
|
// Rebuild spawn catalog: global + base-level-specific catalogs
|
||||||
@@ -721,7 +893,7 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
ExtraSublevel->SetShouldBeLoaded(true);
|
ExtraSublevel->SetShouldBeLoaded(true);
|
||||||
ExtraSublevel->SetShouldBeVisible(true);
|
ExtraSublevel->SetShouldBeVisible(true);
|
||||||
AdditionalSublevels.Add(ExtraSublevel);
|
AdditionalSublevels.Add(ExtraSublevel);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded additional sublevel '%s'"), *ExtraPath);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Loaded additional sublevel '%s'"), *ExtraPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -769,7 +941,7 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
Actor->MarkComponentsRenderStateDirty();
|
Actor->MarkComponentsRenderStateDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deferred lightmap refresh on sublevel"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deferred lightmap refresh on sublevel"));
|
||||||
}, 0.5f, false);
|
}, 0.5f, false);
|
||||||
|
|
||||||
APS_Editor_CameraStart* CameraStart = nullptr;
|
APS_Editor_CameraStart* CameraStart = nullptr;
|
||||||
@@ -782,7 +954,7 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
if (APostProcessVolume* PPVolume = Cast<APostProcessVolume>(Actor))
|
if (APostProcessVolume* PPVolume = Cast<APostProcessVolume>(Actor))
|
||||||
{
|
{
|
||||||
PPVolume->Settings.WeightedBlendables.Array.Empty();
|
PPVolume->Settings.WeightedBlendables.Array.Empty();
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find camera start marker
|
// Find camera start marker
|
||||||
@@ -800,11 +972,21 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
const FRotator YawOnly(CameraStart->GetActorRotation().Pitch, CameraStart->GetActorRotation().Yaw, 0.0f);
|
const FRotator YawOnly(CameraStart->GetActorRotation().Pitch, CameraStart->GetActorRotation().Yaw, 0.0f);
|
||||||
MyPawn->SetActorLocationAndRotation(CameraStart->GetActorLocation(), YawOnly);
|
MyPawn->SetActorLocationAndRotation(CameraStart->GetActorLocation(), YawOnly);
|
||||||
SetControlRotation(YawOnly);
|
SetControlRotation(YawOnly);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Camera moved to CameraStart at %s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Camera moved to CameraStart at %s"),
|
||||||
*CameraStart->GetActorLocation().ToString());
|
*CameraStart->GetActorLocation().ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-spawn mandatory actors now that the baseLevel is loaded and visible. This covers:
|
||||||
|
// - Initial editor startup (default baseLevel loads at BeginPlay)
|
||||||
|
// - User switching baseLevel via the UI
|
||||||
|
// Idempotent — already-present mandatory instances are skipped. The catalog was rebuilt
|
||||||
|
// just before LoadLevelInstance so its bIsMandatory cache is fresh.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
|
}
|
||||||
|
|
||||||
// Delay 2s then fade in from black over 2 seconds
|
// Delay 2s then fade in from black over 2 seconds
|
||||||
if (bWaitingForSublevelFadeIn)
|
if (bWaitingForSublevelFadeIn)
|
||||||
{
|
{
|
||||||
@@ -909,7 +1091,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
|||||||
{
|
{
|
||||||
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
||||||
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,58 +30,63 @@ class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
|||||||
public:
|
public:
|
||||||
APS_Editor_PlayerController();
|
APS_Editor_PlayerController();
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
UPS_Editor_SpawnManager* GetSpawnManager() const { return SpawnManager; }
|
UPS_Editor_SpawnManager* GetSpawnManager() const { return SpawnManager; }
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
||||||
|
|
||||||
/** Master catalog grouping all sub-catalogs (Tools, Characters, Props...). */
|
/** Master catalog grouping all sub-catalogs (Tools, Characters, Props...). */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TObjectPtr<UPS_Editor_MasterCatalog> MasterCatalogAsset;
|
TObjectPtr<UPS_Editor_MasterCatalog> MasterCatalogAsset;
|
||||||
|
|
||||||
/** Current base level for scene save/load filtering. Auto-detected at startup, switchable via UI. */
|
/** Current base level for scene save/load filtering. Auto-detected at startup, switchable via UI. */
|
||||||
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString CurrentBaseLevel;
|
FString CurrentBaseLevel;
|
||||||
|
|
||||||
/** Fired when the editor Close button is pressed (after optional save).
|
/** Fired when the editor Close button is pressed (after optional save).
|
||||||
* Bind this in Blueprint to override the default behavior (quit application).
|
* Bind this in Blueprint to override the default behavior (quit application).
|
||||||
* If nothing is bound, the application exits. */
|
* If nothing is bound, the application exits. */
|
||||||
UPROPERTY(BlueprintAssignable, Category = "ASTERION|PS_Editor")
|
UPROPERTY(BlueprintAssignable, Category = "PS_Editor")
|
||||||
FOnEditorClosed OnEditorClosed;
|
FOnEditorClosed OnEditorClosed;
|
||||||
|
|
||||||
/** Call to close the editor. Broadcasts OnEditorClosed; if unbound, quits the app. */
|
/** Call to close the editor. Broadcasts OnEditorClosed; if unbound, quits the app. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void CloseEditor();
|
void CloseEditor();
|
||||||
|
|
||||||
// ---- Simulate Mode ----
|
// ---- Simulate Mode ----
|
||||||
|
|
||||||
/** Start simulation: enable AI, store actor positions, disable saving.
|
/** Start simulation: enable AI, store actor positions, disable saving.
|
||||||
* Cancels any in-flight deferred RequestStopSimulation. */
|
* Cancels any in-flight deferred RequestStopSimulation. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void StartSimulation();
|
void StartSimulation();
|
||||||
|
|
||||||
/** Stop simulation: disable AI, restore actor positions, re-enable saving.
|
/** Stop simulation: disable AI, restore actor positions, re-enable saving.
|
||||||
* Synchronous — fires OnEditorSimulateStop and cuts AI immediately. Callers that want
|
* Synchronous — fires OnEditorSimulateStop and cuts AI immediately. Callers that want
|
||||||
* a BT grace period (with OnEditorBeforeSimulateStop) should use RequestStopSimulation. */
|
* a BT grace period (with OnEditorBeforeSimulateStop) should use RequestStopSimulation. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void StopSimulation();
|
void StopSimulation();
|
||||||
|
|
||||||
/** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules
|
/** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules
|
||||||
* the real StopSimulation after a grace period (500ms by default) so BT has time to
|
* the real StopSimulation after a grace period (500ms by default) so BT has time to
|
||||||
* react to whatever the BP handler does in OnEditorBeforeSimulateStop.
|
* react to whatever the BP handler does in OnEditorBeforeSimulateStop.
|
||||||
* Timeline Stop and the Simulate toolbar button both go through this. */
|
* Timeline Stop and the Simulate toolbar button both go through this.
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
*
|
||||||
void RequestStopSimulation();
|
* @param bImmediate If true, fire OnEditorBeforeSimulateStop and run StopSimulation
|
||||||
|
* synchronously in the same call (no grace period). Used by
|
||||||
|
* timeline Restart, where we want transforms restored before the
|
||||||
|
* subsequent Play(). Defaults to false (deferred). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
|
void RequestStopSimulation(bool bImmediate = false);
|
||||||
|
|
||||||
/** True while simulation is running. */
|
/** True while simulation is running. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
bool IsSimulating() const { return bSimulating; }
|
bool IsSimulating() const { return bSimulating; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +94,7 @@ public:
|
|||||||
* saved-transforms state. Used by the timeline's Pause so the characters freeze
|
* saved-transforms state. Used by the timeline's Pause so the characters freeze
|
||||||
* in place instead of snapping back to their pre-play positions.
|
* in place instead of snapping back to their pre-play positions.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void SetSimulationPaused(bool bPaused);
|
void SetSimulationPaused(bool bPaused);
|
||||||
|
|
||||||
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
||||||
@@ -98,7 +103,7 @@ public:
|
|||||||
* If empty, LevelName is used as-is (legacy behavior).
|
* If empty, LevelName is used as-is (legacy behavior).
|
||||||
* @param ExtraSublevels Additional sublevels to load alongside (e.g. always-loaded streaming levels).
|
* @param ExtraSublevels Additional sublevels to load alongside (e.g. always-loaded streaming levels).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath, const TArray<FString>& ExtraSublevels);
|
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath, const TArray<FString>& ExtraSublevels);
|
||||||
|
|
||||||
/** Overload without extra sublevels. */
|
/** Overload without extra sublevels. */
|
||||||
@@ -106,7 +111,7 @@ public:
|
|||||||
|
|
||||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
|
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
|
||||||
|
|
||||||
/** Enter point-placement mode for any actor implementing IPS_Editor_PointPlaceable. */
|
/** Enter point-placement mode for any actor implementing IPS_Editor_PointPlaceable. */
|
||||||
@@ -138,6 +143,7 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
virtual void Tick(float DeltaTime) override;
|
virtual void Tick(float DeltaTime) override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
|
|
||||||
@@ -175,6 +181,12 @@ private:
|
|||||||
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
|
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
|
||||||
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
|
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
|
||||||
|
|
||||||
|
/** Saved exported-text values for editable properties (UPS_Editor_EditableComponent::EditableProperties)
|
||||||
|
* that aren't already managed by the timeline. Game logic may mutate these during simulate;
|
||||||
|
* StopSimulation restores them so the editor returns to the pre-Play state for ALL editable
|
||||||
|
* properties, animated or not. Map: Actor -> { PropertyPath -> ExportedTextValue }. */
|
||||||
|
TMap<TWeakObjectPtr<AActor>, TMap<FName, FString>> SimulationSavedEditableProps;
|
||||||
|
|
||||||
/** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */
|
/** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */
|
||||||
FTimerHandle PendingStopSimulationHandle;
|
FTimerHandle PendingStopSimulationHandle;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ APS_Editor_Gizmo::APS_Editor_Gizmo()
|
|||||||
{
|
{
|
||||||
PrimaryActorTick.bCanEverTick = true;
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
|
||||||
|
// Hard-ref the gizmo base material at CDO-construction time so the cooker follows it
|
||||||
|
// into packaged builds. Replaces the old runtime LoadObject<> soft-string path.
|
||||||
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GizmoBaseMatFinder(
|
||||||
|
TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||||
|
if (GizmoBaseMatFinder.Succeeded())
|
||||||
|
{
|
||||||
|
GizmoBaseMaterial = GizmoBaseMatFinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
GizmoRoot = CreateDefaultSubobject<USceneComponent>(TEXT("GizmoRoot"));
|
GizmoRoot = CreateDefaultSubobject<USceneComponent>(TEXT("GizmoRoot"));
|
||||||
SetRootComponent(GizmoRoot);
|
SetRootComponent(GizmoRoot);
|
||||||
|
|
||||||
@@ -92,9 +101,8 @@ void APS_Editor_Gizmo::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
// Load gizmo material
|
// GizmoBaseMaterial is resolved in the ctor via ConstructorHelpers (hard cook ref).
|
||||||
UMaterialInterface* GizmoBaseMat = LoadObject<UMaterialInterface>(
|
UMaterialInterface* GizmoBaseMat = GizmoBaseMaterial.Get();
|
||||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
|
||||||
|
|
||||||
auto MakeColorMat = [&](const FLinearColor& Color) -> UMaterialInstanceDynamic*
|
auto MakeColorMat = [&](const FLinearColor& Color) -> UMaterialInstanceDynamic*
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
class UStaticMeshComponent;
|
class UStaticMeshComponent;
|
||||||
class UProceduralMeshComponent;
|
class UProceduralMeshComponent;
|
||||||
class UMaterialInstanceDynamic;
|
class UMaterialInstanceDynamic;
|
||||||
|
class UMaterialInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom runtime transform gizmo for PS_Editor.
|
* Custom runtime transform gizmo for PS_Editor.
|
||||||
@@ -109,6 +110,12 @@ private:
|
|||||||
TObjectPtr<UStaticMeshComponent> ScaleCube_Center;
|
TObjectPtr<UStaticMeshComponent> ScaleCube_Center;
|
||||||
|
|
||||||
// ---- Materials ----
|
// ---- Materials ----
|
||||||
|
|
||||||
|
/** Base gizmo material (from /PS_Editor/M_PS_Editor_Gizmo). Hard-ref so the cooker
|
||||||
|
* picks it up in packaged builds. Resolved via ConstructorHelpers in the ctor. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInterface> GizmoBaseMaterial;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class PS_Editor : ModuleRules
|
|||||||
"JsonUtilities",
|
"JsonUtilities",
|
||||||
"ProceduralMeshComponent",
|
"ProceduralMeshComponent",
|
||||||
"DesktopPlatform",
|
"DesktopPlatform",
|
||||||
"AIModule"
|
"AIModule",
|
||||||
|
"NavigationSystem"
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#include "PS_Editor_GroundSnap.h"
|
||||||
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "CollisionQueryParams.h"
|
||||||
|
|
||||||
|
namespace PS_Editor_GroundSnap
|
||||||
|
{
|
||||||
|
// Normal.Z threshold that classifies a hit surface as "floor-like" (roughly walkable
|
||||||
|
// slope — cos(72°) ≈ 0.3). Below this, we treat the surface as a wall / ceiling /
|
||||||
|
// overhang and keep searching.
|
||||||
|
static constexpr float GFloorNormalThreshold = 0.3f;
|
||||||
|
|
||||||
|
/** Single downward trace that returns the first floor-like hit. Returns false if
|
||||||
|
* nothing below qualifies (all walls, ceiling-like geometry, etc.). */
|
||||||
|
static bool FindFloorBelow(UWorld* World, const FVector& Above, float Distance,
|
||||||
|
const FCollisionQueryParams& Params, FVector& OutImpact)
|
||||||
|
{
|
||||||
|
TArray<FHitResult> Hits;
|
||||||
|
if (!World->LineTraceMultiByChannel(Hits, Above, Above - FVector::UpVector * Distance,
|
||||||
|
ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const FHitResult& H : Hits)
|
||||||
|
{
|
||||||
|
if (H.ImpactNormal.Z >= GFloorNormalThreshold)
|
||||||
|
{
|
||||||
|
OutImpact = H.ImpactPoint;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector ProjectCameraRayToGround(UWorld* World,
|
||||||
|
const FVector& Start,
|
||||||
|
const FVector& Direction,
|
||||||
|
const TArray<AActor*>& IgnoreActors,
|
||||||
|
float MaxCameraDistance,
|
||||||
|
float DefaultDistance,
|
||||||
|
float DownTraceDistance)
|
||||||
|
{
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
return Start + Direction * DefaultDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(PS_Editor_GroundSnap), false);
|
||||||
|
for (AActor* A : IgnoreActors)
|
||||||
|
{
|
||||||
|
if (A) Params.AddIgnoredActor(A);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Primary camera-aligned trace.
|
||||||
|
FHitResult Hit;
|
||||||
|
if (World->LineTraceSingleByChannel(Hit, Start,
|
||||||
|
Start + Direction.GetSafeNormal() * MaxCameraDistance,
|
||||||
|
ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
const float NormalZ = Hit.ImpactNormal.Z;
|
||||||
|
|
||||||
|
// Ceiling / overhang: normal points DOWN. Re-cast downward from a point just
|
||||||
|
// BELOW the ceiling impact (not on the surface itself — avoids re-hitting the
|
||||||
|
// backface due to floating-point slop).
|
||||||
|
if (NormalZ <= -0.3f)
|
||||||
|
{
|
||||||
|
const FVector StartBelowCeiling = Hit.ImpactPoint - FVector::UpVector * 2.0f;
|
||||||
|
FVector FloorImpact;
|
||||||
|
if (FindFloorBelow(World, StartBelowCeiling, DownTraceDistance, Params, FloorImpact))
|
||||||
|
{
|
||||||
|
return FloorImpact;
|
||||||
|
}
|
||||||
|
return Hit.ImpactPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wall: roughly horizontal normal. Step BACK along the wall normal (away from
|
||||||
|
// the surface) by a small distance so the placed object doesn't end up with its
|
||||||
|
// bbox half-buried in the wall, then cast down to find the floor at the foot
|
||||||
|
// of the wall. Without this, clicking on a wall drops the object inside it.
|
||||||
|
if (NormalZ < 0.3f) // not floor-like either
|
||||||
|
{
|
||||||
|
constexpr float WallStandoff = 30.0f;
|
||||||
|
const FVector StartAwayFromWall = Hit.ImpactPoint + Hit.ImpactNormal * WallStandoff;
|
||||||
|
FVector FloorImpact;
|
||||||
|
if (FindFloorBelow(World, StartAwayFromWall, DownTraceDistance, Params, FloorImpact))
|
||||||
|
{
|
||||||
|
return FloorImpact;
|
||||||
|
}
|
||||||
|
// No floor below — at least return the stood-off point so we're not inside
|
||||||
|
// the wall.
|
||||||
|
return StartAwayFromWall;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floor-like: use impact directly.
|
||||||
|
return Hit.ImpactPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Camera missed. Aim DOWN from a fallback point in front of the camera so the
|
||||||
|
// object still lands on ground instead of hovering in the sky.
|
||||||
|
const FVector FallbackTarget = Start + Direction.GetSafeNormal() * DefaultDistance;
|
||||||
|
FVector FloorImpact;
|
||||||
|
if (FindFloorBelow(World, FallbackTarget, DownTraceDistance, Params, FloorImpact))
|
||||||
|
{
|
||||||
|
return FloorImpact;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Nothing at all — give up and return the in-front point.
|
||||||
|
return FallbackTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SnapActorToGround(AActor* Actor,
|
||||||
|
FVector& OutLocation,
|
||||||
|
float OverheadRaise,
|
||||||
|
float MaxDistance,
|
||||||
|
const TArray<AActor*>& ExtraIgnored)
|
||||||
|
{
|
||||||
|
if (!Actor || !Actor->GetWorld())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure the bbox bottom relative to the pivot.
|
||||||
|
//
|
||||||
|
// Primary path — colliding components only: a Character's capsule drives the
|
||||||
|
// calculation, ignoring the skeletal mesh (often with collision disabled) or any
|
||||||
|
// attached weapons / child actors whose meshes might extend below the capsule and
|
||||||
|
// artificially lift the pawn by that overflow. Formula: pivot is placed so the
|
||||||
|
// collision-bbox bottom rests on the ground.
|
||||||
|
//
|
||||||
|
// Fallback — no colliding components found: the actor is treated as a pure visual
|
||||||
|
// marker (VR start zone, placement helper, or a BP that enables collision only
|
||||||
|
// when entering editor mode and hasn't done so yet). For these, we don't trust
|
||||||
|
// the full-bounds bbox (it may wrap helper meshes far below the pivot and push
|
||||||
|
// the object absurdly high). Instead we anchor the PIVOT directly on the ground —
|
||||||
|
// which is what the designer authored the pivot for on these markers.
|
||||||
|
FVector BoundsOrigin, BoundsExtent;
|
||||||
|
Actor->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent);
|
||||||
|
const bool bUsedFallback = BoundsExtent.IsNearlyZero();
|
||||||
|
if (bUsedFallback)
|
||||||
|
{
|
||||||
|
// Read the full bounds too — only to know where "above the actor" is for the
|
||||||
|
// ray start position, not to anchor the snap.
|
||||||
|
Actor->GetActorBounds(false, BoundsOrigin, BoundsExtent);
|
||||||
|
}
|
||||||
|
const float ActorZ = Actor->GetActorLocation().Z;
|
||||||
|
const float BBoxBottomZ = BoundsOrigin.Z - BoundsExtent.Z;
|
||||||
|
const float BBoxTopZ = BoundsOrigin.Z + BoundsExtent.Z;
|
||||||
|
const float PivotAboveBottom = bUsedFallback ? 0.0f : (ActorZ - BBoxBottomZ);
|
||||||
|
|
||||||
|
float ExtraOffset = 0.0f;
|
||||||
|
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||||
|
{
|
||||||
|
ExtraOffset = SpawnComp->GroundOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start INSIDE the actor (at its pivot + a small buffer), not above its bbox top.
|
||||||
|
// Why: if we started above the bbox top and the actor was raised even slightly into
|
||||||
|
// a ceiling, the ray would cross the ceiling and the FIRST floor-like hit (filtered
|
||||||
|
// by normal.Z >= 0.3) would be the CEILING'S TOP FACE — i.e. the floor of the
|
||||||
|
// upper story. That made "End" teleport the actor up to the next story when the
|
||||||
|
// user only nudged it a bit too high.
|
||||||
|
//
|
||||||
|
// Starting at the pivot keeps the ray origin within the actor's own volume (pivot
|
||||||
|
// is always inside or at the edge of the bbox for normal meshes / characters). The
|
||||||
|
// actor is in the ignore list, so the trace passes straight through its own body
|
||||||
|
// and finds the first floor-like surface UNDERNEATH the actor — i.e. the floor of
|
||||||
|
// its current room.
|
||||||
|
//
|
||||||
|
// OverheadRaise is unused now (kept for API compat); the 2.0f buffer just avoids
|
||||||
|
// a degenerate "ray starts exactly on ground surface" edge case when the pivot
|
||||||
|
// coincides with the ground.
|
||||||
|
(void)BBoxTopZ; (void)OverheadRaise;
|
||||||
|
const FVector Start = Actor->GetActorLocation() + FVector(0.f, 0.f, 2.0f);
|
||||||
|
const FVector End = Start - FVector::UpVector * MaxDistance;
|
||||||
|
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(PS_Editor_SnapActorToGround), false);
|
||||||
|
Params.AddIgnoredActor(Actor);
|
||||||
|
for (AActor* A : ExtraIgnored)
|
||||||
|
{
|
||||||
|
if (A) Params.AddIgnoredActor(A);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-hit trace + filter by normal so we only snap to floor-like surfaces.
|
||||||
|
// Accepts slopes up to ~72 degrees (normal.Z > 0.3). Rejects walls, ceilings,
|
||||||
|
// overhangs, and any backface we might cross on the way down.
|
||||||
|
TArray<FHitResult> Hits;
|
||||||
|
if (!Actor->GetWorld()->LineTraceMultiByChannel(Hits, Start, End, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float FloorNormalThreshold = 0.3f; // cos(~72 deg)
|
||||||
|
for (const FHitResult& H : Hits)
|
||||||
|
{
|
||||||
|
if (H.ImpactNormal.Z >= FloorNormalThreshold)
|
||||||
|
{
|
||||||
|
OutLocation = Actor->GetActorLocation();
|
||||||
|
OutLocation.Z = H.ImpactPoint.Z + PivotAboveBottom + ExtraOffset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trace hit stuff but nothing floor-like — don't move the actor.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
class AActor;
|
||||||
|
class UWorld;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ground-placement helpers used across catalogue spawn, End-key snap, and spline point
|
||||||
|
* placement. The goal is that every object the user drops / snaps ends up on the ground
|
||||||
|
* by default, even when the camera is pointing at the sky.
|
||||||
|
*/
|
||||||
|
namespace PS_Editor_GroundSnap
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Project a camera-style ray onto ground.
|
||||||
|
*
|
||||||
|
* Sequence:
|
||||||
|
* 1. Line-trace from Start along Direction for up to MaxCameraDistance.
|
||||||
|
* - Hit a surface whose normal points UP / sideways -> return ImpactPoint.
|
||||||
|
* - Hit a surface whose normal points DOWN (ceiling, overhang) -> re-cast straight
|
||||||
|
* down from the impact point so the object lands on the floor underneath the
|
||||||
|
* overhead surface instead of glued to the ceiling.
|
||||||
|
* 2. Camera ray missed entirely (user aiming at the sky). Fall back to a point at
|
||||||
|
* DefaultDistance in front of the camera, then cast DOWN from there for
|
||||||
|
* DownTraceDistance. If THAT hits, return the downward impact.
|
||||||
|
* 3. Still nothing (infinite drop) -> return the in-front fallback unmodified so the
|
||||||
|
* caller at least gets a point in world space instead of a zero vector.
|
||||||
|
*
|
||||||
|
* @param IgnoreActors Actors the line trace must skip (typically the player pawn and,
|
||||||
|
* for re-snapping an existing actor, the actor itself — otherwise
|
||||||
|
* the trace hits its own mesh and nothing moves).
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API FVector ProjectCameraRayToGround(UWorld* World,
|
||||||
|
const FVector& Start,
|
||||||
|
const FVector& Direction,
|
||||||
|
const TArray<AActor*>& IgnoreActors = {},
|
||||||
|
float MaxCameraDistance = 20000.0f,
|
||||||
|
float DefaultDistance = 1500.0f,
|
||||||
|
float DownTraceDistance = 50000.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a world position such that Actor's bounding-box bottom lands on the ground
|
||||||
|
* directly under its current XY position. Optionally adds a SpawnableComponent's
|
||||||
|
* GroundOffset for props that look better floating slightly (rugs, characters, etc.).
|
||||||
|
*
|
||||||
|
* Properly handles pivots not at bbox center (e.g. characters with pivot at feet) by
|
||||||
|
* measuring the pivot-to-bbox-bottom delta from GetActorBounds.
|
||||||
|
*
|
||||||
|
* The downward ray starts AT the actor's pivot (+ small buffer), not above its bbox.
|
||||||
|
* This keeps the trace origin within the current room (the pivot is always inside or
|
||||||
|
* on the edge of the actor's own volume) so the snap lands on the floor the actor is
|
||||||
|
* currently in, even when the actor has been nudged slightly into a ceiling. A multi-
|
||||||
|
* hit trace + a normal.Z >= 0.3 floor filter skips walls, ceilings, and backfaces
|
||||||
|
* encountered on the way down.
|
||||||
|
*
|
||||||
|
* OverheadRaise is kept for API compatibility but is no longer used — starting above
|
||||||
|
* the bbox was exactly what caused "End teleports to the upper story" bugs.
|
||||||
|
*
|
||||||
|
* @return true + OutLocation filled when ground was found; false when nothing is below.
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API bool SnapActorToGround(AActor* Actor,
|
||||||
|
FVector& OutLocation,
|
||||||
|
float OverheadRaise = 50.0f,
|
||||||
|
float MaxDistance = 50000.0f,
|
||||||
|
const TArray<AActor*>& ExtraIgnored = {});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_GroundSnap.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
@@ -194,7 +195,7 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
|||||||
Spline->SetSelected(true);
|
Spline->SetSelected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
||||||
@@ -234,7 +235,7 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +287,7 @@ void UPS_Editor_SelectionManager::ClearSelection()
|
|||||||
|
|
||||||
if (Count > 0)
|
if (Count > 0)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +449,11 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
TSharedPtr<FPS_Editor_TransformAction> UndoAction = MakeShared<FPS_Editor_TransformAction>();
|
TSharedPtr<FPS_Editor_TransformAction> UndoAction = MakeShared<FPS_Editor_TransformAction>();
|
||||||
UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num());
|
UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num());
|
||||||
|
|
||||||
|
// Extra ignores shared across every actor in the batch.
|
||||||
|
TArray<AActor*> ExtraIgnored;
|
||||||
|
if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);
|
||||||
|
if (Gizmo) ExtraIgnored.Add(Gizmo);
|
||||||
|
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
{
|
{
|
||||||
AActor* Actor = Weak.Get();
|
AActor* Actor = Weak.Get();
|
||||||
@@ -456,36 +462,11 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
UndoAction->Actors.Add(Actor);
|
UndoAction->Actors.Add(Actor);
|
||||||
UndoAction->OldTransforms.Add(Actor->GetActorTransform());
|
UndoAction->OldTransforms.Add(Actor->GetActorTransform());
|
||||||
|
|
||||||
FVector Origin, Extent;
|
FVector NewLocation;
|
||||||
Actor->GetActorBounds(false, Origin, Extent);
|
if (PS_Editor_GroundSnap::SnapActorToGround(Actor, NewLocation,
|
||||||
float BottomOffset = Extent.Z;
|
/*OverheadRaise*/ 50.0f, /*MaxDistance*/ 50000.0f, ExtraIgnored))
|
||||||
|
|
||||||
// Add custom ground offset from SpawnableComponent if available
|
|
||||||
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
|
||||||
{
|
{
|
||||||
BottomOffset += SpawnComp->GroundOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVector Start = Actor->GetActorLocation();
|
|
||||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
|
||||||
|
|
||||||
FHitResult Hit;
|
|
||||||
FCollisionQueryParams Params;
|
|
||||||
Params.AddIgnoredActor(Actor);
|
|
||||||
Params.AddIgnoredActor(PC->GetPawn());
|
|
||||||
if (Gizmo) Params.AddIgnoredActor(Gizmo);
|
|
||||||
|
|
||||||
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
|
|
||||||
{
|
|
||||||
FVector NewLocation = Hit.ImpactPoint;
|
|
||||||
NewLocation.Z += BottomOffset;
|
|
||||||
Actor->SetActorLocation(NewLocation);
|
Actor->SetActorLocation(NewLocation);
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround '%s' -> Hit '%s' (%s) at Z=%.1f, BottomOffset=%.1f, NewZ=%.1f"),
|
|
||||||
*Actor->GetName(),
|
|
||||||
*Hit.GetActor()->GetName(),
|
|
||||||
*Hit.GetComponent()->GetName(),
|
|
||||||
Hit.ImpactPoint.Z, BottomOffset, NewLocation.Z);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ public:
|
|||||||
|
|
||||||
FPS_Editor_OnSelectionChanged OnSelectionChanged;
|
FPS_Editor_OnSelectionChanged OnSelectionChanged;
|
||||||
|
|
||||||
|
/** Update gizmo visibility, position, orientation, and axis constraints from the
|
||||||
|
* current selection. Called on every selection mutation, and per-tick during
|
||||||
|
* simulation so the gizmo tracks animated pawns. */
|
||||||
|
void UpdateGizmo();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
@@ -127,7 +132,4 @@ private:
|
|||||||
|
|
||||||
/** Clean up any stale weak pointers in the selection array. */
|
/** Clean up any stale weak pointers in the selection array. */
|
||||||
void CleanupStaleReferences();
|
void CleanupStaleReferences();
|
||||||
|
|
||||||
/** Update gizmo visibility and position based on current selection. */
|
|
||||||
void UpdateGizmo();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ void FPS_Editor_SplinePointAction::Execute()
|
|||||||
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
||||||
{
|
{
|
||||||
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints);
|
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints);
|
||||||
|
// Apply rotations only if the BP captured them (otherwise we'd reset to identity).
|
||||||
|
if (NewRotations.Num() > 0)
|
||||||
|
{
|
||||||
|
IPS_Editor_ChildMovable::Execute_SetAllChildRotations(SplineActor.Get(), NewRotations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +30,10 @@ void FPS_Editor_SplinePointAction::Undo()
|
|||||||
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
|
||||||
{
|
{
|
||||||
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints);
|
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints);
|
||||||
|
if (OldRotations.Num() > 0)
|
||||||
|
{
|
||||||
|
IPS_Editor_ChildMovable::Execute_SetAllChildRotations(SplineActor.Get(), OldRotations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +98,7 @@ void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
|||||||
|
|
||||||
PruneHistory();
|
PruneHistory();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
||||||
*Action->GetDescription(), ActionStack.Num());
|
*Action->GetDescription(), ActionStack.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ struct FPS_Editor_SplinePointAction : public FPS_Editor_Action
|
|||||||
TWeakObjectPtr<AActor> SplineActor;
|
TWeakObjectPtr<AActor> SplineActor;
|
||||||
TArray<FVector> OldPoints;
|
TArray<FVector> OldPoints;
|
||||||
TArray<FVector> NewPoints;
|
TArray<FVector> NewPoints;
|
||||||
|
// ChildMovable-only: rotation snapshots. Empty for spline points and for ChildMovable BPs
|
||||||
|
// that don't implement rotation. Execute/Undo only apply rotations when both arrays match
|
||||||
|
// the points array length.
|
||||||
|
TArray<FRotator> OldRotations;
|
||||||
|
TArray<FRotator> NewRotations;
|
||||||
FString Description;
|
FString Description;
|
||||||
|
|
||||||
virtual void Execute() override;
|
virtual void Execute() override;
|
||||||
|
|||||||
@@ -61,4 +61,17 @@ struct FPS_Editor_SceneData
|
|||||||
/** Global timeline data (animation tracks + duration). Empty on legacy scenes. */
|
/** Global timeline data (animation tracks + duration). Empty on legacy scenes. */
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FPS_Editor_TimelineData Timeline;
|
FPS_Editor_TimelineData Timeline;
|
||||||
|
|
||||||
|
/** If true, the level's nav mesh is switched to Dynamic runtime generation when this
|
||||||
|
* scenario loads — both in the runtime editor (so spawn / move actors update nav live)
|
||||||
|
* and in gameplay (for scenarios that need runtime-built navigation). False keeps the
|
||||||
|
* cooked Static nav, which has zero runtime cost. Default: false. */
|
||||||
|
UPROPERTY()
|
||||||
|
bool bUseDynamicNavigation = false;
|
||||||
|
|
||||||
|
/** Project-specific scenario type as a uint8 (index into UPS_Editor_MasterCatalog::ScenarioTypeEnum).
|
||||||
|
* Selected by the user via the Settings popup dropdown. Defaults to 0 (= first enum entry) for
|
||||||
|
* legacy scenarios that were saved before this field existed. */
|
||||||
|
UPROPERTY()
|
||||||
|
uint8 ScenarioType = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
#include "BrainComponent.h"
|
#include "BrainComponent.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
@@ -48,11 +49,13 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
|
TArray<TWeakObjectPtr<AActor>> UPS_Editor_SceneLoader::TrackedSpawnedActors;
|
||||||
|
|
||||||
|
bool UPS_Editor_SceneLoader::LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
|
||||||
{
|
{
|
||||||
OutActors.Empty();
|
OutActors.Empty();
|
||||||
|
|
||||||
if (!WorldContextObject || SceneName.IsEmpty())
|
if (!WorldContextObject || ScenarioName.IsEmpty())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -64,7 +67,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read JSON file
|
// Read JSON file
|
||||||
const FString FilePath = GetSceneFilePath(SceneName);
|
const FString FilePath = GetSceneFilePath(ScenarioName);
|
||||||
FString JsonString;
|
FString JsonString;
|
||||||
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
|
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
|
||||||
{
|
{
|
||||||
@@ -80,11 +83,22 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LoadSceneFromData(World, SceneData, OutActors, Filter))
|
if (!LoadScenarioFromData(World, SceneData, OutActors, Filter))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track every spawned actor so UnloadScenario() can dispose of them later — this is the
|
||||||
|
// briefing-flow hook (load preload markers, then unload on scenario change). Gameplay
|
||||||
|
// callers that never unload don't pay a price beyond a small TWeakObjectPtr array.
|
||||||
|
for (AActor* Actor : OutActors)
|
||||||
|
{
|
||||||
|
if (Actor)
|
||||||
|
{
|
||||||
|
TrackedSpawnedActors.Add(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Strip editor-only components for clean gameplay
|
// Strip editor-only components for clean gameplay
|
||||||
if (bStripEditorComponents)
|
if (bStripEditorComponents)
|
||||||
{
|
{
|
||||||
@@ -93,11 +107,6 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
|
|||||||
if (!Actor) continue;
|
if (!Actor) continue;
|
||||||
|
|
||||||
TArray<UActorComponent*> ToRemove;
|
TArray<UActorComponent*> ToRemove;
|
||||||
for (UActorComponent* Comp : Actor->GetComponentsByInterface(UPS_Editor_PointPlaceable::StaticClass()))
|
|
||||||
{
|
|
||||||
// Don't remove — PointPlaceable is on the actor itself, not a component
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UActorComponent* Spawnable = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
if (UActorComponent* Spawnable = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||||
{
|
{
|
||||||
ToRemove.Add(Spawnable);
|
ToRemove.Add(Spawnable);
|
||||||
@@ -123,14 +132,54 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
|
int32 UPS_Editor_SceneLoader::UnloadScenario(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
int32 Destroyed = 0;
|
||||||
|
for (TWeakObjectPtr<AActor>& Weak : TrackedSpawnedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
++Destroyed;
|
||||||
|
}
|
||||||
|
// else: already gone (GC, manual destroy, level reload) — skip silently
|
||||||
|
}
|
||||||
|
TrackedSpawnedActors.Reset();
|
||||||
|
|
||||||
|
if (Destroyed > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: UnloadScenario destroyed %d actor(s)"), Destroyed);
|
||||||
|
}
|
||||||
|
return Destroyed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SceneLoader::LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
|
||||||
{
|
{
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UPS_Editor_TimelineSubsystem* TimelineSS = World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
|
// Defensive: reaching the gameplay-side SceneLoader means we are NOT in the runtime
|
||||||
|
// editor (the editor uses UPS_Editor_SceneSerializer instead). Force the static flag
|
||||||
|
// false now so subsequent IsInEditorMode() checks reflect reality even if a previous
|
||||||
|
// PIE session left the static at 'true' — PIE keeps C++ statics across stop/start, and
|
||||||
|
// a forced PIE-stop or BP-driven OpenLevel can skip the EndPlay reset on the editor PC.
|
||||||
|
bIsEditorModeActive = false;
|
||||||
|
|
||||||
|
// Apply the scenario's dynamic-navigation flag BEFORE spawning actors. If the scenario
|
||||||
|
// needs runtime nav generation, switching the level's RecastNavMesh to Dynamic now
|
||||||
|
// means the actors we're about to spawn will dirty + rebuild tiles around them. Cooked
|
||||||
|
// Static nav is preserved on disk for scenarios that don't need this.
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(World, SceneData.bUseDynamicNavigation);
|
||||||
|
|
||||||
|
// PreloadOnly is the briefing path: only player-placement markers / briefing props
|
||||||
|
// should spawn, and the briefing scene runs its OWN timeline (don't push the scenario's
|
||||||
|
// timeline data or auto-play it). Skip the timeline subsystem entirely in that case.
|
||||||
|
const bool bSkipTimeline = (Filter == EPS_Editor_SceneLoadFilter::PreloadOnly);
|
||||||
|
UPS_Editor_TimelineSubsystem* TimelineSS = bSkipTimeline
|
||||||
|
? nullptr
|
||||||
|
: World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
|
||||||
|
|
||||||
int32 SpawnedCount = 0;
|
int32 SpawnedCount = 0;
|
||||||
|
|
||||||
@@ -140,7 +189,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
|
|||||||
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
|
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
|
||||||
if (SpawnedActor)
|
if (SpawnedActor)
|
||||||
{
|
{
|
||||||
// Register the stable ID so timeline tracks can find this actor
|
// Register the stable ID so timeline tracks can find this actor.
|
||||||
|
// Only when timeline is active for this load (PreloadOnly skips the subsystem).
|
||||||
if (TimelineSS && ActorData.ActorId.IsValid())
|
if (TimelineSS && ActorData.ActorId.IsValid())
|
||||||
{
|
{
|
||||||
TimelineSS->RegisterActorId(SpawnedActor, ActorData.ActorId);
|
TimelineSS->RegisterActorId(SpawnedActor, ActorData.ActorId);
|
||||||
@@ -206,8 +256,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actors from scene '%s'"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actor(s) from scenario '%s' [filter=%d]"),
|
||||||
SpawnedCount, *SceneData.SceneName);
|
SpawnedCount, *SceneData.SceneName, (int32)Filter);
|
||||||
return SpawnedCount > 0;
|
return SpawnedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
#include "PS_Editor_SceneLoader.generated.h"
|
#include "PS_Editor_SceneLoader.generated.h"
|
||||||
|
|
||||||
/** Filter for selective scene loading. */
|
/** Filter for selective scenario loading. */
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EPS_Editor_SceneLoadFilter : uint8
|
enum class EPS_Editor_SceneLoadFilter : uint8
|
||||||
{
|
{
|
||||||
@@ -15,20 +15,27 @@ enum class EPS_Editor_SceneLoadFilter : uint8
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standalone scene loader for PS_Editor.
|
* Standalone scenario loader for PS_Editor.
|
||||||
* Loads a JSON scene file and spawns all actors with their properties and spline data.
|
* A "scenario" is a .pss file describing actors + properties + timeline saved by the runtime
|
||||||
|
* editor. This loader spawns those actors into an already-loaded UE level — it does NOT
|
||||||
|
* change the current UE level. Spawning is additive: existing world content is preserved.
|
||||||
*
|
*
|
||||||
* This is the entry point for GAME MODE usage — it does NOT require
|
* This is the entry point for GAME MODE usage — it does NOT require
|
||||||
* PS_Editor's PlayerController, SelectionManager, Gizmo, or UI.
|
* PS_Editor's PlayerController, SelectionManager, Gizmo, or UI.
|
||||||
* Just call LoadScene() from your GameMode's BeginPlay.
|
* Just call LoadScenario() from your GameMode's BeginPlay.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* void AMyGameMode::BeginPlay()
|
* void AMyGameMode::BeginPlay()
|
||||||
* {
|
* {
|
||||||
* Super::BeginPlay();
|
* Super::BeginPlay();
|
||||||
* TArray<AActor*> SpawnedActors;
|
* TArray<AActor*> SpawnedActors;
|
||||||
* UPS_Editor_SceneLoader::LoadScene(GetWorld(), TEXT("MyScene"), SpawnedActors);
|
* UPS_Editor_SceneLoader::LoadScenario(GetWorld(), TEXT("MyScenario"), SpawnedActors);
|
||||||
* }
|
* }
|
||||||
|
*
|
||||||
|
* Briefing usage (player placement markers only, no timeline animation):
|
||||||
|
* UPS_Editor_SceneLoader::UnloadScenario(this);
|
||||||
|
* UPS_Editor_SceneLoader::LoadScenario(this, TEXT("MyScenario"), Out, true,
|
||||||
|
* EPS_Editor_SceneLoadFilter::PreloadOnly);
|
||||||
*/
|
*/
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
|
class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
|
||||||
@@ -37,59 +44,74 @@ class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Load a scene from a JSON file and spawn all actors.
|
* Load a scenario from a .pss file and spawn its actors into the current world.
|
||||||
*
|
*
|
||||||
* @param World The world to spawn actors into.
|
* The call is additive — existing world content is preserved. Spawned actors are tracked
|
||||||
* @param SceneName Scene name (without .json extension).
|
* internally so UnloadScenario() can clean them up; this lets you switch scenarios at
|
||||||
* @param OutActors Filled with all spawned actors.
|
* runtime (typical briefing flow). When Filter == PreloadOnly, the timeline subsystem is
|
||||||
|
* NOT touched (no SetData, no auto-play, no ActorId registration) so a briefing scene's
|
||||||
|
* own timeline keeps running untouched.
|
||||||
|
*
|
||||||
|
* @param ScenarioName Scenario name (without .pss extension).
|
||||||
|
* @param OutActors Filled with all spawned actors (also tracked internally).
|
||||||
* @param bStripEditorComponents If true, removes SpawnableComponent and EditableComponent after spawn (cleaner for gameplay).
|
* @param bStripEditorComponents If true, removes SpawnableComponent and EditableComponent after spawn (cleaner for gameplay).
|
||||||
* @return True if the scene was loaded successfully.
|
* @param Filter Which actors to spawn. PreloadOnly = briefing-only, skips timeline setup.
|
||||||
|
* @return True if the scenario was loaded successfully.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors,
|
static bool LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors,
|
||||||
bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
|
bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load scene from an already-parsed SceneData struct.
|
* Destroy every actor previously spawned via LoadScenario(). No-op if nothing was loaded
|
||||||
* Useful if you want to parse the JSON yourself or modify data before spawning.
|
* or all tracked actors have already been GC'd / manually destroyed.
|
||||||
|
* Call this before LoadScenario() when switching scenarios.
|
||||||
|
* @return Number of actors destroyed.
|
||||||
*/
|
*/
|
||||||
static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
|
static int32 UnloadScenario(const UObject* WorldContextObject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load scenario actors from an already-parsed SceneData struct.
|
||||||
|
* Useful if you want to parse the .pss JSON yourself or modify data before spawning.
|
||||||
|
*/
|
||||||
|
static bool LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
|
||||||
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
|
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the directory where scenes are stored.
|
* Get the directory where scenarios are stored.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
static FString GetScenesDirectory();
|
static FString GetScenesDirectory();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full file path for a scene name.
|
* Get the full file path for a scenario name.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
static FString GetSceneFilePath(const FString& SceneName);
|
static FString GetSceneFilePath(const FString& SceneName);
|
||||||
|
|
||||||
/** Get the full file path for a scene name and its base level. Uses level-specific subdirectory. */
|
/** Get the full file path for a scenario name and its base level. Uses level-specific subdirectory. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName);
|
static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all saved scene names. Optionally filter by level name.
|
* Get all saved scenario names. Optionally filter by level name.
|
||||||
* @param LevelFilter If not empty, only return scenes associated with this level.
|
* @param LevelFilter If not empty, only return scenarios associated with this level.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT(""));
|
static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT(""));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the level name associated with a saved scene (reads the JSON header without spawning).
|
* Get the level name associated with a saved scenario (reads the JSON header without spawning).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
static FString GetSceneLevelName(const FString& SceneName);
|
static FString GetSceneLevelName(const FString& SceneName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true while a scene is being loaded (actors are being spawned from JSON).
|
* Returns true while a scenario is being loaded (actors are being spawned from JSON).
|
||||||
* Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead.
|
* Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
static bool IsLoadingFromScene(const AActor* Actor);
|
static bool IsLoadingFromScene(const AActor* Actor);
|
||||||
|
|
||||||
/** Static flag: true while SceneLoader or SceneSerializer is spawning actors. */
|
/** Static flag: true while SceneLoader or SceneSerializer is spawning actors. */
|
||||||
@@ -99,13 +121,13 @@ public:
|
|||||||
static bool bIsEditorModeActive;
|
static bool bIsEditorModeActive;
|
||||||
|
|
||||||
/** Returns true if the PS_Editor runtime editor is currently active. Callable from any BP. */
|
/** Returns true if the PS_Editor runtime editor is currently active. Callable from any BP. */
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
static bool IsInEditorMode() { return bIsEditorModeActive; }
|
static bool IsInEditorMode() { return bIsEditorModeActive; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all saved scenes for the current level.
|
* Get all saved scenarios for the current level.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
|
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -114,4 +136,8 @@ private:
|
|||||||
|
|
||||||
/** Apply custom editable properties to a spawned actor. */
|
/** Apply custom editable properties to a spawned actor. */
|
||||||
static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties);
|
static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties);
|
||||||
|
|
||||||
|
/** Actors spawned via LoadScenario(), tracked so UnloadScenario() can find them.
|
||||||
|
* TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */
|
||||||
|
static TArray<TWeakObjectPtr<AActor>> TrackedSpawnedActors;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
#include "PS_Editor_SplineEditable.h"
|
#include "PS_Editor_SplineEditable.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
@@ -110,7 +111,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
|
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
|
||||||
{
|
{
|
||||||
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
||||||
Spline->GetNumPoints(), *ActorData.ClassPath);
|
Spline->GetNumPoints(), *ActorData.ClassPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,13 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
SceneData.Timeline = TimelineSS->GetData();
|
SceneData.Timeline = TimelineSS->GetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-scenario settings — read from the SpawnManager's session state.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SceneData.bUseDynamicNavigation = SpawnManager->bUseDynamicNavigation;
|
||||||
|
SceneData.ScenarioType = SpawnManager->ScenarioType;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to JSON
|
// Convert to JSON
|
||||||
FString JsonString;
|
FString JsonString;
|
||||||
if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true))
|
if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true))
|
||||||
@@ -194,7 +202,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
bool UPS_Editor_SceneSerializer::LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
||||||
{
|
{
|
||||||
if (!SpawnManager || SceneName.IsEmpty())
|
if (!SpawnManager || SceneName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -438,6 +446,24 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-scenario settings — push into the SpawnManager's session state and apply
|
||||||
|
// the dynamic-navigation switch to the level's RecastNavMesh actors right away.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->bUseDynamicNavigation = SceneData.bUseDynamicNavigation;
|
||||||
|
SpawnManager->ScenarioType = SceneData.ScenarioType;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOuter()))
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(EditorPC->GetWorld(), SpawnManager->bUseDynamicNavigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add any mandatory actors missing from the loaded scene. Handles legacy saves
|
||||||
|
// that predate the bIsMandatory flag — the mandatory actor is auto-spawned at the
|
||||||
|
// baseLevel's MandatorySpawnAnchor. If all mandatory actors were already in the JSON,
|
||||||
|
// this is a no-op.
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -494,6 +520,14 @@ void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Mandatory-actor auto-spawn is intentionally NOT done here. ClearScene is called
|
||||||
|
// from three contexts: (1) "New Scenario" button (we want mandatory spawn — handled in the
|
||||||
|
// widget after ClearScene returns), (2) LoadScenario before re-spawning saved actors (we
|
||||||
|
// don't want it — the JSON itself contains the mandatory actor, and there's a final
|
||||||
|
// EnsureMandatoryActorsPresent at the end of LoadScenario that handles legacy saves), and
|
||||||
|
// (3) BaseLevel switch (we don't want it — the new baseLevel isn't loaded yet, the
|
||||||
|
// MandatoryAnchor isn't in the world; the post-OnLevelShown hook handles that).
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const
|
FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ public:
|
|||||||
/** Save the current spawned actors to a JSON file. */
|
/** Save the current spawned actors to a JSON file. */
|
||||||
bool SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
bool SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
||||||
|
|
||||||
/** Load a scene from a JSON file. Destroys current spawned actors and re-spawns from file. */
|
/** Load a scenario from a .pss file. Destroys current spawned actors and re-spawns from file. */
|
||||||
bool LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
bool LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
||||||
|
|
||||||
/** Get a list of all saved scene names (without .json extension). */
|
/** Get a list of all saved scene names (without .json extension). */
|
||||||
TArray<FString> GetSavedSceneNames() const;
|
TArray<FString> GetSavedSceneNames() const;
|
||||||
|
|||||||
@@ -25,47 +25,67 @@ class PS_EDITOR_API IPS_Editor_ChildMovable
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/** Number of movable sub-elements. */
|
/** Number of movable sub-elements. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
int32 GetMovableChildCount() const;
|
int32 GetMovableChildCount() const;
|
||||||
|
|
||||||
/** World location of the sub-element at the given index. */
|
/** World location of the sub-element at the given index. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
FVector GetChildLocation(int32 Index) const;
|
FVector GetChildLocation(int32 Index) const;
|
||||||
|
|
||||||
/** Move the sub-element at the given index to a new world position. */
|
/** Move the sub-element at the given index to a new world position. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void MoveChild(int32 Index, const FVector& NewWorldPosition);
|
void MoveChild(int32 Index, const FVector& NewWorldPosition);
|
||||||
|
|
||||||
/** Return all sub-element positions (used for undo snapshots). */
|
/** Return all sub-element positions (used for undo snapshots). */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
TArray<FVector> GetAllChildLocations() const;
|
TArray<FVector> GetAllChildLocations() const;
|
||||||
|
|
||||||
/** Restore all sub-element positions (used for undo). */
|
/** Restore all sub-element positions (used for undo). */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void SetAllChildLocations(const TArray<FVector>& Positions);
|
void SetAllChildLocations(const TArray<FVector>& Positions);
|
||||||
|
|
||||||
|
/** World rotation of the sub-element at the given index. Implement to constrain the
|
||||||
|
* rotation axes you actually expose (e.g. yaw only) — the plugin gizmo writes whatever
|
||||||
|
* the user drags, so any clamping must happen in RotateChild on the BP side. */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
|
FRotator GetChildRotation(int32 Index) const;
|
||||||
|
|
||||||
|
/** Apply a new world rotation to the sub-element at the given index. The BP impl is the
|
||||||
|
* right place to drop unsupported axes (e.g. keep only Yaw, force Pitch/Roll to zero). */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
|
void RotateChild(int32 Index, const FRotator& NewWorldRotation);
|
||||||
|
|
||||||
|
/** Return all sub-element rotations (used for undo snapshots). Empty array allowed if
|
||||||
|
* rotation is not supported on this actor — the undo system then ignores rotation. */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
|
TArray<FRotator> GetAllChildRotations() const;
|
||||||
|
|
||||||
|
/** Restore all sub-element rotations (used for undo). */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
|
void SetAllChildRotations(const TArray<FRotator>& Rotations);
|
||||||
|
|
||||||
/** Index of the sub-element under the cursor ray (-1 if none).
|
/** Index of the sub-element under the cursor ray (-1 if none).
|
||||||
* Implement with sphere-ray intersection or any hit detection that suits your visuals. */
|
* Implement with sphere-ray intersection or any hit detection that suits your visuals. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
|
int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
|
||||||
|
|
||||||
/** Show or hide visual handles for the sub-elements. */
|
/** Show or hide visual handles for the sub-elements. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void SetChildHandlesVisible(bool bVisible);
|
void SetChildHandlesVisible(bool bVisible);
|
||||||
|
|
||||||
/** Highlight a specific sub-element (e.g. change handle color). */
|
/** Highlight a specific sub-element (e.g. change handle color). */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void HighlightChild(int32 Index);
|
void HighlightChild(int32 Index);
|
||||||
|
|
||||||
/** Clear all sub-element highlights. */
|
/** Clear all sub-element highlights. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void ClearChildHighlight();
|
void ClearChildHighlight();
|
||||||
|
|
||||||
/** Export sub-element positions to the CustomProperties map for scene saving. */
|
/** Export sub-element positions to the CustomProperties map for scene saving. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void ExportChildLocations(TMap<FString, FString>& OutProperties) const;
|
void ExportChildLocations(TMap<FString, FString>& OutProperties) const;
|
||||||
|
|
||||||
/** Import sub-element positions from the CustomProperties map on scene load. */
|
/** Import sub-element positions from the CustomProperties map on scene load. */
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor|Child Movable")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
|
||||||
void ImportChildLocations(const TMap<FString, FString>& Properties);
|
void ImportChildLocations(const TMap<FString, FString>& Properties);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent&
|
|||||||
FPS_Editor_EditablePropertyEntry NewEntry;
|
FPS_Editor_EditablePropertyEntry NewEntry;
|
||||||
NewEntry.Path = FullPath;
|
NewEntry.Path = FullPath;
|
||||||
EditableProperties.Add(NewEntry);
|
EditableProperties.Add(NewEntry);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AddProperty = NAME_None; // Reset for next pick
|
AddProperty = NAME_None; // Reset for next pick
|
||||||
|
|||||||
@@ -15,25 +15,25 @@ struct FPS_Editor_EditablePropertyEntry
|
|||||||
/**
|
/**
|
||||||
* Property path: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
* Property path: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
FName Path;
|
FName Path;
|
||||||
|
|
||||||
/** Display name override for the runtime editor UI. If empty, the property name is used. */
|
/** Display name override for the runtime editor UI. If empty, the property name is used. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
FString DisplayName;
|
FString DisplayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, calls OnEditorPropertiesChanged (IPS_Editor_EditableInterface)
|
* If true, calls OnEditorPropertiesChanged (IPS_Editor_EditableInterface)
|
||||||
* after changing this property, so the actor can re-run its initialization logic.
|
* after changing this property, so the actor can re-run its initialization logic.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
bool bNeedsReinit = false;
|
bool bNeedsReinit = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, the actor is destroyed and respawned with all current properties applied,
|
* If true, the actor is destroyed and respawned with all current properties applied,
|
||||||
* then OnEditorPropertiesChanged is called. Use when reinit alone is not enough.
|
* then OnEditorPropertiesChanged is called. Use when reinit alone is not enough.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
bool bNeedsRespawn = false;
|
bool bNeedsRespawn = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +43,7 @@ struct FPS_Editor_EditablePropertyEntry
|
|||||||
* Replicated (ideally ReplicatedUsing=OnRep_X) — the server-side timeline writes the value
|
* Replicated (ideally ReplicatedUsing=OnRep_X) — the server-side timeline writes the value
|
||||||
* and standard UE replication pushes it to clients.
|
* and standard UE replication pushes it to clients.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
bool bAnimatable = false;
|
bool bAnimatable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ struct FPS_Editor_EditablePropertyEntry
|
|||||||
* 2. Pick a property from "Add Property" (filtered to that component)
|
* 2. Pick a property from "Add Property" (filtered to that component)
|
||||||
* 3. It auto-adds to "Editable Properties" as "Component.Property"
|
* 3. It auto-adds to "Editable Properties" as "Component.Property"
|
||||||
*/
|
*/
|
||||||
UCLASS(ClassGroup = "ASTERION|PS Editor", meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Editable"))
|
UCLASS(ClassGroup = "PS Editor", meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Editable"))
|
||||||
class PS_EDITOR_API UPS_Editor_EditableComponent : public UActorComponent
|
class PS_EDITOR_API UPS_Editor_EditableComponent : public UActorComponent
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct FPS_Editor_PropertyOption
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
/** Label shown in the runtime editor combo box. */
|
/** Label shown in the runtime editor combo box. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString DisplayName;
|
FString DisplayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +24,7 @@ struct FPS_Editor_PropertyOption
|
|||||||
* the full class path; for an enum you'd use the NameString; for a free FString,
|
* the full class path; for an enum you'd use the NameString; for a free FString,
|
||||||
* any value you want stored.
|
* any value you want stored.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
FString Value;
|
FString Value;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,21 +51,21 @@ public:
|
|||||||
* The new value is already applied when this is called.
|
* The new value is already applied when this is called.
|
||||||
* @param PropertyPath The property that changed (e.g. "CharacterType" or "MyComponent.SomeProperty").
|
* @param PropertyPath The property that changed (e.g. "CharacterType" or "MyComponent.SomeProperty").
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorPropertyChanged(const FString& PropertyPath);
|
void OnEditorPropertyChanged(const FString& PropertyPath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a property with bNeedsReinit is changed.
|
* Called when a property with bNeedsReinit is changed.
|
||||||
* The new value is already applied. Use this to re-run initialization logic.
|
* The new value is already applied. Use this to re-run initialization logic.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorNeedsReinit();
|
void OnEditorNeedsReinit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called on the NEW actor after a respawn triggered by bNeedsRespawn.
|
* Called on the NEW actor after a respawn triggered by bNeedsRespawn.
|
||||||
* All properties have been applied. Use this for post-respawn setup.
|
* All properties have been applied. Use this for post-respawn setup.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorNeedsRespawn();
|
void OnEditorNeedsRespawn();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +73,7 @@ public:
|
|||||||
* Fires both in editor (SceneSerializer) and in gameplay (SceneLoader).
|
* Fires both in editor (SceneSerializer) and in gameplay (SceneLoader).
|
||||||
* Use this to re-initialize your actor based on the loaded property values.
|
* Use this to re-initialize your actor based on the loaded property values.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnPropertiesLoaded();
|
void OnPropertiesLoaded();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +81,7 @@ public:
|
|||||||
* Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.).
|
* Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.).
|
||||||
* @param bIsEditing True when entering edit mode, false when leaving (Play, return to game).
|
* @param bIsEditing True when entering edit mode, false when leaving (Play, return to game).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorModeChanged(bool bIsEditing);
|
void OnEditorModeChanged(bool bIsEditing);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +90,7 @@ public:
|
|||||||
* the hood). Use this to kick off behaviour that should only happen while AI is live
|
* the hood). Use this to kick off behaviour that should only happen while AI is live
|
||||||
* (spawn FX, reset state counters, unlock input, etc.).
|
* (spawn FX, reset state counters, unlock input, etc.).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorSimulateStart();
|
void OnEditorSimulateStart();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +102,7 @@ public:
|
|||||||
* Timing: fires at the "AI cut" moment, AFTER any grace period the timeline applied
|
* Timing: fires at the "AI cut" moment, AFTER any grace period the timeline applied
|
||||||
* (typically 500ms after Stop was clicked). BT is still live but about to be disabled.
|
* (typically 500ms after Stop was clicked). BT is still live but about to be disabled.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorSimulateStop();
|
void OnEditorSimulateStop();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +130,7 @@ public:
|
|||||||
* event still fires + grace period still granted)
|
* event still fires + grace period still granted)
|
||||||
* Does NOT fire for direct StopSimulation calls (rare — e.g. forced editor close).
|
* Does NOT fire for direct StopSimulation calls (rare — e.g. forced editor close).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorBeforeSimulateStop();
|
void OnEditorBeforeSimulateStop();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +138,7 @@ public:
|
|||||||
* Use this to update visuals dynamically based on property changes, animate helpers, etc.
|
* Use this to update visuals dynamically based on property changes, animate helpers, etc.
|
||||||
* @param DeltaTime Time since last frame.
|
* @param DeltaTime Time since last frame.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
void OnEditorTick(float DeltaTime);
|
void OnEditorTick(float DeltaTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +150,7 @@ public:
|
|||||||
* @return List of option strings. Empty = use default widget / fall through to the
|
* @return List of option strings. Empty = use default widget / fall through to the
|
||||||
* structured variant below.
|
* structured variant below.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
TArray<FString> GetCustomOptionsForProperty(const FString& PropertyPath) const;
|
TArray<FString> GetCustomOptionsForProperty(const FString& PropertyPath) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +164,7 @@ public:
|
|||||||
* @param PropertyPath The property path (e.g. "WeaponName" or "MyComponent.WeaponType").
|
* @param PropertyPath The property path (e.g. "WeaponName" or "MyComponent.WeaponType").
|
||||||
* @return List of display/value pairs. Empty = fall back to the flat-string variant.
|
* @return List of display/value pairs. Empty = fall back to the flat-string variant.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
TArray<FPS_Editor_PropertyOption> GetCustomOptionsWithValuesForProperty(const FString& PropertyPath) const;
|
TArray<FPS_Editor_PropertyOption> GetCustomOptionsWithValuesForProperty(const FString& PropertyPath) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,6 +185,6 @@ public:
|
|||||||
* @param PropertyPath The property path (e.g. "Weapon" or "MyComponent.WeaponType").
|
* @param PropertyPath The property path (e.g. "Weapon" or "MyComponent.WeaponType").
|
||||||
* @return DisplayName to show in the combo, or empty string to fall back.
|
* @return DisplayName to show in the combo, or empty string to fall back.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
|
||||||
FString GetCurrentDisplayForProperty(const FString& PropertyPath) const;
|
FString GetCurrentDisplayForProperty(const FString& PropertyPath) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "PS_Editor_SceneLoader.h"
|
||||||
|
#include "Components/PrimitiveComponent.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
bool UPS_Editor_FloorCutManager::ShouldCreateSubsystem(UObject* Outer) const
|
||||||
|
{
|
||||||
|
// Only create on real game worlds — skip preview / inactive worlds.
|
||||||
|
if (const UWorld* World = Cast<UWorld>(Outer))
|
||||||
|
{
|
||||||
|
const EWorldType::Type WT = World->WorldType;
|
||||||
|
return WT == EWorldType::Game || WT == EWorldType::PIE || WT == EWorldType::Editor;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
bEnabled = false;
|
||||||
|
HeightAboveAnchor = 200.f;
|
||||||
|
bAnchorZCached = false;
|
||||||
|
CachedAnchorZ = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::Deinitialize()
|
||||||
|
{
|
||||||
|
// Restore everything we touched, so a world tear-down leaves a clean state.
|
||||||
|
RestoreAll();
|
||||||
|
Snapshots.Empty();
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::SetEnabled(bool bInEnabled)
|
||||||
|
{
|
||||||
|
// Editor-only feature: silently ignore in gameplay mode. The widgets gate calls already,
|
||||||
|
// but this is a belt-and-braces safety so nothing leaks into a SceneLoader-driven runtime.
|
||||||
|
if (!UPS_Editor_SceneLoader::IsInEditorMode())
|
||||||
|
{
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
RestoreAll();
|
||||||
|
bEnabled = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bEnabled == bInEnabled) return;
|
||||||
|
|
||||||
|
bEnabled = bInEnabled;
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::SetHeightAboveAnchor(float HeightCm)
|
||||||
|
{
|
||||||
|
// Clamp to a sane range matching the UI slider (0–1000 cm).
|
||||||
|
HeightCm = FMath::Clamp(HeightCm, 0.f, 10000.f);
|
||||||
|
if (FMath::IsNearlyEqual(HeightAboveAnchor, HeightCm)) return;
|
||||||
|
|
||||||
|
HeightAboveAnchor = HeightCm;
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_FloorCutManager::GetCutWorldZ() const
|
||||||
|
{
|
||||||
|
if (!bAnchorZCached)
|
||||||
|
{
|
||||||
|
// Const-cast acceptable: lazy cache only, no observable mutation of "logical" state.
|
||||||
|
const_cast<UPS_Editor_FloorCutManager*>(this)->ResolveAnchorZ();
|
||||||
|
}
|
||||||
|
return CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ReapplyCut()
|
||||||
|
{
|
||||||
|
if (!bEnabled) return;
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::OnActorSpawned(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (!bEnabled || !Actor) return;
|
||||||
|
|
||||||
|
ResolveAnchorZ();
|
||||||
|
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
|
||||||
|
TArray<UPrimitiveComponent*> Prims;
|
||||||
|
Actor->GetComponents<UPrimitiveComponent>(Prims);
|
||||||
|
for (UPrimitiveComponent* P : Prims)
|
||||||
|
{
|
||||||
|
ApplyCutToComponent(P, CutZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::InvalidateAnchorCache()
|
||||||
|
{
|
||||||
|
bAnchorZCached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_FloorCutManager::ResolveAnchorZ()
|
||||||
|
{
|
||||||
|
bAnchorZCached = true;
|
||||||
|
CachedAnchorZ = 0.f;
|
||||||
|
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World) return CachedAnchorZ;
|
||||||
|
|
||||||
|
for (TActorIterator<APS_Editor_MandatoryAnchor> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
if (APS_Editor_MandatoryAnchor* Anchor = *It)
|
||||||
|
{
|
||||||
|
CachedAnchorZ = Anchor->GetActorLocation().Z;
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut anchor Z = %f"), CachedAnchorZ);
|
||||||
|
return CachedAnchorZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: FloorCut — no APS_Editor_MandatoryAnchor found, using world Z=0 as reference"));
|
||||||
|
return CachedAnchorZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ApplyCutToWorld()
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
ResolveAnchorZ();
|
||||||
|
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
|
||||||
|
int32 Hidden = 0;
|
||||||
|
int32 Visible = 0;
|
||||||
|
|
||||||
|
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
AActor* Actor = *ActorIt;
|
||||||
|
if (!Actor) continue;
|
||||||
|
|
||||||
|
// Never hide the anchor itself (it's hidden in game anyway, but defensive).
|
||||||
|
if (Actor->IsA<APS_Editor_MandatoryAnchor>()) continue;
|
||||||
|
|
||||||
|
TArray<UPrimitiveComponent*> Prims;
|
||||||
|
Actor->GetComponents<UPrimitiveComponent>(Prims);
|
||||||
|
for (UPrimitiveComponent* P : Prims)
|
||||||
|
{
|
||||||
|
if (!P) continue;
|
||||||
|
|
||||||
|
const FBoxSphereBounds B = P->Bounds;
|
||||||
|
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
|
||||||
|
if (MinZ > CutZ)
|
||||||
|
{
|
||||||
|
ApplyCutToComponent(P, CutZ);
|
||||||
|
++Hidden;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Component sits at or straddles the plane → must be in its restored state.
|
||||||
|
RestoreComponent(P);
|
||||||
|
++Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut applied — CutZ=%.1f, hidden=%d, kept=%d, anchor=%.1f, height=%.1f"),
|
||||||
|
CutZ, Hidden, Visible, CachedAnchorZ, HeightAboveAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ)
|
||||||
|
{
|
||||||
|
if (!Comp) return;
|
||||||
|
|
||||||
|
const FBoxSphereBounds B = Comp->Bounds;
|
||||||
|
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
|
||||||
|
if (MinZ <= CutZ)
|
||||||
|
{
|
||||||
|
// Below the plane — make sure it is in its restored state (caller may not have).
|
||||||
|
RestoreComponent(Comp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot before first mutation, so we can restore the exact original state.
|
||||||
|
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
|
||||||
|
if (!Snapshots.Contains(Key))
|
||||||
|
{
|
||||||
|
FCutSnapshot Snap;
|
||||||
|
Snap.bWasVisible = Comp->IsVisible();
|
||||||
|
Snap.CollisionEnabled = Comp->GetCollisionEnabled();
|
||||||
|
Snapshots.Add(Key, Snap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comp->SetVisibility(false, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::RestoreComponent(UPrimitiveComponent* Comp)
|
||||||
|
{
|
||||||
|
if (!Comp) return;
|
||||||
|
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
|
||||||
|
const FCutSnapshot* Snap = Snapshots.Find(Key);
|
||||||
|
if (!Snap) return;
|
||||||
|
|
||||||
|
Comp->SetVisibility(Snap->bWasVisible, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(Snap->CollisionEnabled);
|
||||||
|
Snapshots.Remove(Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::RestoreAll()
|
||||||
|
{
|
||||||
|
for (auto It = Snapshots.CreateIterator(); It; ++It)
|
||||||
|
{
|
||||||
|
UPrimitiveComponent* Comp = It.Key().Get();
|
||||||
|
if (Comp)
|
||||||
|
{
|
||||||
|
Comp->SetVisibility(It.Value().bWasVisible, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(It.Value().CollisionEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Snapshots.Empty();
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "Engine/EngineTypes.h"
|
||||||
|
#include "UObject/WeakObjectPtr.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.generated.h"
|
||||||
|
|
||||||
|
class UPrimitiveComponent;
|
||||||
|
class APS_Editor_MandatoryAnchor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor-only horizontal "floor cut" plane.
|
||||||
|
*
|
||||||
|
* Hides every UPrimitiveComponent whose world-space bounds-min Z is strictly above
|
||||||
|
* AnchorZ + HeightAboveAnchor, by toggling per-component visibility and collision.
|
||||||
|
* The original state is snapshotted before any modification and restored on disable
|
||||||
|
* (or on plugin exit), so the feature is non-destructive.
|
||||||
|
*
|
||||||
|
* Editor-only by design — the gameplay runtime loaded via SceneLoader::LoadScene never
|
||||||
|
* touches this subsystem. UI in the editor widgets is the only consumer.
|
||||||
|
*
|
||||||
|
* Limitations:
|
||||||
|
* - Components straddling the cut plane (minZ <= cut <= maxZ) stay fully visible.
|
||||||
|
* Per-pixel cuts would require modifying master materials, which we explicitly
|
||||||
|
* don't want.
|
||||||
|
* - Anchor Z is sampled once and cached. If the user moves the APS_Editor_MandatoryAnchor
|
||||||
|
* at runtime, call InvalidateAnchorCache() to re-sample.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Get via UWorld::GetSubsystem<UPS_Editor_FloorCutManager>(World)
|
||||||
|
* - SetEnabled(true), SetHeightAboveAnchor(200.f)
|
||||||
|
* - OnActorSpawned(NewActor) when SpawnManager spawns a new actor while active
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API UPS_Editor_FloorCutManager : public UWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// UWorldSubsystem
|
||||||
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
|
||||||
|
|
||||||
|
/** Enable / disable the cut. On disable, all snapshotted components are restored. */
|
||||||
|
void SetEnabled(bool bInEnabled);
|
||||||
|
bool IsEnabled() const { return bEnabled; }
|
||||||
|
|
||||||
|
/** Set the cut height in centimeters above the MandatoryAnchor. Re-applies immediately
|
||||||
|
* if the cut is enabled. */
|
||||||
|
void SetHeightAboveAnchor(float HeightCm);
|
||||||
|
float GetHeightAboveAnchor() const { return HeightAboveAnchor; }
|
||||||
|
|
||||||
|
/** Computed world Z of the cut plane (AnchorZ + HeightAboveAnchor). Returns
|
||||||
|
* TNumericLimits<float>::Max() if no anchor is found. */
|
||||||
|
float GetCutWorldZ() const;
|
||||||
|
|
||||||
|
/** Force a re-application of the current cut on all current components.
|
||||||
|
* Cheap (no allocation if components haven't moved). */
|
||||||
|
void ReapplyCut();
|
||||||
|
|
||||||
|
/** Called by the SpawnManager whenever it spawns a new actor while the cut is
|
||||||
|
* active, so the new actor's primitives are evaluated against the cut. */
|
||||||
|
void OnActorSpawned(AActor* Actor);
|
||||||
|
|
||||||
|
/** Drop the cached anchor Z (re-sampled on next ReapplyCut). */
|
||||||
|
void InvalidateAnchorCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Per-component snapshot of the state we tweak, so we can restore exactly. */
|
||||||
|
struct FCutSnapshot
|
||||||
|
{
|
||||||
|
bool bWasVisible = true;
|
||||||
|
TEnumAsByte<ECollisionEnabled::Type> CollisionEnabled = ECollisionEnabled::QueryAndPhysics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Apply the cut to a single primitive (snapshot if needed, then hide). */
|
||||||
|
void ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ);
|
||||||
|
/** Restore a single primitive from its snapshot, if any. */
|
||||||
|
void RestoreComponent(UPrimitiveComponent* Comp);
|
||||||
|
|
||||||
|
/** Walk the world and apply the cut to every primitive. */
|
||||||
|
void ApplyCutToWorld();
|
||||||
|
/** Restore every snapshotted primitive. */
|
||||||
|
void RestoreAll();
|
||||||
|
|
||||||
|
/** Find the world Z of the first MandatoryAnchor in the world, or fall back to 0.
|
||||||
|
* Result is cached in CachedAnchorZ. */
|
||||||
|
float ResolveAnchorZ();
|
||||||
|
|
||||||
|
bool bEnabled = false;
|
||||||
|
float HeightAboveAnchor = 200.f; // cm — default 2m as agreed with user
|
||||||
|
|
||||||
|
bool bAnchorZCached = false;
|
||||||
|
float CachedAnchorZ = 0.f;
|
||||||
|
|
||||||
|
/** Snapshots of components we have modified, keyed by weak pointer so destroyed
|
||||||
|
* primitives are simply ignored on restore. */
|
||||||
|
TMap<TWeakObjectPtr<UPrimitiveComponent>, FCutSnapshot> Snapshots;
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "Components/SceneComponent.h"
|
||||||
|
#include "Components/ArrowComponent.h"
|
||||||
|
#include "Components/BillboardComponent.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
|
||||||
|
APS_Editor_MandatoryAnchor::APS_Editor_MandatoryAnchor()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||||
|
RootComponent = Root;
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
SpriteComponent = CreateDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
|
||||||
|
SpriteComponent->SetupAttachment(Root);
|
||||||
|
SpriteComponent->SetHiddenInGame(true);
|
||||||
|
SpriteComponent->bIsScreenSizeScaled = true;
|
||||||
|
SpriteComponent->ScreenSize = 0.0015f;
|
||||||
|
|
||||||
|
// Target-point icon — clearly distinct from APS_Editor_CameraStart's note icon.
|
||||||
|
// (Engine/EditorResources/S_Pivot does not exist in UE 5.5 — using S_TargetPoint instead,
|
||||||
|
// which fits the "positional anchor marker" semantic and avoids the cook-time
|
||||||
|
// "Failed to find /Engine/EditorResources/S_Pivot" CDO error.)
|
||||||
|
static ConstructorHelpers::FObjectFinder<UTexture2D> IconFinder(TEXT("/Engine/EditorResources/S_TargetPoint"));
|
||||||
|
if (IconFinder.Succeeded())
|
||||||
|
{
|
||||||
|
SpriteComponent->SetSprite(IconFinder.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
|
||||||
|
ArrowComponent->SetupAttachment(Root);
|
||||||
|
ArrowComponent->ArrowColor = FColor(255, 200, 60); // orange so it pops in the viewport
|
||||||
|
ArrowComponent->ArrowSize = 1.5f;
|
||||||
|
ArrowComponent->bIsScreenSizeScaled = true;
|
||||||
|
ArrowComponent->SetHiddenInGame(true);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The anchor is purely a marker — no rendering in PIE / packaged
|
||||||
|
SetActorHiddenInGame(true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone marker actor that defines where mandatory actors
|
||||||
|
* (UPS_Editor_SpawnableComponent::bIsMandatory) auto-spawn when a new scenario is created
|
||||||
|
* or when a legacy save is missing one.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Place ONE instance of this actor in each baseLevel
|
||||||
|
* - Position it freely — independent from APS_Editor_CameraStart
|
||||||
|
* - Save the level
|
||||||
|
*
|
||||||
|
* The actor is hidden in game / PIE; it has a visible orange arrow + sprite in the level
|
||||||
|
* editor for placement. SpawnManager::EnsureMandatoryActorsPresent reads its world transform.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS Editor Mandatory Anchor"))
|
||||||
|
class PS_EDITOR_API APS_Editor_MandatoryAnchor : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APS_Editor_MandatoryAnchor();
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
protected:
|
||||||
|
/** Editor-only billboard sprite so the actor is easy to pick in the level. */
|
||||||
|
UPROPERTY(VisibleAnywhere, Category = "PS_Editor")
|
||||||
|
TObjectPtr<class UBillboardComponent> SpriteComponent;
|
||||||
|
|
||||||
|
/** Editor-only forward arrow visualizing the anchor's orientation. */
|
||||||
|
UPROPERTY(VisibleAnywhere, Category = "PS_Editor")
|
||||||
|
TObjectPtr<class UArrowComponent> ArrowComponent;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "NavigationSystem.h"
|
||||||
|
#include "NavigationData.h"
|
||||||
|
#include "NavMesh/RecastNavMesh.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/GameViewportClient.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "ProceduralMeshComponent.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
#include "UObject/SoftObjectPath.h"
|
||||||
|
|
||||||
|
namespace PS_Editor_NavUtils
|
||||||
|
{
|
||||||
|
// ANavigationData::RuntimeGeneration and bEnableDrawing are both protected. Local
|
||||||
|
// using-trick subclass to re-publish them — the C++ access check uses the static
|
||||||
|
// type, so a static_cast through this exposing class lets us read/write them
|
||||||
|
// without modifying the engine.
|
||||||
|
struct FNavDataAccessor : public ANavigationData
|
||||||
|
{
|
||||||
|
using ANavigationData::RuntimeGeneration;
|
||||||
|
using ANavigationData::bEnableDrawing;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recast-specific draw flags exposer.
|
||||||
|
struct FRecastAccessor : public ARecastNavMesh
|
||||||
|
{
|
||||||
|
using ARecastNavMesh::bDrawTriangleEdges;
|
||||||
|
using ARecastNavMesh::bDrawPolyEdges;
|
||||||
|
using ARecastNavMesh::bDrawNavMeshEdges;
|
||||||
|
using ARecastNavMesh::bDrawTileBounds;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode)
|
||||||
|
{
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
TArray<AActor*> NavMeshActors;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors);
|
||||||
|
for (AActor* A : NavMeshActors)
|
||||||
|
{
|
||||||
|
if (ANavigationData* NavData = Cast<ANavigationData>(A))
|
||||||
|
{
|
||||||
|
FNavDataAccessor* Accessor = static_cast<FNavDataAccessor*>(NavData);
|
||||||
|
if (Accessor->RuntimeGeneration != Mode)
|
||||||
|
{
|
||||||
|
Accessor->RuntimeGeneration = Mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic)
|
||||||
|
{
|
||||||
|
SetRuntimeGenerationMode(World,
|
||||||
|
bDynamic ? ERuntimeGenerationType::Dynamic : ERuntimeGenerationType::Static);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetNavMeshVisible(UWorld* World, bool bVisible)
|
||||||
|
{
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
// PIE/runtime nav visualization in UE has multiple gates that all need to align:
|
||||||
|
//
|
||||||
|
// 1. ANavigationData::bEnableDrawing — controls whether the rendering
|
||||||
|
// component creates a scene proxy at all.
|
||||||
|
// 2. ARecastNavMesh::bDrawNavMeshEdges (+ bDrawPolyEdges, bDrawTileBounds, etc.)
|
||||||
|
// — control what the proxy actually paints. With bEnableDrawing=true but every
|
||||||
|
// bDraw* flag false, the proxy is created but draws nothing.
|
||||||
|
// 3. GameViewportClient::EngineShowFlags.Navigation — the show-flag the
|
||||||
|
// `show navigation` console command toggles. The proxy is only rendered
|
||||||
|
// when this flag is on.
|
||||||
|
// 4. ReregisterAllComponents — if bEnableDrawing was false at construction
|
||||||
|
// time, the rendering component never created its proxy. Toggling
|
||||||
|
// bEnableDrawing alone won't recreate it; we have to re-register the
|
||||||
|
// components so the proxy is rebuilt with the new flag value.
|
||||||
|
//
|
||||||
|
// All four are required for runtime visualization to actually appear.
|
||||||
|
TArray<AActor*> NavMeshActors;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors);
|
||||||
|
for (AActor* A : NavMeshActors)
|
||||||
|
{
|
||||||
|
if (ANavigationData* NavData = Cast<ANavigationData>(A))
|
||||||
|
{
|
||||||
|
FNavDataAccessor* Accessor = static_cast<FNavDataAccessor*>(NavData);
|
||||||
|
Accessor->bEnableDrawing = bVisible;
|
||||||
|
|
||||||
|
// Recast-specific: enable enough draw flags that the proxy actually paints
|
||||||
|
// something visible. Edges only — tile bounds are noisy.
|
||||||
|
if (ARecastNavMesh* Recast = Cast<ARecastNavMesh>(NavData))
|
||||||
|
{
|
||||||
|
FRecastAccessor* RA = static_cast<FRecastAccessor*>(Recast);
|
||||||
|
RA->bDrawNavMeshEdges = bVisible;
|
||||||
|
RA->bDrawPolyEdges = bVisible;
|
||||||
|
RA->bDrawTriangleEdges = bVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force proxy recreation. MarkComponentsRenderStateDirty alone is not
|
||||||
|
// enough when the proxy was never created (bEnableDrawing=false at init).
|
||||||
|
NavData->ReregisterAllComponents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UGameViewportClient* GVC = World->GetGameViewport())
|
||||||
|
{
|
||||||
|
GVC->EngineShowFlags.SetNavigation(bVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Custom navmesh visualization (works without engine modifications) ----
|
||||||
|
//
|
||||||
|
// Cache the geometry between frames to avoid the expensive GetDebugGeometry call every
|
||||||
|
// tick. We refresh it on a small interval (default 0.5s) — enough for visual feedback
|
||||||
|
// during dynamic nav rebuilds without burning CPU on static maps.
|
||||||
|
struct FCachedNavMeshGeo
|
||||||
|
{
|
||||||
|
TArray<FVector> Vertices;
|
||||||
|
TArray<int32> TriIndices; // groups of 3 — A B C
|
||||||
|
};
|
||||||
|
static TArray<FCachedNavMeshGeo> GCachedNavMeshes;
|
||||||
|
static float GTimeSinceRefresh = 9999.f; // force initial refresh
|
||||||
|
|
||||||
|
// Lift the cached navmesh geometry slightly so the wireframe + translucent surface don't
|
||||||
|
// z-fight with the floor mesh underneath (the navmesh sits exactly on the walkable surface
|
||||||
|
// per the cell height, which causes shimmering with translucent materials especially).
|
||||||
|
// 2cm is enough to suppress visual artifacts on typical level geometry without making the
|
||||||
|
// overlay look detached from the floor.
|
||||||
|
static constexpr float NavMeshDrawZOffset = 2.0f;
|
||||||
|
|
||||||
|
// ---- Surface visualization (procedural mesh + user-supplied translucent material) ----
|
||||||
|
//
|
||||||
|
// We spawn a single persistent actor with one UProceduralMeshComponent that holds the
|
||||||
|
// triangulated navmesh. Its material is loaded once from a hardcoded soft path:
|
||||||
|
// /PS_Editor/M_PS_Editor_NavMeshSurface
|
||||||
|
// Users create that material in the plugin Content folder with their own settings
|
||||||
|
// (translucent, two-sided, color, etc). If the asset is missing, we log a one-shot warning
|
||||||
|
// and the surface stays hidden — the wireframe still draws so the feature degrades
|
||||||
|
// gracefully rather than crashing.
|
||||||
|
static const TCHAR* GNavMeshSurfaceMaterialPath = TEXT("/PS_Editor/M_PS_Editor_NavMeshSurface.M_PS_Editor_NavMeshSurface");
|
||||||
|
static TWeakObjectPtr<AActor> GSurfaceActor;
|
||||||
|
static TWeakObjectPtr<UProceduralMeshComponent> GSurfaceMesh;
|
||||||
|
static TWeakObjectPtr<UMaterialInterface> GSurfaceMaterial;
|
||||||
|
static bool GSurfaceMaterialMissingWarned = false;
|
||||||
|
|
||||||
|
static UMaterialInterface* LoadSurfaceMaterial()
|
||||||
|
{
|
||||||
|
if (GSurfaceMaterial.IsValid()) return GSurfaceMaterial.Get();
|
||||||
|
|
||||||
|
// Always retry — LoadObject is cheap when the asset is already loaded, and we want
|
||||||
|
// the load to succeed even if the material was created AFTER the first attempt
|
||||||
|
// (Live Coding hot-reload, or user just created the asset). Suppressing retries
|
||||||
|
// breaks "create the material, hit P, see it work" without restarting the editor.
|
||||||
|
UMaterialInterface* Mat = LoadObject<UMaterialInterface>(nullptr, GNavMeshSurfaceMaterialPath);
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
GSurfaceMaterial = Mat;
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: NavMesh surface material loaded from %s"), GNavMeshSurfaceMaterialPath);
|
||||||
|
GSurfaceMaterialMissingWarned = false;
|
||||||
|
return Mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GSurfaceMaterialMissingWarned)
|
||||||
|
{
|
||||||
|
GSurfaceMaterialMissingWarned = true;
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: NavMesh surface material not found at %s. ")
|
||||||
|
TEXT("Create a translucent material at that path to enable surface visualization. ")
|
||||||
|
TEXT("Falling back to wireframe-only."), GNavMeshSurfaceMaterialPath);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UProceduralMeshComponent* EnsureSurfaceComponent(UWorld* World)
|
||||||
|
{
|
||||||
|
if (!World) return nullptr;
|
||||||
|
if (GSurfaceMesh.IsValid()) return GSurfaceMesh.Get();
|
||||||
|
|
||||||
|
// Spawn a minimal actor and add a UProceduralMeshComponent at runtime. The actor is
|
||||||
|
// transient — never saved with the scenario, never selectable in the editor.
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.ObjectFlags = RF_Transient;
|
||||||
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
AActor* Actor = World->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity, Params);
|
||||||
|
if (!Actor) return nullptr;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
Actor->SetActorLabel(TEXT("PS_Editor_NavMeshSurfaceViz"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(Actor, TEXT("NavMeshSurface"));
|
||||||
|
Mesh->SetMobility(EComponentMobility::Movable);
|
||||||
|
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Mesh->bUseAsyncCooking = false;
|
||||||
|
Mesh->SetCastShadow(false);
|
||||||
|
Mesh->SetGenerateOverlapEvents(false);
|
||||||
|
Mesh->RegisterComponent();
|
||||||
|
Actor->SetRootComponent(Mesh);
|
||||||
|
|
||||||
|
GSurfaceActor = Actor;
|
||||||
|
GSurfaceMesh = Mesh;
|
||||||
|
return Mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DestroySurfaceActor()
|
||||||
|
{
|
||||||
|
if (AActor* Actor = GSurfaceActor.Get())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
}
|
||||||
|
GSurfaceActor.Reset();
|
||||||
|
GSurfaceMesh.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearNavMeshDebugCache()
|
||||||
|
{
|
||||||
|
GCachedNavMeshes.Empty();
|
||||||
|
GTimeSinceRefresh = 9999.f;
|
||||||
|
DestroySurfaceActor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawNavMeshDebug(UWorld* World, float DeltaTime, float RefreshIntervalSeconds)
|
||||||
|
{
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
GTimeSinceRefresh += DeltaTime;
|
||||||
|
if (GTimeSinceRefresh >= RefreshIntervalSeconds || GCachedNavMeshes.Num() == 0)
|
||||||
|
{
|
||||||
|
GTimeSinceRefresh = 0.f;
|
||||||
|
GCachedNavMeshes.Empty();
|
||||||
|
|
||||||
|
TArray<AActor*> NavMeshActors;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(World, ARecastNavMesh::StaticClass(), NavMeshActors);
|
||||||
|
for (AActor* A : NavMeshActors)
|
||||||
|
{
|
||||||
|
if (ARecastNavMesh* Recast = Cast<ARecastNavMesh>(A))
|
||||||
|
{
|
||||||
|
Recast->BeginBatchQuery();
|
||||||
|
|
||||||
|
FCachedNavMeshGeo Cached;
|
||||||
|
|
||||||
|
// Iterate every tile and pull its debug geometry. UE 5.5 dropped the
|
||||||
|
// single-call GetDebugGeometry; only the per-tile variant remains.
|
||||||
|
// As of 5.5 the int32-tile-index overload is deprecated — use the
|
||||||
|
// FNavTileRef overload via GetAllNavMeshTiles.
|
||||||
|
TArray<FNavTileRef> TileRefs;
|
||||||
|
Recast->GetAllNavMeshTiles(TileRefs);
|
||||||
|
for (const FNavTileRef& TileRef : TileRefs)
|
||||||
|
{
|
||||||
|
FRecastDebugGeometry Geo;
|
||||||
|
Recast->GetDebugGeometryForTile(Geo, TileRef);
|
||||||
|
|
||||||
|
const int32 BaseIdx = Cached.Vertices.Num();
|
||||||
|
Cached.Vertices.Reserve(BaseIdx + Geo.MeshVerts.Num());
|
||||||
|
for (const FVector& V : Geo.MeshVerts)
|
||||||
|
{
|
||||||
|
Cached.Vertices.Add(V + FVector(0.f, 0.f, NavMeshDrawZOffset));
|
||||||
|
}
|
||||||
|
for (int32 AreaIdx = 0; AreaIdx < RECAST_MAX_AREAS; ++AreaIdx)
|
||||||
|
{
|
||||||
|
const TArray<int32>& AreaIndices = Geo.AreaIndices[AreaIdx];
|
||||||
|
Cached.TriIndices.Reserve(Cached.TriIndices.Num() + AreaIndices.Num());
|
||||||
|
for (int32 Idx : AreaIndices)
|
||||||
|
{
|
||||||
|
Cached.TriIndices.Add(BaseIdx + Idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Recast->FinishBatchQuery();
|
||||||
|
GCachedNavMeshes.Add(MoveTemp(Cached));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache rebuilt — also refresh the procedural surface mesh so the translucent
|
||||||
|
// overlay tracks live navmesh changes. We merge every navmesh's geometry into a
|
||||||
|
// single section to keep the proc mesh component lightweight (one section, one
|
||||||
|
// material, one draw call).
|
||||||
|
if (UProceduralMeshComponent* Surface = EnsureSurfaceComponent(World))
|
||||||
|
{
|
||||||
|
TArray<FVector> Vertices;
|
||||||
|
TArray<int32> Triangles;
|
||||||
|
for (const FCachedNavMeshGeo& Cached : GCachedNavMeshes)
|
||||||
|
{
|
||||||
|
const int32 BaseIdx = Vertices.Num();
|
||||||
|
Vertices.Append(Cached.Vertices);
|
||||||
|
Triangles.Reserve(Triangles.Num() + Cached.TriIndices.Num());
|
||||||
|
for (int32 Idx : Cached.TriIndices)
|
||||||
|
{
|
||||||
|
Triangles.Add(BaseIdx + Idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty arrays for normals/UVs/colors/tangents — the unlit translucent material
|
||||||
|
// the user provides doesn't need them. ProcMesh treats empty arrays as "skip
|
||||||
|
// this stream", which is fine for an unlit overlay.
|
||||||
|
static const TArray<FVector> EmptyNormals;
|
||||||
|
static const TArray<FVector2D> EmptyUVs;
|
||||||
|
static const TArray<FColor> EmptyColors;
|
||||||
|
static const TArray<FProcMeshTangent> EmptyTangents;
|
||||||
|
|
||||||
|
if (Vertices.Num() > 0 && Triangles.Num() > 0)
|
||||||
|
{
|
||||||
|
Surface->CreateMeshSection(/*SectionIndex=*/ 0, Vertices, Triangles,
|
||||||
|
EmptyNormals, EmptyUVs, EmptyColors, EmptyTangents,
|
||||||
|
/*bCreateCollision=*/ false);
|
||||||
|
|
||||||
|
UMaterialInterface* Mat = LoadSurfaceMaterial();
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
Surface->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
Surface->SetVisibility(true);
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: NavMesh surface refreshed — %d verts, %d tris, material=%s"),
|
||||||
|
Vertices.Num(), Triangles.Num() / 3,
|
||||||
|
Mat ? *Mat->GetName() : TEXT("<none, wireframe-only>"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Surface->ClearAllMeshSections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-frame draw of cached geometry. Lifetime=0 + bPersistent=false means each call
|
||||||
|
// only paints for one frame, so we get clean redraw every tick.
|
||||||
|
const FColor EdgeColor(40, 220, 90); // green
|
||||||
|
for (const FCachedNavMeshGeo& Cached : GCachedNavMeshes)
|
||||||
|
{
|
||||||
|
const int32 NumTris = Cached.TriIndices.Num() / 3;
|
||||||
|
for (int32 t = 0; t < NumTris; ++t)
|
||||||
|
{
|
||||||
|
const int32 I0 = Cached.TriIndices[t * 3 + 0];
|
||||||
|
const int32 I1 = Cached.TriIndices[t * 3 + 1];
|
||||||
|
const int32 I2 = Cached.TriIndices[t * 3 + 2];
|
||||||
|
if (!Cached.Vertices.IsValidIndex(I0) || !Cached.Vertices.IsValidIndex(I1) || !Cached.Vertices.IsValidIndex(I2))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const FVector& V0 = Cached.Vertices[I0];
|
||||||
|
const FVector& V1 = Cached.Vertices[I1];
|
||||||
|
const FVector& V2 = Cached.Vertices[I2];
|
||||||
|
DrawDebugLine(World, V0, V1, EdgeColor, /*bPersistent=*/ false, /*LifeTime=*/ 0.f, /*DepthPriority=*/ 0, /*Thickness=*/ 1.0f);
|
||||||
|
DrawDebugLine(World, V1, V2, EdgeColor, false, 0.f, 0, 1.0f);
|
||||||
|
DrawDebugLine(World, V2, V0, EdgeColor, false, 0.f, 0, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
class UWorld;
|
||||||
|
enum class ERuntimeGenerationType : uint8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralised helpers for switching the level's RecastNavMesh runtime-generation mode and
|
||||||
|
* toggling the runtime navmesh visualization. Used by:
|
||||||
|
* - The runtime editor "Settings" popup (live toggle of dynamic nav + show navmesh)
|
||||||
|
* - SceneLoader (gameplay path: applies the scenario's bUseDynamicNavigation flag)
|
||||||
|
* - Scenario save/load (persists bUseDynamicNavigation in JSON)
|
||||||
|
*/
|
||||||
|
namespace PS_Editor_NavUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Switch every RecastNavMesh in the world to the requested runtime-generation mode.
|
||||||
|
* Static = baked nav only, no runtime updates (fastest gameplay).
|
||||||
|
* DynamicModifiersOnly = only NavModifierVolumes affect nav at runtime.
|
||||||
|
* Dynamic = all collision-affecting actors update the nav mesh on spawn / move.
|
||||||
|
*
|
||||||
|
* Idempotent — calling with the same mode the navmesh already has is a no-op.
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode);
|
||||||
|
|
||||||
|
/** Convenience: switch to Dynamic if bDynamic, Static otherwise. */
|
||||||
|
PS_EDITOR_API void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the runtime navmesh visualization (mimics the editor's P key — green nav lines
|
||||||
|
* + tile bounds rendered on top of the world). Affects all RecastNavMesh actors in the
|
||||||
|
* world. Free in editor PIE/Standalone; in shipping packaged builds, debug rendering may
|
||||||
|
* be stripped depending on build flags.
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API void SetNavMeshVisible(UWorld* World, bool bVisible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the navmesh as green debug lines for the current frame. Bypasses UE's own nav
|
||||||
|
* debug renderer (which is gated behind UE_ALLOW_NAVMESH_DEBUG_DRAWING_IN_GAME and
|
||||||
|
* doesn't work on binary engine installs at runtime). Pulls the geometry from each
|
||||||
|
* ARecastNavMesh and emits DrawDebugLine calls — works in PIE / Standalone / packaged
|
||||||
|
* shipping alike.
|
||||||
|
*
|
||||||
|
* Internally caches the FRecastDebugGeometry between calls and only refreshes it when
|
||||||
|
* RefreshIntervalSeconds elapses, since GetDebugGeometry is expensive on big navmeshes.
|
||||||
|
*
|
||||||
|
* Call from a per-tick code path while the user wants the visualization shown; stop
|
||||||
|
* calling when they hide it (DrawDebugLine entries auto-expire after 1 frame).
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API void DrawNavMeshDebug(UWorld* World, float DeltaTime, float RefreshIntervalSeconds = 0.5f);
|
||||||
|
|
||||||
|
/** Drop the cached debug geometry (call when leaving editor mode / unloading scene). */
|
||||||
|
PS_EDITOR_API void ClearNavMeshDebugCache();
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
|||||||
// Skip if already has a custom thumbnail
|
// Skip if already has a custom thumbnail
|
||||||
if (SpawnComp->Thumbnail)
|
if (SpawnComp->Thumbnail)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: %s already has a thumbnail, skipping"), *LoadedClass->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: %s already has a thumbnail, skipping"), *LoadedClass->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
|||||||
Blueprint->MarkPackageDirty();
|
Blueprint->MarkPackageDirty();
|
||||||
|
|
||||||
Captured++;
|
Captured++;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured thumbnail for %s -> %s"), *Blueprint->GetName(), *FullPath);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Captured thumbnail for %s -> %s"), *Blueprint->GetName(), *FullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured %d thumbnails"), Captured);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured %d thumbnails"), Captured);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct FPS_Editor_SpawnEntry
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
/** The Blueprint class to spawn. Must have a PS_Editor_SpawnableComponent for name/category. */
|
/** The Blueprint class to spawn. Must have a PS_Editor_SpawnableComponent for name/category. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TSubclassOf<AActor> ActorClass;
|
TSubclassOf<AActor> ActorClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,12 +34,12 @@ class PS_EDITOR_API UPS_Editor_SpawnCatalog : public UPrimaryDataAsset
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/** List of spawnable actors. */
|
/** List of spawnable actors. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TArray<FPS_Editor_SpawnEntry> Entries;
|
TArray<FPS_Editor_SpawnEntry> Entries;
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
/** Captures UE-generated thumbnails for all Blueprints in the catalog and assigns them to their SpawnableComponents. */
|
/** Captures UE-generated thumbnails for all Blueprints in the catalog and assigns them to their SpawnableComponents. */
|
||||||
UFUNCTION(CallInEditor, Category = "ASTERION|PS_Editor")
|
UFUNCTION(CallInEditor, Category = "PS_Editor")
|
||||||
void CaptureAllThumbnails();
|
void CaptureAllThumbnails();
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
@@ -55,18 +55,18 @@ struct FPS_Editor_BaseLevelEntry
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
/** Display name shown in the editor UI and used for scene filtering. */
|
/** Display name shown in the editor UI and used for scene filtering. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
FString DisplayName;
|
FString DisplayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full map path used for OpenLevel and sublevel loading.
|
* Full map path used for OpenLevel and sublevel loading.
|
||||||
* Example: "/Game/Maps/Warehouse"
|
* Example: "/Game/Maps/Warehouse"
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
FString MapPath;
|
FString MapPath;
|
||||||
|
|
||||||
/** Thumbnail image shown in the BaseLevel selection popup. */
|
/** Thumbnail image shown in the BaseLevel selection popup. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TObjectPtr<UTexture2D> Thumbnail;
|
TObjectPtr<UTexture2D> Thumbnail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +75,7 @@ struct FPS_Editor_BaseLevelEntry
|
|||||||
* but not automatically loaded when the base level is a sublevel itself.
|
* but not automatically loaded when the base level is a sublevel itself.
|
||||||
* Full map paths, e.g. "/Game/Maps/Warehouse_Lighting"
|
* Full map paths, e.g. "/Game/Maps/Warehouse_Lighting"
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TArray<FString> AdditionalSublevels;
|
TArray<FString> AdditionalSublevels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +83,7 @@ struct FPS_Editor_BaseLevelEntry
|
|||||||
* Their entries are merged with the global catalogs (MasterCatalog::Catalogs).
|
* Their entries are merged with the global catalogs (MasterCatalog::Catalogs).
|
||||||
* Empty = only global catalogs are shown.
|
* Empty = only global catalogs are shown.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +92,7 @@ struct FPS_Editor_BaseLevelEntry
|
|||||||
* are, the first one wins; if none is, scenarios without a base level keep whatever
|
* are, the first one wins; if none is, scenarios without a base level keep whatever
|
||||||
* sublevel is currently loaded.
|
* sublevel is currently loaded.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
bool bIsDefault = false;
|
bool bIsDefault = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class PS_EDITOR_API UPS_Editor_MasterCatalog : public UPrimaryDataAsset
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/** Sub-catalogs to load. Each one contributes its entries to the runtime spawn UI. */
|
/** Sub-catalogs to load. Each one contributes its entries to the runtime spawn UI. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,6 +116,18 @@ public:
|
|||||||
* DisplayName is used for UI display and scene filtering.
|
* DisplayName is used for UI display and scene filtering.
|
||||||
* MapPath is the full package path for OpenLevel (e.g. "/Game/Maps/Warehouse").
|
* MapPath is the full package path for OpenLevel (e.g. "/Game/Maps/Warehouse").
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ASTERION|PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
|
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional project-specific enum used to categorize scenarios. The runtime editor's
|
||||||
|
* "Scenario Type" dropdown (Settings popup) reads this enum to populate its choices.
|
||||||
|
* The picked value is saved per-scenario as a uint8 (index into the enum) and exposed
|
||||||
|
* at runtime via UPS_Editor_GameInstanceSubsystem::PendingScenarioType — the gameplay
|
||||||
|
* GameMode reads it (Byte to Enum) and dispatches its scenario-type-specific logic.
|
||||||
|
*
|
||||||
|
* Leave null if your project doesn't use scenario typing — the dropdown is hidden.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
|
TObjectPtr<UEnum> ScenarioTypeEnum;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,13 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_GroundSnap.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
#include "BrainComponent.h"
|
#include "BrainComponent.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
@@ -90,11 +95,7 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
|||||||
|
|
||||||
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
||||||
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
||||||
|
Resolved.bIsMandatory = SpawnComp ? SpawnComp->bIsMandatory : false;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
|
|
||||||
*Resolved.DisplayName,
|
|
||||||
SpawnComp ? TEXT("found") : TEXT("null"),
|
|
||||||
Resolved.Thumbnail ? *Resolved.Thumbnail->GetPathName() : TEXT("null"));
|
|
||||||
|
|
||||||
ResolvedEntries.Add(Resolved);
|
ResolvedEntries.Add(Resolved);
|
||||||
}
|
}
|
||||||
@@ -129,7 +130,13 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
|||||||
}
|
}
|
||||||
|
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
// AlwaysSpawn (instead of AdjustIfPossibleButAlwaysSpawn) so UE doesn't push the actor
|
||||||
|
// upward when its bounds overlap the ground at spawn time — our SnapActorToGround call
|
||||||
|
// below is the authoritative ground placement step, and it uses collision-only bounds
|
||||||
|
// which correctly ignore non-collidable attached meshes (weapons, child actors).
|
||||||
|
// With the old override, a character with a weapon dangling below the capsule could
|
||||||
|
// be shoved high by the auto-adjust, leaving it stuck in the air.
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
AActor* SpawnedActor = PC->GetWorld()->SpawnActor<AActor>(Entry.ActorClass, WorldLocation, FRotator::ZeroRotator, SpawnParams);
|
AActor* SpawnedActor = PC->GetWorld()->SpawnActor<AActor>(Entry.ActorClass, WorldLocation, FRotator::ZeroRotator, SpawnParams);
|
||||||
if (!SpawnedActor)
|
if (!SpawnedActor)
|
||||||
@@ -159,27 +166,22 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
|||||||
SpawnedActors.Add(SpawnedActor);
|
SpawnedActors.Add(SpawnedActor);
|
||||||
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
||||||
|
|
||||||
// Auto-snap to ground if configured
|
// Catalogue spawns are always a deliberate user placement, so always snap to ground.
|
||||||
|
// Previously gated on SpawnableComponent::bAutoSnapToGround — that flag is preserved
|
||||||
|
// as the gameplay-spawn hint (for SceneLoader / runtime spawning) but here we want
|
||||||
|
// consistent placement regardless of BP configuration. If the actor genuinely shouldn't
|
||||||
|
// touch the ground, the user can move it with the gizmo after spawning.
|
||||||
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||||
if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld())
|
if (PC->GetWorld())
|
||||||
{
|
{
|
||||||
FVector Origin, Extent;
|
TArray<AActor*> ExtraIgnored;
|
||||||
SpawnedActor->GetActorBounds(false, Origin, Extent);
|
if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);
|
||||||
float BottomOffset = Extent.Z + SpawnComp->GroundOffset;
|
|
||||||
|
|
||||||
const FVector Start = SpawnedActor->GetActorLocation();
|
FVector SnappedLoc;
|
||||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
if (PS_Editor_GroundSnap::SnapActorToGround(SpawnedActor, SnappedLoc,
|
||||||
|
/*OverheadRaise*/ 50.0f, /*MaxDistance*/ 50000.0f, ExtraIgnored))
|
||||||
FHitResult Hit;
|
|
||||||
FCollisionQueryParams SnapParams;
|
|
||||||
SnapParams.AddIgnoredActor(SpawnedActor);
|
|
||||||
SnapParams.AddIgnoredActor(PC->GetPawn());
|
|
||||||
|
|
||||||
if (PC->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, SnapParams))
|
|
||||||
{
|
{
|
||||||
FVector NewLoc = Hit.ImpactPoint;
|
SpawnedActor->SetActorLocation(SnappedLoc);
|
||||||
NewLoc.Z += BottomOffset;
|
|
||||||
SpawnedActor->SetActorLocation(NewLoc);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,23 +275,14 @@ FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
|||||||
FRotator CameraRotation;
|
FRotator CameraRotation;
|
||||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||||
|
|
||||||
const FVector Direction = CameraRotation.Vector();
|
// Unified placement: camera ray first, and if the camera is pointing at the sky / empty
|
||||||
const float MaxDistance = 5000.0f;
|
// space, fall back to a downward trace from a point in front of the camera so the spawn
|
||||||
const float DefaultDistance = 500.0f;
|
// still lands on ground. Avoids the "aim up, object hovers in the sky" bug.
|
||||||
const float SurfaceOffset = 5.0f;
|
TArray<AActor*> Ignored;
|
||||||
|
if (APawn* Pwn = PC->GetPawn()) Ignored.Add(Pwn);
|
||||||
|
|
||||||
FHitResult Hit;
|
return PS_Editor_GroundSnap::ProjectCameraRayToGround(PC->GetWorld(), CameraLocation,
|
||||||
FCollisionQueryParams Params;
|
CameraRotation.Vector(), Ignored);
|
||||||
Params.AddIgnoredActor(PC->GetPawn());
|
|
||||||
|
|
||||||
if (PC->GetWorld()->LineTraceSingleByChannel(Hit, CameraLocation, CameraLocation + Direction * MaxDistance, ECC_Visibility, Params))
|
|
||||||
{
|
|
||||||
// Hit something — place at impact point, slightly offset along the surface normal
|
|
||||||
return Hit.ImpactPoint + Hit.ImpactNormal * SurfaceOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing hit — fallback to default distance
|
|
||||||
return CameraLocation + Direction * DefaultDistance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
||||||
@@ -318,6 +311,19 @@ AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Lo
|
|||||||
SpawnedActors.Add(SpawnedActor);
|
SpawnedActors.Add(SpawnedActor);
|
||||||
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
||||||
|
|
||||||
|
// If the editor's floor-cut is active, evaluate the new actor against it so it doesn't
|
||||||
|
// appear floating above a "hidden" floor.
|
||||||
|
if (UWorld* W = PC->GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
if (FC->IsEnabled())
|
||||||
|
{
|
||||||
|
FC->OnActorSpawned(SpawnedActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SpawnedActor;
|
return SpawnedActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +349,17 @@ void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
|
|||||||
if (APlayerController* PC = OwnerPC.Get())
|
if (APlayerController* PC = OwnerPC.Get())
|
||||||
{
|
{
|
||||||
RegisterSpawnedActorId(PC->GetWorld(), Actor);
|
RegisterSpawnedActorId(PC->GetWorld(), Actor);
|
||||||
|
// Re-evaluate against the floor cut (duplicate / paste paths land here).
|
||||||
|
if (UWorld* W = PC->GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
if (FC->IsEnabled())
|
||||||
|
{
|
||||||
|
FC->OnActorSpawned(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,7 +447,6 @@ AActor* UPS_Editor_SpawnManager::RespawnActorWithProperties(AActor* OldActor, co
|
|||||||
if (Prop)
|
if (Prop)
|
||||||
{
|
{
|
||||||
Prop->ImportText_InContainer(*Pair.Value, Target, Target, PPF_None);
|
Prop->ImportText_InContainer(*Pair.Value, Target, Target, PPF_None);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawn apply [%s] = '%s' on %s"), *Pair.Key, *Pair.Value, *Target->GetName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,6 +492,111 @@ void UPS_Editor_SpawnManager::NotifyEditorModeChanged(bool bIsEditing)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SpawnManager::IsActorDeletable(const AActor* Actor) const
|
||||||
|
{
|
||||||
|
if (!Actor) return true;
|
||||||
|
const UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||||
|
return !(SpawnComp && SpawnComp->bIsMandatory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SpawnManager::EnsureMandatoryActorsPresent()
|
||||||
|
{
|
||||||
|
APlayerController* PC = OwnerPC.Get();
|
||||||
|
UWorld* World = PC ? PC->GetWorld() : nullptr;
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
// Diagnostic: count how many catalog entries are flagged mandatory. If zero, the user
|
||||||
|
// likely hasn't restarted PIE since marking the BP — Live Coding doesn't re-resolve the
|
||||||
|
// catalog, so cached bIsMandatory values stay stale. Surface this clearly in the log.
|
||||||
|
int32 MandatoryCount = 0;
|
||||||
|
for (const FPS_Editor_ResolvedEntry& E : ResolvedEntries)
|
||||||
|
{
|
||||||
|
if (E.bIsMandatory) ++MandatoryCount;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: EnsureMandatoryActorsPresent — scanning %d catalog entries (%d marked mandatory)"),
|
||||||
|
ResolvedEntries.Num(), MandatoryCount);
|
||||||
|
if (MandatoryCount == 0)
|
||||||
|
{
|
||||||
|
// Nothing to do. Don't bother locating an anchor.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Locate the standalone APS_Editor_MandatoryAnchor in the loaded baseLevel. We take
|
||||||
|
// the first one found. If absent, fall back to world origin (with a warning) so
|
||||||
|
// mandatory actors still land somewhere reachable.
|
||||||
|
FTransform AnchorXf = FTransform::Identity;
|
||||||
|
bool bAnchorFound = false;
|
||||||
|
for (TActorIterator<APS_Editor_MandatoryAnchor> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
if (APS_Editor_MandatoryAnchor* Anchor = *It)
|
||||||
|
{
|
||||||
|
AnchorXf = Anchor->GetActorTransform();
|
||||||
|
bAnchorFound = true;
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: MandatoryAnchor found at %s"), *AnchorXf.GetLocation().ToString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bAnchorFound)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EnsureMandatoryActorsPresent — no APS_Editor_MandatoryAnchor placed in the baseLevel. Mandatory actors will spawn at world origin."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. For each mandatory class in the catalog: skip if any tracked actor of that class
|
||||||
|
// already exists, otherwise spawn one at the anchor transform. SpawnActorDirect routes
|
||||||
|
// through the standard tracking path so the new actor lands in SpawnedActors and gets
|
||||||
|
// a stable timeline ActorId.
|
||||||
|
int32 SpawnedNow = 0;
|
||||||
|
int32 SkippedExisting = 0;
|
||||||
|
for (const FPS_Editor_ResolvedEntry& Entry : ResolvedEntries)
|
||||||
|
{
|
||||||
|
if (!Entry.bIsMandatory || !Entry.ActorClass) continue;
|
||||||
|
|
||||||
|
// Already present?
|
||||||
|
bool bAlreadyPresent = false;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
|
{
|
||||||
|
AActor* Existing = Weak.Get();
|
||||||
|
if (Existing && Existing->IsA(Entry.ActorClass))
|
||||||
|
{
|
||||||
|
bAlreadyPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bAlreadyPresent)
|
||||||
|
{
|
||||||
|
++SkippedExisting;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* NewActor = SpawnActorDirect(Entry.ActorClass, AnchorXf.GetLocation(),
|
||||||
|
AnchorXf.Rotator(), AnchorXf.GetScale3D());
|
||||||
|
if (NewActor)
|
||||||
|
{
|
||||||
|
++SpawnedNow;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Mandatory actor auto-spawned: %s at %s"),
|
||||||
|
*Entry.DisplayName, *AnchorXf.GetLocation().ToString());
|
||||||
|
|
||||||
|
// Fire OnPropertiesLoaded so the actor's BP can run its init logic. For loaded
|
||||||
|
// scenes, the SceneLoader/Serializer already fires this after applying JSON
|
||||||
|
// properties; mandatory auto-spawn has no properties to apply, but the actor
|
||||||
|
// still needs the same init hook (e.g. setting up visuals based on defaults).
|
||||||
|
// Without this call, BP code that relies on OnPropertiesLoaded silently doesn't
|
||||||
|
// run for auto-spawned actors → the actor stays in its raw post-BeginPlay state.
|
||||||
|
if (NewActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
IPS_Editor_EditableInterface::Execute_OnPropertiesLoaded(NewActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to spawn mandatory actor %s"), *Entry.DisplayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: EnsureMandatoryActorsPresent — done. Spawned=%d, AlreadyPresent=%d"),
|
||||||
|
SpawnedNow, SkippedExisting);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
@@ -487,6 +608,24 @@ void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
|||||||
IPS_Editor_EditableInterface::Execute_OnEditorTick(Actor, DeltaTime);
|
IPS_Editor_EditableInterface::Execute_OnEditorTick(Actor, DeltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom navmesh visualization (P key / Settings checkbox). Bypasses UE's nav debug
|
||||||
|
// renderer (which is gated behind UE_ALLOW_NAVMESH_DEBUG_DRAWING_IN_GAME and unusable
|
||||||
|
// on binary engine builds at runtime). Triangulates each ARecastNavMesh and emits
|
||||||
|
// DrawDebugLine calls per edge — works in PIE / Standalone / packaged. The proc-mesh
|
||||||
|
// surface overlay is also (re)built inside DrawNavMeshDebug; on the on→off transition
|
||||||
|
// we explicitly clear the cache so the surface actor is destroyed (DrawDebugLine entries
|
||||||
|
// auto-expire after 1 frame, but the proc-mesh actor needs an explicit teardown).
|
||||||
|
if (bShowNavMesh)
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::DrawNavMeshDebug(GetWorld(), DeltaTime);
|
||||||
|
bWasShowingNavMesh = true;
|
||||||
|
}
|
||||||
|
else if (bWasShowingNavMesh)
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::ClearNavMeshDebugCache();
|
||||||
|
bWasShowingNavMesh = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ struct FPS_Editor_ResolvedEntry
|
|||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UTexture2D> Thumbnail;
|
TObjectPtr<UTexture2D> Thumbnail;
|
||||||
|
|
||||||
|
/** Cached from UPS_Editor_SpawnableComponent::bIsMandatory at catalog resolution time.
|
||||||
|
* Mandatory entries are auto-spawned on new-scene/load and hidden from the catalog UI. */
|
||||||
|
bool bIsMandatory = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +79,46 @@ public:
|
|||||||
/** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */
|
/** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */
|
||||||
void NotifyEditorModeChanged(bool bIsEditing);
|
void NotifyEditorModeChanged(bool bIsEditing);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the catalog, and for every class flagged bIsMandatory that has no instance currently
|
||||||
|
* tracked in SpawnedActors, spawn one at the world's APS_Editor_CameraStart::MandatorySpawnAnchor
|
||||||
|
* (or world origin if none found). Idempotent — calling repeatedly is safe; missing instances
|
||||||
|
* are added, existing ones are left untouched.
|
||||||
|
*
|
||||||
|
* Called from SceneSerializer::ClearScene (new scene) and SceneLoader (after JSON load) so
|
||||||
|
* mandatory actors are guaranteed present whether the user starts fresh or opens a legacy
|
||||||
|
* save that predates the mandatory flag.
|
||||||
|
*/
|
||||||
|
void EnsureMandatoryActorsPresent();
|
||||||
|
|
||||||
|
/** Returns false if Actor has a UPS_Editor_SpawnableComponent with bIsMandatory==true.
|
||||||
|
* Used by the delete code path (Pawn::HandleDelete) to skip mandatory actors silently. */
|
||||||
|
bool IsActorDeletable(const AActor* Actor) const;
|
||||||
|
|
||||||
|
// ---- Per-scenario settings (persisted in JSON) ----
|
||||||
|
|
||||||
|
/** If true, switch the level's RecastNavMesh to Dynamic runtime generation when this
|
||||||
|
* scenario is loaded — both in editor and gameplay. False = keep cooked Static nav.
|
||||||
|
* Persisted in the scenario JSON; default false. */
|
||||||
|
bool bUseDynamicNavigation = false;
|
||||||
|
|
||||||
|
/** Project-specific scenario type as uint8 (index into MasterCatalog::ScenarioTypeEnum).
|
||||||
|
* Set via the Settings popup dropdown, persisted in the scenario JSON, pushed into
|
||||||
|
* PS_Editor_GameInstanceSubsystem::PendingScenarioType at Play click for gameplay
|
||||||
|
* GameMode to consume. Default 0 (first enum entry). */
|
||||||
|
uint8 ScenarioType = 0;
|
||||||
|
|
||||||
|
// ---- Session-only UI preferences (NOT persisted) ----
|
||||||
|
|
||||||
|
/** If true, the navmesh is rendered at runtime (mimics the editor's P key). Personal
|
||||||
|
* visualization preference, never saved with the scenario. */
|
||||||
|
bool bShowNavMesh = false;
|
||||||
|
|
||||||
|
/** Tracks the previous tick's bShowNavMesh state so TickEditorMode can detect the
|
||||||
|
* on→off transition and tear down the proc-mesh surface actor that DrawNavMeshDebug
|
||||||
|
* spawned. Without this, the surface would stay visible after the user toggled it off. */
|
||||||
|
bool bWasShowingNavMesh = false;
|
||||||
|
|
||||||
/** Tick all spawned actors that implement IPS_Editor_EditableInterface. */
|
/** Tick all spawned actors that implement IPS_Editor_EditableInterface. */
|
||||||
void TickEditorMode(float DeltaTime);
|
void TickEditorMode(float DeltaTime);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Marker component for Blueprints that can be spawned via the PS_Editor catalogue.
|
* Marker component for Blueprints that can be spawned via the PS_Editor catalogue.
|
||||||
* Add this component to any Blueprint to make it appear in the runtime spawn catalogue.
|
* Add this component to any Blueprint to make it appear in the runtime spawn catalogue.
|
||||||
*/
|
*/
|
||||||
UCLASS(ClassGroup = "ASTERION|PS Editor", meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Spawnable"))
|
UCLASS(ClassGroup = "PS Editor", meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Spawnable"))
|
||||||
class PS_EDITOR_API UPS_Editor_SpawnableComponent : public UActorComponent
|
class PS_EDITOR_API UPS_Editor_SpawnableComponent : public UActorComponent
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
@@ -54,6 +54,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||||
bool bPreload = false;
|
bool bPreload = false;
|
||||||
|
|
||||||
|
/** If true, this actor class is mandatory for any scenario:
|
||||||
|
* - Auto-spawned on "New Scene" (one instance, at the MandatorySpawnAnchor of the
|
||||||
|
* baseLevel's APS_Editor_CameraStart, falling back to world origin)
|
||||||
|
* - On scene load, if missing, auto-added (handles legacy saves cleanly)
|
||||||
|
* - Cannot be deleted by the user (delete is silently refused with a warning log)
|
||||||
|
* - Hidden from the catalog (you can't add a 2nd instance) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||||
|
bool bIsMandatory = false;
|
||||||
|
|
||||||
// ---- Runtime-only: saved movement/rotation settings before editor override ----
|
// ---- Runtime-only: saved movement/rotation settings before editor override ----
|
||||||
// Set when the editor disables AI/rotation overrides, restored on StartSimulation.
|
// Set when the editor disables AI/rotation overrides, restored on StartSimulation.
|
||||||
bool bSavedMovementState = false;
|
bool bSavedMovementState = false;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
#include "ProceduralMeshComponent.h"
|
#include "ProceduralMeshComponent.h"
|
||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
#include "Engine/StaticMesh.h"
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
#include "Materials/MaterialInstanceDynamic.h"
|
#include "Materials/MaterialInstanceDynamic.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
|
||||||
APS_Editor_SplineActor::APS_Editor_SplineActor()
|
APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||||
{
|
{
|
||||||
@@ -50,25 +52,46 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
|||||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
|
||||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
|
||||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))});
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))});
|
||||||
|
|
||||||
|
// Hard-ref plugin + engine assets at CDO-construction time so the cooker follows them
|
||||||
|
// into packaged builds. Replaces the old runtime LoadObject<> soft-string paths.
|
||||||
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GizmoBaseMatFinder(
|
||||||
|
TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||||
|
if (GizmoBaseMatFinder.Succeeded())
|
||||||
|
{
|
||||||
|
GizmoBaseMaterial = GizmoBaseMatFinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> SplineLineMatFinder(
|
||||||
|
TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
||||||
|
if (SplineLineMatFinder.Succeeded())
|
||||||
|
{
|
||||||
|
SplineLineMaterial = SplineLineMatFinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeFinder(
|
||||||
|
TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
|
if (CubeFinder.Succeeded())
|
||||||
|
{
|
||||||
|
CenterCubeMesh = CubeFinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereFinder(
|
||||||
|
TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||||
|
if (SphereFinder.Succeeded())
|
||||||
|
{
|
||||||
|
HandleSphereMesh = SphereFinder.Object;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_SplineActor::BeginPlay()
|
void APS_Editor_SplineActor::BeginPlay()
|
||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
// Gizmo material (no depth test, renders on top) for handles and selected state
|
// GizmoBaseMaterial / SplineLineMaterial / meshes are resolved in the ctor via
|
||||||
UMaterialInterface* GizmoMat = LoadObject<UMaterialInterface>(
|
// ConstructorHelpers — hard cook references, available here.
|
||||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
UMaterialInterface* GizmoMat = GizmoBaseMaterial.Get();
|
||||||
|
UMaterialInterface* SplineLineMat = SplineLineMaterial.Get();
|
||||||
// Auto-generated opaque unlit material (depth tested) for spline line
|
|
||||||
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
|
|
||||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
|
||||||
|
|
||||||
if (!SplineLineMat)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: M_PS_Editor_SplineLine not found, trying without double name..."));
|
|
||||||
SplineLineMat = LoadObject<UMaterialInterface>(nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spline line: normal depth-tested material (uses SplineColor property)
|
// Spline line: normal depth-tested material (uses SplineColor property)
|
||||||
if (SplineLineMat)
|
if (SplineLineMat)
|
||||||
@@ -102,10 +125,9 @@ void APS_Editor_SplineActor::BeginPlay()
|
|||||||
CenterCube = NewObject<UStaticMeshComponent>(this);
|
CenterCube = NewObject<UStaticMeshComponent>(this);
|
||||||
CenterCube->SetupAttachment(GetRootComponent());
|
CenterCube->SetupAttachment(GetRootComponent());
|
||||||
CenterCube->RegisterComponent();
|
CenterCube->RegisterComponent();
|
||||||
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
if (CenterCubeMesh)
|
||||||
if (CubeMesh)
|
|
||||||
{
|
{
|
||||||
CenterCube->SetStaticMesh(CubeMesh);
|
CenterCube->SetStaticMesh(CenterCubeMesh);
|
||||||
}
|
}
|
||||||
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||||
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
@@ -310,11 +332,10 @@ UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& W
|
|||||||
Handle->SetupAttachment(HandleRoot);
|
Handle->SetupAttachment(HandleRoot);
|
||||||
Handle->RegisterComponent();
|
Handle->RegisterComponent();
|
||||||
|
|
||||||
// Load sphere mesh
|
// HandleSphereMesh resolved via ConstructorHelpers (hard cook ref).
|
||||||
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
if (HandleSphereMesh)
|
||||||
if (SphereMesh)
|
|
||||||
{
|
{
|
||||||
Handle->SetStaticMesh(SphereMesh);
|
Handle->SetStaticMesh(HandleSphereMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
Handle->SetWorldLocation(WorldPosition);
|
Handle->SetWorldLocation(WorldPosition);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class USplineComponent;
|
|||||||
class UProceduralMeshComponent;
|
class UProceduralMeshComponent;
|
||||||
class UPS_Editor_SpawnableComponent;
|
class UPS_Editor_SpawnableComponent;
|
||||||
class UPS_Editor_EditableComponent;
|
class UPS_Editor_EditableComponent;
|
||||||
|
class UMaterialInterface;
|
||||||
|
class UStaticMesh;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime spline actor for path/route editing in PS_Editor.
|
* Runtime spline actor for path/route editing in PS_Editor.
|
||||||
@@ -29,7 +31,7 @@ public:
|
|||||||
virtual int32 GetCurrentPointCount() const override { return GetNumPoints(); }
|
virtual int32 GetCurrentPointCount() const override { return GetNumPoints(); }
|
||||||
|
|
||||||
/** Get the underlying USplineComponent (for AI path following, etc.). */
|
/** Get the underlying USplineComponent (for AI path following, etc.). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Spline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
USplineComponent* GetSplineComponent() const { return SplineComp; }
|
USplineComponent* GetSplineComponent() const { return SplineComp; }
|
||||||
|
|
||||||
/** Spline display color (override in Blueprint subclasses for variants). */
|
/** Spline display color (override in Blueprint subclasses for variants). */
|
||||||
@@ -73,7 +75,7 @@ public:
|
|||||||
virtual void UpdateVisualization() override;
|
virtual void UpdateVisualization() override;
|
||||||
|
|
||||||
/** Re-apply SplineColor to materials (call after changing SplineColor at runtime). */
|
/** Re-apply SplineColor to materials (call after changing SplineColor at runtime). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Spline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
virtual void UpdateSplineMaterials() override;
|
virtual void UpdateSplineMaterials() override;
|
||||||
|
|
||||||
// ---- Handle interaction ----
|
// ---- Handle interaction ----
|
||||||
@@ -166,6 +168,23 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||||
|
|
||||||
|
// ---- Hard-ref source assets (cooked into packaged builds via ConstructorHelpers) ----
|
||||||
|
|
||||||
|
/** Gizmo base material for spline handles / selected state (renders on top). */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInterface> GizmoBaseMaterial;
|
||||||
|
|
||||||
|
/** Depth-tested material for the normal spline line. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInterface> SplineLineMaterial;
|
||||||
|
|
||||||
|
/** Engine basic shapes used for the center cube and point handles. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UStaticMesh> CenterCubeMesh;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UStaticMesh> HandleSphereMesh;
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere)
|
UPROPERTY(VisibleAnywhere)
|
||||||
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
|
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
|
||||||
|
|
||||||
|
|||||||
@@ -194,20 +194,16 @@ void UPS_Editor_TimelineSubsystem::CaptureBaselinesForAllMissingTracks()
|
|||||||
|
|
||||||
void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
||||||
{
|
{
|
||||||
if (PrePlayBaseline.Num() == 0) return;
|
|
||||||
|
|
||||||
// Server-only write path (same guard as ApplyValuesAtTime).
|
// Server-only write path (same guard as ApplyValuesAtTime).
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (World->GetNetMode() == NM_Client) return;
|
if (World->GetNetMode() == NM_Client) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float ZeroEpsilon = 0.0001f;
|
||||||
|
|
||||||
for (const FPS_Editor_TimelineTrack& Track : Data.Tracks)
|
for (const FPS_Editor_TimelineTrack& Track : Data.Tracks)
|
||||||
{
|
{
|
||||||
const TPair<FGuid, FName> Key(Track.ActorId, Track.PropertyPath);
|
|
||||||
const FString* BaselineValue = PrePlayBaseline.Find(Key);
|
|
||||||
if (!BaselineValue) continue;
|
|
||||||
|
|
||||||
AActor* Actor = FindActorById(Track.ActorId);
|
AActor* Actor = FindActorById(Track.ActorId);
|
||||||
if (!Actor) continue;
|
if (!Actor) continue;
|
||||||
UObject* Target = nullptr;
|
UObject* Target = nullptr;
|
||||||
@@ -215,7 +211,39 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
|||||||
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
||||||
if (!Target || !Prop) continue;
|
if (!Target || !Prop) continue;
|
||||||
|
|
||||||
Prop->ImportText_InContainer(**BaselineValue, Target, Target, PPF_None);
|
// Pick the value to restore:
|
||||||
|
// - If the user placed a key at (or before) t=0 → use that key's value. This
|
||||||
|
// matches the natural mental model where a key at t=0 sets the timeline's
|
||||||
|
// starting state — and lets the user adjust it just by re-keying at t=0
|
||||||
|
// (otherwise Stop would keep snapping back to the captured pre-animation
|
||||||
|
// baseline forever, ignoring their edits).
|
||||||
|
// - Otherwise fall back to the captured pre-animation baseline so Stop still
|
||||||
|
// undoes whatever the timeline pushed (relevant for tracks whose first key
|
||||||
|
// is past t=0, e.g. a property that only changes mid-timeline).
|
||||||
|
const FString* RestoreValue = nullptr;
|
||||||
|
FString KeyZeroValue;
|
||||||
|
for (const FPS_Editor_TimelineKey& K : Track.Keys)
|
||||||
|
{
|
||||||
|
if (K.Time <= ZeroEpsilon)
|
||||||
|
{
|
||||||
|
KeyZeroValue = K.Value;
|
||||||
|
RestoreValue = &KeyZeroValue;
|
||||||
|
// Continue scanning in case there are several keys at t≈0 — the LAST one wins
|
||||||
|
// (matches sort order: later same-time entries override earlier ones).
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break; // keys are sorted by time; no need to look past t>0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!RestoreValue)
|
||||||
|
{
|
||||||
|
const TPair<FGuid, FName> BaselineKey(Track.ActorId, Track.PropertyPath);
|
||||||
|
RestoreValue = PrePlayBaseline.Find(BaselineKey);
|
||||||
|
}
|
||||||
|
if (!RestoreValue) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(**RestoreValue, Target, Target, PPF_None);
|
||||||
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
||||||
{
|
{
|
||||||
AsComp->MarkRenderStateDirty();
|
AsComp->MarkRenderStateDirty();
|
||||||
@@ -234,15 +262,40 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't clear the map — a subsequent Stop (or Restart) must still restore the same baseline.
|
// Don't clear PrePlayBaseline — subsequent Stops without a t=0 key still need it.
|
||||||
// SetData/Clear are the only resetters.
|
// SetData/Clear are the only resetters.
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_TimelineSubsystem::Restart()
|
void UPS_Editor_TimelineSubsystem::Restart()
|
||||||
{
|
{
|
||||||
// Rewind + replay from the start. Done in two steps so simulation transforms are restored
|
// Rewind + replay from the start.
|
||||||
// by Stop() before Play() re-enables AI, giving a clean "from the beginning" rerun.
|
//
|
||||||
Stop();
|
// We can't just call Stop() then Play() because Stop() goes through
|
||||||
|
// PlayerController::RequestStopSimulation which DEFERS the actual StopSimulation by
|
||||||
|
// 500ms (BT grace period). The Play() call right after cancels that pending timer
|
||||||
|
// AND re-enters StartSimulation early (bSimulating still true), so transforms are
|
||||||
|
// never restored — characters stay at their current position instead of rewinding.
|
||||||
|
//
|
||||||
|
// Fix: do the full reset synchronously here. Fire OnEditorBeforeSimulateStop (via
|
||||||
|
// RequestStopSimulation(bImmediate=true) so BP behaviour-reset hooks still run),
|
||||||
|
// then immediately StopSimulation (which restores transforms + fires
|
||||||
|
// OnEditorSimulateStop), then Play() to capture the rewound transforms and restart
|
||||||
|
// AI from the clean state.
|
||||||
|
bIsPlaying = false;
|
||||||
|
CurrentTime = 0.f;
|
||||||
|
LastAppliedValues.Empty();
|
||||||
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
|
|
||||||
|
RestoreBaseline();
|
||||||
|
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld()))
|
||||||
|
{
|
||||||
|
if (EditorPC->IsSimulating())
|
||||||
|
{
|
||||||
|
EditorPC->RequestStopSimulation(/*bImmediate=*/ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Play();
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +353,10 @@ void UPS_Editor_TimelineSubsystem::Clear()
|
|||||||
bIsPlaying = false;
|
bIsPlaying = false;
|
||||||
LastAppliedValues.Empty();
|
LastAppliedValues.Empty();
|
||||||
PrePlayBaseline.Reset();
|
PrePlayBaseline.Reset();
|
||||||
|
// Drop the old ActorIds — the previous scenario's actors have been destroyed and the
|
||||||
|
// weak references are now stale. Keeping them around confuses FindActorById when the
|
||||||
|
// next scenario happens to reuse the same FGuid (it can return a stale null entry).
|
||||||
|
ActorIds.Reset();
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +374,14 @@ static int32 FindTrack(const FPS_Editor_TimelineData& Data, const FGuid& ActorId
|
|||||||
return INDEX_NONE;
|
return INDEX_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_TimelineSubsystem::IsPropertyTracked(AActor* Actor, const FName& PropertyPath) const
|
||||||
|
{
|
||||||
|
if (!Actor) return false;
|
||||||
|
const FGuid ActorId = FindActorId(Actor);
|
||||||
|
if (!ActorId.IsValid()) return false;
|
||||||
|
return FindTrack(Data, ActorId, PropertyPath) != INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, const FName& PropertyPath)
|
bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, const FName& PropertyPath)
|
||||||
{
|
{
|
||||||
if (!Actor) return false;
|
if (!Actor) return false;
|
||||||
@@ -324,7 +389,7 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co
|
|||||||
const FGuid ActorId = GetOrCreateActorId(Actor);
|
const FGuid ActorId = GetOrCreateActorId(Actor);
|
||||||
if (!ActorId.IsValid()) return false;
|
if (!ActorId.IsValid()) return false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor Timeline: AddOrUpdateKey — Actor=%s ActorId=%s Prop=%s CurrentTime=%.3f"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor Timeline: AddOrUpdateKey — Actor=%s ActorId=%s Prop=%s CurrentTime=%.3f"),
|
||||||
*Actor->GetName(), *ActorId.ToString(), *PropertyPath.ToString(), CurrentTime);
|
*Actor->GetName(), *ActorId.ToString(), *PropertyPath.ToString(), CurrentTime);
|
||||||
|
|
||||||
// Resolve property to read current value
|
// Resolve property to read current value
|
||||||
@@ -360,6 +425,14 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co
|
|||||||
if (FMath::IsNearlyEqual(Track.Keys[InsertAt].Time, CurrentTime, Epsilon))
|
if (FMath::IsNearlyEqual(Track.Keys[InsertAt].Time, CurrentTime, Epsilon))
|
||||||
{
|
{
|
||||||
Track.Keys[InsertAt].Value = CurrentValue;
|
Track.Keys[InsertAt].Value = CurrentValue;
|
||||||
|
// Invalidate the per-track applied-value cache so the next ApplyTrackAtTime
|
||||||
|
// re-writes the (possibly different) value instead of skipping based on the
|
||||||
|
// stale cache entry. Otherwise, updating a key with a value the cache happened
|
||||||
|
// to hold from a previous apply would silently revert the property to it.
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -400,6 +473,12 @@ void UPS_Editor_TimelineSubsystem::RemoveKey(int32 TrackIndex, int32 KeyIndex)
|
|||||||
Data.Tracks.RemoveAt(TrackIndex);
|
Data.Tracks.RemoveAt(TrackIndex);
|
||||||
LastAppliedValues.SetNum(Data.Tracks.Num());
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
}
|
}
|
||||||
|
else if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
// Track still has keys but its surrounding keys for any time may have changed —
|
||||||
|
// invalidate the cache so the next apply re-writes the correct value.
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +488,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyInterpolate(int32 TrackIndex, int32 Key
|
|||||||
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
||||||
if (!Track.Keys.IsValidIndex(KeyIndex)) return;
|
if (!Track.Keys.IsValidIndex(KeyIndex)) return;
|
||||||
Track.Keys[KeyIndex].bInterpolate = bInterpolate;
|
Track.Keys[KeyIndex].bInterpolate = bInterpolate;
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +507,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyTime(int32 TrackIndex, int32 KeyIndex,
|
|||||||
{
|
{
|
||||||
return A.Time < B.Time;
|
return A.Time < B.Time;
|
||||||
});
|
});
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,11 +657,18 @@ FGuid UPS_Editor_TimelineSubsystem::FindActorId(AActor* Actor) const
|
|||||||
AActor* UPS_Editor_TimelineSubsystem::FindActorById(const FGuid& Id) const
|
AActor* UPS_Editor_TimelineSubsystem::FindActorById(const FGuid& Id) const
|
||||||
{
|
{
|
||||||
if (!Id.IsValid()) return nullptr;
|
if (!Id.IsValid()) return nullptr;
|
||||||
|
// Iterate every pair: the map can legitimately contain stale weak references from a
|
||||||
|
// previous scenario whose actors were destroyed. Without this check the first match on
|
||||||
|
// GUID can return a null key before we reach the live entry, giving "(missing)" after
|
||||||
|
// the second load of the same scenario.
|
||||||
for (const TPair<TWeakObjectPtr<AActor>, FGuid>& Pair : ActorIds)
|
for (const TPair<TWeakObjectPtr<AActor>, FGuid>& Pair : ActorIds)
|
||||||
{
|
{
|
||||||
if (Pair.Value == Id)
|
if (Pair.Value == Id)
|
||||||
{
|
{
|
||||||
return Pair.Key.Get();
|
if (AActor* Live = Pair.Key.Get())
|
||||||
|
{
|
||||||
|
return Live;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -583,6 +677,16 @@ AActor* UPS_Editor_TimelineSubsystem::FindActorById(const FGuid& Id) const
|
|||||||
void UPS_Editor_TimelineSubsystem::RegisterActorId(AActor* Actor, const FGuid& Id)
|
void UPS_Editor_TimelineSubsystem::RegisterActorId(AActor* Actor, const FGuid& Id)
|
||||||
{
|
{
|
||||||
if (!Actor || !Id.IsValid()) return;
|
if (!Actor || !Id.IsValid()) return;
|
||||||
|
// Prune any stale entries plus any previous entry holding the same GUID (the old actor
|
||||||
|
// has been destroyed and replaced by this one). Keeps the map tight and ensures
|
||||||
|
// FindActorById cannot hit a ghost key with the same GUID.
|
||||||
|
for (auto It = ActorIds.CreateIterator(); It; ++It)
|
||||||
|
{
|
||||||
|
if (!It.Key().IsValid() || It.Value() == Id)
|
||||||
|
{
|
||||||
|
It.RemoveCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
ActorIds.Add(TWeakObjectPtr<AActor>(Actor), Id);
|
ActorIds.Add(TWeakObjectPtr<AActor>(Actor), Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,39 +46,39 @@ public:
|
|||||||
// ---- Playback controls ----
|
// ---- Playback controls ----
|
||||||
|
|
||||||
/** Start / resume playback from CurrentTime. */
|
/** Start / resume playback from CurrentTime. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void Play();
|
void Play();
|
||||||
|
|
||||||
/** Pause playback, keeping CurrentTime. Simulation keeps running. */
|
/** Pause playback, keeping CurrentTime. Simulation keeps running. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void Pause();
|
void Pause();
|
||||||
|
|
||||||
/** Pause playback, rewind to CurrentTime=0, stop simulation and apply t=0 values. */
|
/** Pause playback, rewind to CurrentTime=0, stop simulation and apply t=0 values. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
/** Rewind to t=0 and immediately replay from the start (re-enables simulation). */
|
/** Rewind to t=0 and immediately replay from the start (re-enables simulation). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void Restart();
|
void Restart();
|
||||||
|
|
||||||
/** Jump to a specific time (seconds, clamped to [0, Duration]). Applies values immediately. */
|
/** Jump to a specific time (seconds, clamped to [0, Duration]). Applies values immediately. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void Seek(float Time);
|
void Seek(float Time);
|
||||||
|
|
||||||
/** True while Tick is advancing CurrentTime. */
|
/** True while Tick is advancing CurrentTime. */
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
bool IsPlaying() const { return bIsPlaying; }
|
bool IsPlaying() const { return bIsPlaying; }
|
||||||
|
|
||||||
/** Get current playback time (seconds). */
|
/** Get current playback time (seconds). */
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
float GetCurrentTime() const { return CurrentTime; }
|
float GetCurrentTime() const { return CurrentTime; }
|
||||||
|
|
||||||
/** Get the configured duration (seconds). */
|
/** Get the configured duration (seconds). */
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
float GetDuration() const { return Data.Duration; }
|
float GetDuration() const { return Data.Duration; }
|
||||||
|
|
||||||
/** Change the timeline duration. Values <= 0 are clamped to 0.1. */
|
/** Change the timeline duration. Values <= 0 are clamped to 0.1. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|Timeline")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void SetDuration(float NewDuration);
|
void SetDuration(float NewDuration);
|
||||||
|
|
||||||
// ---- Tracks / keys ----
|
// ---- Tracks / keys ----
|
||||||
@@ -88,6 +88,11 @@ public:
|
|||||||
|
|
||||||
const TArray<FPS_Editor_TimelineTrack>& GetTracks() const { return Data.Tracks; }
|
const TArray<FPS_Editor_TimelineTrack>& GetTracks() const { return Data.Tracks; }
|
||||||
|
|
||||||
|
/** Returns true if the timeline holds a track for (Actor, PropertyPath). Used by the
|
||||||
|
* PlayerController's editable-property snapshot/restore so we don't double-restore
|
||||||
|
* properties already handled by the timeline's RestoreBaseline. */
|
||||||
|
bool IsPropertyTracked(AActor* Actor, const FName& PropertyPath) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a key at CurrentTime for (Actor, PropertyPath), reading the actor's current value.
|
* Add a key at CurrentTime for (Actor, PropertyPath), reading the actor's current value.
|
||||||
* Creates the track if it doesn't exist. If a key already exists at this time, updates its value.
|
* Creates the track if it doesn't exist. If a key already exists at this time, updates its value.
|
||||||
|
|||||||
@@ -35,16 +35,16 @@ class PS_EDITOR_API APS_Editor_HUD : public AHUD
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
/** Which UI implementation to instantiate. Set via Details panel on the HUD Blueprint. */
|
/** Which UI implementation to instantiate. Set via Details panel on the HUD Blueprint. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI")
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PS_Editor")
|
||||||
EPS_Editor_UIMode UIMode = EPS_Editor_UIMode::NewSlate;
|
EPS_Editor_UIMode UIMode = EPS_Editor_UIMode::NewSlate;
|
||||||
|
|
||||||
/** UMG Widget Blueprint class to spawn when UIMode == UMG.
|
/** UMG Widget Blueprint class to spawn when UIMode == UMG.
|
||||||
* Must derive from UPS_Editor_MainWidget_UMG. */
|
* Must derive from UPS_Editor_MainWidget_UMG. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI", meta = (EditCondition = "UIMode == EPS_Editor_UIMode::UMG"))
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PS_Editor", meta = (EditCondition = "UIMode == EPS_Editor_UIMode::UMG"))
|
||||||
TSoftClassPtr<UPS_Editor_MainWidget_UMG> UMGWidgetClass;
|
TSoftClassPtr<UPS_Editor_MainWidget_UMG> UMGWidgetClass;
|
||||||
|
|
||||||
/** Returns the active main editor widget (cast as needed). */
|
/** Returns the active main editor widget (cast as needed). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|UI")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
UUserWidget* GetMainWidget() const { return MainWidget; }
|
UUserWidget* GetMainWidget() const { return MainWidget; }
|
||||||
|
|
||||||
/** Backward-compat accessor for the refactored Slate widget (returns nullptr in Legacy/UMG modes). */
|
/** Backward-compat accessor for the refactored Slate widget (returns nullptr in Legacy/UMG modes). */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_MainWidget.h"
|
#include "PS_Editor_MainWidget.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
#include "DesktopPlatformModule.h"
|
#include "DesktopPlatformModule.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "PS_Editor_UIStyleAsset.h"
|
#include "PS_Editor_UIStyleAsset.h"
|
||||||
#include "PS_Editor_UIStyle.h"
|
#include "PS_Editor_UIStyle.h"
|
||||||
@@ -500,6 +502,173 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings popup (per-scenario nav option + session navmesh visu)
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Center)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SettingsPopup, SBorder)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.BorderImage(BrushGlassStrong.Get())
|
||||||
|
.Padding(FMargin(28.0f, 20.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnUseDynamicNavigationToggled(State); })
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Use Dynamic Navigation")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario).")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnShowNavMeshToggled(State); })
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Show NavMesh (P)")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Scenario Type dropdown — visible only when MasterCatalog::ScenarioTypeEnum is set.
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
.Visibility_Lambda([this]() -> EVisibility
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
return EVisibility::Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EVisibility::Collapsed;
|
||||||
|
})
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.45f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scenario Type")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Saved per-scenario; pushed to PendingScenarioType at Play.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.55f).VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(ScenarioTypeCombo, SComboBox<TSharedPtr<FString>>)
|
||||||
|
.OptionsSource(&ScenarioTypeOptions)
|
||||||
|
.OnComboBoxOpening_Lambda([this]() { RefreshScenarioTypeOptions(); })
|
||||||
|
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? *Item : FString()));
|
||||||
|
})
|
||||||
|
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> NewItem, ESelectInfo::Type SelectType)
|
||||||
|
{
|
||||||
|
if (!NewItem.IsValid()) return;
|
||||||
|
const int32 Idx = ScenarioTypeOptions.IndexOfByKey(NewItem);
|
||||||
|
if (Idx == INDEX_NONE) return;
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
SM->ScenarioType = (uint8)Idx;
|
||||||
|
bSceneDirty = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text_Lambda([this]() -> FText
|
||||||
|
{
|
||||||
|
return FText::FromString(GetCurrentScenarioTypeDisplayName());
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ButtonStyle(StyleFlatButton.Get())
|
||||||
|
.OnClicked_Lambda([this]() -> FReply { CloseSettingsPopup(); return FReply::Handled(); })
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Close")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
// ============= COMMAND PALETTE (modal, Ctrl+K) =============
|
// ============= COMMAND PALETTE (modal, Ctrl+K) =============
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
@@ -724,6 +893,70 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth() [ MakeSeparator() ]
|
||||||
|
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth()
|
||||||
|
[
|
||||||
|
MakeToggleButton(EPS_Editor_Icon::Grid, EPS_Editor_Icon::Grid,
|
||||||
|
TAttribute<FString>::CreateLambda([this]() -> FString
|
||||||
|
{
|
||||||
|
UWorld* W = GetWorld();
|
||||||
|
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
|
||||||
|
return (FC && FC->IsEnabled()) ? TEXT("Floor cut On") : TEXT("Floor cut");
|
||||||
|
}),
|
||||||
|
[this]() -> bool
|
||||||
|
{
|
||||||
|
UWorld* W = GetWorld();
|
||||||
|
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
|
||||||
|
return FC && FC->IsEnabled();
|
||||||
|
},
|
||||||
|
[this]()
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(!FC->IsEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SBox).MinDesiredWidth(120.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutSlider, SSlider)
|
||||||
|
.MinValue(0.0f)
|
||||||
|
.MaxValue(1000.0f)
|
||||||
|
.StepSize(10.0f)
|
||||||
|
.MouseUsesStep(true)
|
||||||
|
.Value(200.0f)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
|
||||||
|
.OnValueChanged_Lambda([this](float NewValue)
|
||||||
|
{
|
||||||
|
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(Snapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FloorCutValueText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutValueText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("200 cm")))
|
||||||
|
.Font(FPS_Editor_UIStyle::SmallFont(S))
|
||||||
|
.ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S))
|
||||||
|
.MinDesiredWidth(48.0f)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,6 +1209,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||||
|
// Re-spawn mandatory actors immediately after the wipe — this is
|
||||||
|
// the "New Scenario" path, where the user expects a fresh-but-not-empty
|
||||||
|
// scene (mandatory class instances at the MandatoryAnchor).
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
LastKnownSpawnedCount = 0;
|
LastKnownSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
@@ -987,6 +1224,26 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings button
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.AutoWidth()
|
||||||
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ButtonStyle(StyleFlatButton.Get())
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu).")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
OpenSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
]
|
||||||
+ SHorizontalBox::Slot()
|
+ SHorizontalBox::Slot()
|
||||||
.AutoWidth()
|
.AutoWidth()
|
||||||
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
@@ -1041,7 +1298,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||||
{
|
{
|
||||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
const uint8 TypeToSend = SpawnManager.IsValid() ? SpawnManager->ScenarioType : 0;
|
||||||
|
Sub->SetPendingScenario(SceneName, BaseLevel, TypeToSend);
|
||||||
Sub->bIsPlayMode = true;
|
Sub->bIsPlayMode = true;
|
||||||
if (Sub->EditorMapName.IsEmpty())
|
if (Sub->EditorMapName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -1159,7 +1417,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
|
SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
@@ -1418,18 +1676,14 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
|||||||
// No base level in the scenario file — try to resolve the default from the catalog.
|
// No base level in the scenario file — try to resolve the default from the catalog.
|
||||||
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
||||||
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
||||||
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT(" BaseLevel entry: DisplayName='%s', MapPath='%s', bIsDefault=%d"),
|
|
||||||
*Entry.DisplayName, *Entry.MapPath, Entry.bIsDefault ? 1 : 0);
|
|
||||||
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
SceneLevelName = Entry.DisplayName;
|
SceneLevelName = Entry.DisplayName;
|
||||||
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
||||||
UE_LOG(LogTemp, Log, TEXT(" -> Picked default '%s' (in PathMap=%d)"),
|
|
||||||
*SceneLevelName, bHasValidBaseLevel ? 1 : 0);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1464,7 +1718,7 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
|||||||
// current sublevel as-is — don't unload it (avoids empty scenes).
|
// current sublevel as-is — don't unload it (avoids empty scenes).
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(Name, SpawnManager.Get());
|
SceneSerializer->LoadScenario(Name, SpawnManager.Get());
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
@@ -1548,6 +1802,10 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
{
|
{
|
||||||
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
||||||
|
|
||||||
|
// Mandatory classes are auto-spawned and singletons — hide them from the catalog so
|
||||||
|
// the user can't add a 2nd instance.
|
||||||
|
if (Entry.bIsMandatory) continue;
|
||||||
|
|
||||||
// Category header
|
// Category header
|
||||||
if (Entry.Category != CurrentCategory)
|
if (Entry.Category != CurrentCategory)
|
||||||
{
|
{
|
||||||
@@ -1821,9 +2079,33 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
|
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplinePlacement mode is shared between IPS_Editor_SplineEditable and
|
||||||
|
// IPS_Editor_ChildMovable — show ChildMovable-specific help when the active
|
||||||
|
// placement actor implements the child interface (no point insertion, no Del,
|
||||||
|
// rotation gizmo available via R key).
|
||||||
|
bool bChildMovableActive = false;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC2 = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (AActor* ActiveActor = EditorPC2->GetActivePlacementActor())
|
||||||
|
{
|
||||||
|
bChildMovableActive = ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (HelpBarText.IsValid())
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
if (bChildMovableActive)
|
||||||
{
|
{
|
||||||
if (bPointSelected)
|
if (bPointSelected)
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Drag gizmo to move | R: rotate | T: translate | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Click a child to select | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bPointSelected)
|
||||||
{
|
{
|
||||||
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
||||||
}
|
}
|
||||||
@@ -1835,7 +2117,8 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
}
|
}
|
||||||
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
|
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed);
|
// Delete-point button is meaningless for ChildMovable (fixed child set)
|
||||||
|
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility((bPointSelected && !bChildMovableActive) ? EVisibility::Visible : EVisibility::Collapsed);
|
||||||
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
||||||
}
|
}
|
||||||
else if (bChildMovableSelected)
|
else if (bChildMovableSelected)
|
||||||
@@ -2243,7 +2526,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
||||||
|
|
||||||
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
@@ -2271,8 +2554,6 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||||
Row.Key = Entry.Path.ToString();
|
Row.Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
|
||||||
|
|
||||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
@@ -2293,8 +2574,6 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName());
|
|
||||||
|
|
||||||
const int32 RowIndex = DynamicPropertyRows.Num();
|
const int32 RowIndex = DynamicPropertyRows.Num();
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
@@ -2545,7 +2824,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
DynamicPropertyRows.Add(Row);
|
DynamicPropertyRows.Add(Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||||
@@ -2875,7 +3154,7 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
||||||
{
|
{
|
||||||
Target->ProcessEvent(RepFunc, nullptr);
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2902,7 +3181,7 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
||||||
|
|
||||||
// Always notify property changed
|
// Always notify property changed
|
||||||
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
@@ -3182,6 +3461,90 @@ void UPS_Editor_MainWidget::CancelBaseLevelPopup()
|
|||||||
PendingBaseLevelSelection.Empty();
|
PendingBaseLevelSelection.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OpenSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
// Refresh enum-driven options each time the popup opens — covers the case where the
|
||||||
|
// MasterCatalogAsset (or its ScenarioTypeEnum) wasn't set at widget Construct time.
|
||||||
|
RefreshScenarioTypeOptions();
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RefreshScenarioTypeOptions()
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Empty();
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
// NumEnums()-1 to skip the auto-generated _MAX entry on regular UENUMs.
|
||||||
|
// User-defined enums (UUserDefinedEnum) don't have _MAX but the last "MAX" entry
|
||||||
|
// is hidden behind the `bIsHidden` meta — we drop the last one defensively.
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
for (int32 i = 0; i < Count; ++i)
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Add(MakeShared<FString>(E->GetDisplayNameTextByIndex(i).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ScenarioTypeCombo.IsValid())
|
||||||
|
{
|
||||||
|
ScenarioTypeCombo->RefreshOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_MainWidget::GetCurrentScenarioTypeDisplayName() const
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
if (SM->ScenarioType < Count)
|
||||||
|
{
|
||||||
|
return E->GetDisplayNameTextByIndex(SM->ScenarioType).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::CloseSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnUseDynamicNavigationToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bEnabled = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bUseDynamicNavigation = bEnabled;
|
||||||
|
bSceneDirty = true;
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnShowNavMeshToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bVisible = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bShowNavMesh = bVisible;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::OnCloseClicked()
|
void UPS_Editor_MainWidget::OnCloseClicked()
|
||||||
{
|
{
|
||||||
if (CloseConfirmOverlay.IsValid())
|
if (CloseConfirmOverlay.IsValid())
|
||||||
@@ -3844,7 +4207,7 @@ void UPS_Editor_MainWidget::RebuildTimelineTracksList()
|
|||||||
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
||||||
// is properly released and its handles/highlight go away.
|
// is properly released and its handles/highlight go away.
|
||||||
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
||||||
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
||||||
if (TrackActor)
|
if (TrackActor)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ private:
|
|||||||
TSharedPtr<STextBlock> SnapText;
|
TSharedPtr<STextBlock> SnapText;
|
||||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||||
|
|
||||||
|
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
|
||||||
|
TSharedPtr<STextBlock> FloorCutValueText;
|
||||||
|
TSharedPtr<class SSlider> FloorCutSlider;
|
||||||
|
|
||||||
// Spawn catalog
|
// Spawn catalog
|
||||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||||
|
|
||||||
@@ -134,6 +138,19 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu + scenario type).
|
||||||
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
|
void OpenSettingsPopup();
|
||||||
|
void CloseSettingsPopup();
|
||||||
|
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
||||||
|
void OnShowNavMeshToggled(ECheckBoxState State);
|
||||||
|
|
||||||
|
// Scenario type dropdown — populated from MasterCatalog::ScenarioTypeEnum, hidden when null.
|
||||||
|
TArray<TSharedPtr<FString>> ScenarioTypeOptions;
|
||||||
|
TSharedPtr<SComboBox<TSharedPtr<FString>>> ScenarioTypeCombo;
|
||||||
|
void RefreshScenarioTypeOptions();
|
||||||
|
FString GetCurrentScenarioTypeDisplayName() const;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_MainWidget_Legacy.h"
|
#include "PS_Editor_MainWidget_Legacy.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
#include "DesktopPlatformModule.h"
|
#include "DesktopPlatformModule.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
// Forward declaration — the real definition lives further down with the timeline UI block,
|
// Forward declaration — the real definition lives further down with the timeline UI block,
|
||||||
@@ -448,6 +450,187 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
// Settings popup (per-scenario nav option + session navmesh visu)
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Center)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SettingsPopup, SBorder)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f))
|
||||||
|
.Padding(FMargin(28.0f, 20.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
// Header
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
// Use Dynamic Navigation (saved per-scenario)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
|
||||||
|
{
|
||||||
|
OnUseDynamicNavigationToggled(State);
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Use Dynamic Navigation")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario).")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Show NavMesh (session-only)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
|
||||||
|
{
|
||||||
|
OnShowNavMeshToggled(State);
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Show NavMesh (P)")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Scenario Type dropdown — visible only when MasterCatalog::ScenarioTypeEnum is set.
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
.Visibility_Lambda([this]() -> EVisibility
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
return EVisibility::Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EVisibility::Collapsed;
|
||||||
|
})
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.45f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scenario Type")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Saved per-scenario; pushed to PendingScenarioType at Play.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.55f).VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(ScenarioTypeCombo, SComboBox<TSharedPtr<FString>>)
|
||||||
|
.OptionsSource(&ScenarioTypeOptions)
|
||||||
|
.OnComboBoxOpening_Lambda([this]() { RefreshScenarioTypeOptions(); })
|
||||||
|
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? *Item : FString()));
|
||||||
|
})
|
||||||
|
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> NewItem, ESelectInfo::Type SelectType)
|
||||||
|
{
|
||||||
|
if (!NewItem.IsValid()) return;
|
||||||
|
const int32 Idx = ScenarioTypeOptions.IndexOfByKey(NewItem);
|
||||||
|
if (Idx == INDEX_NONE) return;
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
SM->ScenarioType = (uint8)Idx;
|
||||||
|
bSceneDirty = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text_Lambda([this]() -> FText
|
||||||
|
{
|
||||||
|
return FText::FromString(GetCurrentScenarioTypeDisplayName());
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Close button
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
CloseSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Close")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +741,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildToolbar()
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(16.0f, 0.0f, 2.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Toggle floor cut. Hides primitives whose bounds-min Z is above (Anchor.Z + Height)")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(!FC->IsEnabled());
|
||||||
|
if (FloorCutToggleText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutToggleText->SetText(FText::FromString(FC->IsEnabled() ? TEXT("Floor cut: On") : TEXT("Floor cut: Off")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutToggleText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Floor cut: Off")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SBox).MinDesiredWidth(120.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutSlider, SSlider)
|
||||||
|
.MinValue(0.0f)
|
||||||
|
.MaxValue(1000.0f)
|
||||||
|
.StepSize(10.0f)
|
||||||
|
.MouseUsesStep(true)
|
||||||
|
.Value(200.0f)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
|
||||||
|
.OnValueChanged_Lambda([this](float NewValue)
|
||||||
|
{
|
||||||
|
// Snap to 10 cm — SSlider's MouseUsesStep already does this for clicks,
|
||||||
|
// but a drag still emits raw floats, so we re-snap here.
|
||||||
|
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(Snapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FloorCutValueText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutValueText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("200 cm")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.MinDesiredWidth(48.0f)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,6 +1051,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||||
|
// Re-spawn mandatory actors immediately after the wipe — this is
|
||||||
|
// the "New Scenario" path, where the user expects a fresh-but-not-empty
|
||||||
|
// scene (mandatory class instances at the MandatoryAnchor).
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
LastKnownSpawnedCount = 0;
|
LastKnownSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
@@ -816,6 +1066,25 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings button — opens the global Settings popup
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.AutoWidth()
|
||||||
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu).")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
OpenSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
]
|
||||||
+ SHorizontalBox::Slot()
|
+ SHorizontalBox::Slot()
|
||||||
.AutoWidth()
|
.AutoWidth()
|
||||||
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
@@ -869,7 +1138,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||||
{
|
{
|
||||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
const uint8 TypeToSend = SpawnManager.IsValid() ? SpawnManager->ScenarioType : 0;
|
||||||
|
Sub->SetPendingScenario(SceneName, BaseLevel, TypeToSend);
|
||||||
Sub->bIsPlayMode = true;
|
Sub->bIsPlayMode = true;
|
||||||
if (Sub->EditorMapName.IsEmpty())
|
if (Sub->EditorMapName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -986,7 +1256,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
|
SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
@@ -1239,18 +1509,14 @@ void UPS_Editor_MainWidget_Legacy::PopulateSceneList()
|
|||||||
// No base level in the scenario file — try to resolve the default from the catalog.
|
// No base level in the scenario file — try to resolve the default from the catalog.
|
||||||
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
||||||
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
||||||
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT(" BaseLevel entry: DisplayName='%s', MapPath='%s', bIsDefault=%d"),
|
|
||||||
*Entry.DisplayName, *Entry.MapPath, Entry.bIsDefault ? 1 : 0);
|
|
||||||
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
SceneLevelName = Entry.DisplayName;
|
SceneLevelName = Entry.DisplayName;
|
||||||
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
||||||
UE_LOG(LogTemp, Log, TEXT(" -> Picked default '%s' (in PathMap=%d)"),
|
|
||||||
*SceneLevelName, bHasValidBaseLevel ? 1 : 0);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1285,7 +1551,7 @@ void UPS_Editor_MainWidget_Legacy::PopulateSceneList()
|
|||||||
// current sublevel as-is — don't unload it (avoids empty scenes).
|
// current sublevel as-is — don't unload it (avoids empty scenes).
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(Name, SpawnManager.Get());
|
SceneSerializer->LoadScenario(Name, SpawnManager.Get());
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
@@ -1369,6 +1635,10 @@ void UPS_Editor_MainWidget_Legacy::PopulateSpawnCatalog()
|
|||||||
{
|
{
|
||||||
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
||||||
|
|
||||||
|
// Mandatory classes are auto-spawned and singletons — hide them from the catalog so
|
||||||
|
// the user can't add a 2nd instance.
|
||||||
|
if (Entry.bIsMandatory) continue;
|
||||||
|
|
||||||
// Category header
|
// Category header
|
||||||
if (Entry.Category != CurrentCategory)
|
if (Entry.Category != CurrentCategory)
|
||||||
{
|
{
|
||||||
@@ -1573,9 +1843,30 @@ void UPS_Editor_MainWidget_Legacy::RefreshPropertiesFromSelection()
|
|||||||
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
|
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplinePlacement mode is shared with IPS_Editor_ChildMovable — show the right help.
|
||||||
|
bool bChildMovableActive = false;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC2 = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (AActor* ActiveActor = EditorPC2->GetActivePlacementActor())
|
||||||
|
{
|
||||||
|
bChildMovableActive = ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (HelpBarText.IsValid())
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
if (bChildMovableActive)
|
||||||
{
|
{
|
||||||
if (bPointSelected)
|
if (bPointSelected)
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Drag gizmo to move | R: rotate | T: translate | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Click a child to select | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bPointSelected)
|
||||||
{
|
{
|
||||||
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
||||||
}
|
}
|
||||||
@@ -1587,7 +1878,8 @@ void UPS_Editor_MainWidget_Legacy::RefreshPropertiesFromSelection()
|
|||||||
}
|
}
|
||||||
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
|
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed);
|
// Delete-point is meaningless for ChildMovable (fixed child set)
|
||||||
|
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility((bPointSelected && !bChildMovableActive) ? EVisibility::Visible : EVisibility::Collapsed);
|
||||||
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
||||||
}
|
}
|
||||||
else if (bChildMovableSelected)
|
else if (bChildMovableSelected)
|
||||||
@@ -1908,7 +2200,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
||||||
|
|
||||||
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
@@ -1936,8 +2228,6 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||||
Row.Key = Entry.Path.ToString();
|
Row.Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
|
||||||
|
|
||||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
@@ -1958,8 +2248,6 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName());
|
|
||||||
|
|
||||||
const int32 RowIndex = DynamicPropertyRows.Num();
|
const int32 RowIndex = DynamicPropertyRows.Num();
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
@@ -2206,7 +2494,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
DynamicPropertyRows.Add(Row);
|
DynamicPropertyRows.Add(Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues()
|
void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues()
|
||||||
@@ -2539,7 +2827,7 @@ void UPS_Editor_MainWidget_Legacy::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
||||||
{
|
{
|
||||||
Target->ProcessEvent(RepFunc, nullptr);
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2566,7 +2854,7 @@ void UPS_Editor_MainWidget_Legacy::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
||||||
|
|
||||||
// Always notify property changed
|
// Always notify property changed
|
||||||
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
@@ -2846,6 +3134,99 @@ void UPS_Editor_MainWidget_Legacy::CancelBaseLevelPopup()
|
|||||||
PendingBaseLevelSelection.Empty();
|
PendingBaseLevelSelection.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OpenSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
// Refresh enum-driven options each time the popup opens — covers the case where the
|
||||||
|
// MasterCatalogAsset (or its ScenarioTypeEnum) wasn't set at widget Construct time.
|
||||||
|
RefreshScenarioTypeOptions();
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::RefreshScenarioTypeOptions()
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Empty();
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
for (int32 i = 0; i < Count; ++i)
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Add(MakeShared<FString>(E->GetDisplayNameTextByIndex(i).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ScenarioTypeCombo.IsValid())
|
||||||
|
{
|
||||||
|
ScenarioTypeCombo->RefreshOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_MainWidget_Legacy::GetCurrentScenarioTypeDisplayName() const
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
if (SM->ScenarioType < Count)
|
||||||
|
{
|
||||||
|
return E->GetDisplayNameTextByIndex(SM->ScenarioType).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::CloseSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OnUseDynamicNavigationToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bEnabled = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bUseDynamicNavigation = bEnabled;
|
||||||
|
bSceneDirty = true; // user changed a per-scenario setting
|
||||||
|
|
||||||
|
// Apply live to the level's RecastNavMesh actors.
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OnShowNavMeshToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bVisible = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bShowNavMesh = bVisible;
|
||||||
|
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::ToggleShowNavMesh()
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
SM->bShowNavMesh = !SM->bShowNavMesh;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_Legacy::OnCloseClicked()
|
void UPS_Editor_MainWidget_Legacy::OnCloseClicked()
|
||||||
{
|
{
|
||||||
if (CloseConfirmOverlay.IsValid())
|
if (CloseConfirmOverlay.IsValid())
|
||||||
@@ -3472,7 +3853,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildTimelineTracksList()
|
|||||||
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
||||||
// is properly released and its handles/highlight go away.
|
// is properly released and its handles/highlight go away.
|
||||||
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
||||||
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
||||||
if (TrackActor)
|
if (TrackActor)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ private:
|
|||||||
TSharedPtr<STextBlock> SnapText;
|
TSharedPtr<STextBlock> SnapText;
|
||||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||||
|
|
||||||
|
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
|
||||||
|
TSharedPtr<STextBlock> FloorCutToggleText;
|
||||||
|
TSharedPtr<class SSlider> FloorCutSlider;
|
||||||
|
TSharedPtr<STextBlock> FloorCutValueText;
|
||||||
|
|
||||||
// Spawn catalog
|
// Spawn catalog
|
||||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||||
|
|
||||||
@@ -107,6 +112,21 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu + scenario type).
|
||||||
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
|
void OpenSettingsPopup();
|
||||||
|
void CloseSettingsPopup();
|
||||||
|
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
||||||
|
void OnShowNavMeshToggled(ECheckBoxState State);
|
||||||
|
/** Toggle the navmesh visualization (P key handler). */
|
||||||
|
void ToggleShowNavMesh();
|
||||||
|
|
||||||
|
// Scenario type dropdown — populated from MasterCatalog::ScenarioTypeEnum, hidden when null.
|
||||||
|
TArray<TSharedPtr<FString>> ScenarioTypeOptions;
|
||||||
|
TSharedPtr<SComboBox<TSharedPtr<FString>>> ScenarioTypeCombo;
|
||||||
|
void RefreshScenarioTypeOptions();
|
||||||
|
FString GetCurrentScenarioTypeDisplayName() const;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||||
{
|
{
|
||||||
@@ -130,7 +132,7 @@ bool UPS_Editor_MainWidget_UMG::LoadScenario(const FString& Name)
|
|||||||
}
|
}
|
||||||
if (!SM) SM = SpawnManager.Get();
|
if (!SM) SM = SpawnManager.Get();
|
||||||
if (!Ser || !SM || Name.IsEmpty()) return false;
|
if (!Ser || !SM || Name.IsEmpty()) return false;
|
||||||
Ser->LoadScene(Name, SM);
|
Ser->LoadScenario(Name, SM);
|
||||||
OnSceneDirty.Broadcast();
|
OnSceneDirty.Broadcast();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -161,6 +163,54 @@ void UPS_Editor_MainWidget_UMG::ClearSelection()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Floor cut --------------------
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_UMG::SetFloorCutEnabled(bool bEnabled)
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(bEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_UMG::SetFloorCutHeightCm(float HeightCm)
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(HeightCm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_MainWidget_UMG::IsFloorCutEnabled() const
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
return FC->IsEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_MainWidget_UMG::GetFloorCutHeightCm() const
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
return FC->GetHeightAboveAnchor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 200.f;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- Getters --------------------
|
// -------------------- Getters --------------------
|
||||||
|
|
||||||
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const
|
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
|
||||||
void ClearSelection();
|
void ClearSelection();
|
||||||
|
|
||||||
|
// ---- Floor cut (editor-only horizontal plane that hides upper floors) ----
|
||||||
|
|
||||||
|
/** Enable/disable the floor cut. Slider value drives the cut height in cm above the
|
||||||
|
* MandatoryAnchor. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
|
||||||
|
void SetFloorCutEnabled(bool bEnabled);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
|
||||||
|
void SetFloorCutHeightCm(float HeightCm);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
|
||||||
|
bool IsFloorCutEnabled() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
|
||||||
|
float GetFloorCutHeightCm() const;
|
||||||
|
|
||||||
// ==================== BP-PURE GETTERS ====================
|
// ==================== BP-PURE GETTERS ====================
|
||||||
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
|
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
|
||||||
EPS_Editor_TransformMode GetCurrentTransformMode() const;
|
EPS_Editor_TransformMode GetCurrentTransformMode() const;
|
||||||
|
|||||||
@@ -185,6 +185,19 @@ void FPS_Editor_BlueprintFromMesh::CreateBlueprintForAsset(UObject* MeshAsset)
|
|||||||
USCS_Node* EditableNode = SCS->CreateNode(UPS_Editor_EditableComponent::StaticClass(), FName("PS_Editor_Editable"));
|
USCS_Node* EditableNode = SCS->CreateNode(UPS_Editor_EditableComponent::StaticClass(), FName("PS_Editor_Editable"));
|
||||||
SCS->AddNode(EditableNode);
|
SCS->AddNode(EditableNode);
|
||||||
|
|
||||||
|
// Enable replication on the generated BP — without it the actor is server-local only and
|
||||||
|
// clients never see it spawn. SetReplicates() is the public path (the bReplicates field
|
||||||
|
// itself is protected). SetReplicateMovement is paired so any server-side transform change
|
||||||
|
// (timeline keyframe, scripted move) propagates to clients.
|
||||||
|
if (UClass* GeneratedClass = Blueprint->GeneratedClass)
|
||||||
|
{
|
||||||
|
if (AActor* CDO = Cast<AActor>(GeneratedClass->GetDefaultObject()))
|
||||||
|
{
|
||||||
|
CDO->SetReplicates(true);
|
||||||
|
CDO->SetReplicateMovement(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compile and save
|
// Compile and save
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
||||||
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
||||||
|
|||||||
Reference in New Issue
Block a user