Integre le plugin directement dans le repo unique (integration par lien symbolique)

Suppression du depot git imbrique du plugin : les fichiers du plugin sont
suivis normalement ici. L'integration dans les autres projets se fait via
un lien symbolique Windows vers Plugins/PS_StrategicVision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:24:05 +02:00
parent d2fc5a8fbb
commit 2fd45af39a
19 changed files with 1366 additions and 1 deletions

Submodule Unreal/PS_StratVision/Plugins/PS_StrategicVision deleted from 199c14097e

View File

@@ -0,0 +1,3 @@
# Artefacts de build du plugin (regeneres a la compilation)
Binaries/
Intermediate/

View File

@@ -0,0 +1,30 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "0.1.0",
"FriendlyName": "PS Strategic Vision",
"Description": "Affichage strategique pour vues spectateur et replays : cones de vision, et autres visualisations a venir.",
"Category": "Visualization",
"CreatedBy": "ASTERION VR",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "PS_StrategicVision",
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "ProceduralMeshComponent",
"Enabled": true
}
]
}

View File

@@ -0,0 +1,126 @@
# PS Strategic Vision
Plugin Unreal Engine 5.7 d'affichage stratégique pour vues spectateur et replays :
cônes de vision des personnages, et d'autres visualisations à venir.
## Installation dans un projet
Copier (ou cloner) ce dossier dans `Plugins/PS_StrategicVision/` du projet, puis
activer le plugin dans le `.uproject` ou via *Edit > Plugins*.
En submodule git :
```bash
git submodule add <url-du-repo> Plugins/PS_StrategicVision
```
Le plugin est en C++ : le projet hôte doit être un projet C++ (il sera compilé
avec le projet).
## Contenu
| Élément | Rôle |
|---|---|
| `UPS_VisionConeComponent` | Component à poser sur un personnage : affiche son cône de vision (mesh procédural, bloqué par les murs, plan toujours horizontal). |
| `UPS_StrategicDisplayComponent` | Classe de base commune à tous les affichages stratégiques (camp, activation, couleur). Les futurs components en hériteront. |
| `IPS_VisionSource` (interface) | À implémenter sur l'acteur pour injecter l'origine/direction de vision réelles (ex : caméra VR). |
| `UPS_StrategicVisionSubsystem` | Subsystem monde : interrupteurs globaux (tout, par catégorie, par camp). |
| `UPS_StrategicVisionSettings` | Réglages projet : activation au démarrage, style par camp (couleur, hachures), matériau. |
| `M_VisionCone` (Content) | Matériau translucide unlit avec hachures procédurales, utilisé par défaut. |
## Utilisation
### 1. Poser le cône de vision
Dans le Blueprint du personnage (ou en C++), ajouter le component **PS Vision Cone**
(idéalement attaché au root / à la capsule). Régler :
- **Team** : `Player`, `Ally`, `Enemy` ou `Neutral` — détermine la couleur et
l'angle des hachures (configurables dans *Project Settings > Plugins >
PS Strategic Vision*).
- **View Distance** / **View Angle Degrees** : portée et ouverture du cône.
- **Trace Channel** : canal de collision utilisé pour bloquer le cône sur les murs
(`Visibility` par défaut).
Le plan du cône est **toujours horizontal**, même si l'acteur est incliné, et il
est projeté à la hauteur du sol détecté (`Project To Ground`).
### 2. Cas VR : injecter la position/direction de la caméra
Si l'acteur reste à l'origine pendant que la caméra bouge (pawn VR), implémenter
l'interface **PS Vision Source** sur l'acteur (Class Settings > Interfaces en
Blueprint) et surcharger **Get Vision View Point** :
- `Out Location` = position monde de la caméra ;
- `Out Forward Direction` = direction du regard (seul le cap horizontal est utilisé) ;
- retourner `true`.
Le component l'utilise automatiquement (`Use Owner Vision Source`, activé par
défaut). Retourner `false` pour repasser sur le transform du component. Penser à
mettre `Trace Height` à 0 si la position fournie est déjà à hauteur des yeux.
### 3. Activer / désactiver selon l'état de jeu
**Tout est masqué par défaut** : au démarrage d'une partie, aucun affichage
stratégique n'est visible (modifiable via `bEnabledAtStartup` dans les settings).
L'activation se fait via le subsystem (Blueprint : nœud
`Get PS_StrategicVisionSubsystem`, ou `UPS_StrategicVisionSubsystem::Get()` en C++) :
- `SetGlobalDisplayEnabled(true)` — active l'affichage stratégique (ex : entrée
en mode spectateur ou replay) ;
- `SetVisionConesEnabled(bool)` — uniquement les cônes de vision ;
- `SetCategoryEnabled(FName, bool)` — par catégorie (pour les futurs components) ;
- `SetTeamEnabled(EPS_StrategicTeam, bool)` — par camp (ex : ne montrer que les
ennemis).
Chaque component a aussi son interrupteur local `SetDisplayEnabled(bool)` (actif
par défaut — c'est l'état global qui masque tout au départ). Un component n'est
visible que si son interrupteur local ET les interrupteurs globaux le permettent.
Quand un cône est masqué, il ne fait plus aucune trace (coût nul).
### 4. Lisibilité des cônes superposés
Deux mécanismes, configurables par component (catégorie *Vision Cone > Rendering*) :
- **Contour** (`Show Outline`, `Outline Width`, `Outline Opacity`) : bande plus
opaque sur le pourtour du cône — chaque cône reste délimité même superposé.
- **Hachures procédurales** (`Hatching Enabled`, `Hatching Scale`,
`Hatching Intensity`) : rayures en espace monde dont **l'angle dépend du camp**
(45° joueur, 90° allié, 135° ennemi par défaut — réglable dans les settings).
Deux cônes superposés restent identifiables par l'orientation de leurs rayures.
Le matériau `M_VisionCone` fourni par le plugin gère ces options. Il est
régénérable via `Tools/generate_vision_cone_material.py` (commandlet Python).
### 5. Matériau personnalisé (optionnel)
Assigner un matériau via **Material Override** (component) ou
**Default Cone Material** (settings). Le component fournit au matériau :
- paramètre vecteur `Color` : couleur du camp (alpha = opacité) ;
- paramètres scalaires `Opacity`, `HatchAngle`, `HatchScale`, `HatchIntensity` ;
- vertex colors : couleur du camp (alpha = opacité) ;
- UV : U = position angulaire (0..1), V = distance normalisée (0..1) — pratique
pour un fondu vers le bord du cône.
Recette minimale : matériau *Translucent* / *Unlit*, **VertexColor** → Emissive
Color, **VertexColor.A** → Opacity. Laisser *Two Sided* décoché (la géométrie est
déjà double face).
> Note packaging : le matériau par défaut n'est référencé que par soft reference ;
> pour un build packagé, vérifier qu'il est bien cooké (Additional Assets to Cook,
> ou référence en dur via Material Override).
### Performance
- `ArcSegments` (48 par défaut) = nombre de line traces par mise à jour.
- `UpdateInterval` : 0 = chaque frame ; augmenter (ex : 0.05) si beaucoup de
personnages affichent leur cône simultanément.
## Roadmap
- [x] Cône de vision bloqué par les obstacles
- [x] Source de vision injectable (VR) via interface
- [x] Contour + hachures procédurales pour les superpositions
- [ ] Autres affichages stratégiques (trajectoires, zones d'alerte, ...) sous forme
de components héritant de `UPS_StrategicDisplayComponent`

View File

@@ -0,0 +1,18 @@
using UnrealBuildTool;
public class PS_StrategicVision : ModuleRules
{
public PS_StrategicVision(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"DeveloperSettings",
"ProceduralMeshComponent",
});
}
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicDisplayComponent.h"
#include "Engine/World.h"
#include "PS_StrategicVisionSettings.h"
#include "PS_StrategicVisionSubsystem.h"
UPS_StrategicDisplayComponent::UPS_StrategicDisplayComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UPS_StrategicDisplayComponent::SetTeam(EPS_StrategicTeam NewTeam)
{
if (Team != NewTeam)
{
Team = NewTeam;
RefreshDisplayState();
}
}
void UPS_StrategicDisplayComponent::SetDisplayEnabled(bool bEnabled)
{
if (bDisplayEnabled != bEnabled)
{
bDisplayEnabled = bEnabled;
RefreshDisplayState();
}
}
bool UPS_StrategicDisplayComponent::IsDisplayVisible() const
{
if (!bDisplayEnabled)
{
return false;
}
if (const UWorld* World = GetWorld())
{
if (World->IsGameWorld())
{
if (const UPS_StrategicVisionSubsystem* Subsystem = World->GetSubsystem<UPS_StrategicVisionSubsystem>())
{
return Subsystem->ShouldDisplay(GetDisplayCategory(), Team);
}
}
}
// Editor world (preview): only the local activation is considered.
return true;
}
FLinearColor UPS_StrategicDisplayComponent::GetTeamColor() const
{
return GetDefault<UPS_StrategicVisionSettings>()->GetTeamColor(Team);
}
void UPS_StrategicDisplayComponent::BeginPlay()
{
Super::BeginPlay();
if (UPS_StrategicVisionSubsystem* Subsystem = UPS_StrategicVisionSubsystem::Get(this))
{
Subsystem->OnDisplayStateChanged.AddUniqueDynamic(this, &UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged);
}
RefreshDisplayState();
}
void UPS_StrategicDisplayComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (UPS_StrategicVisionSubsystem* Subsystem = UPS_StrategicVisionSubsystem::Get(this))
{
Subsystem->OnDisplayStateChanged.RemoveDynamic(this, &UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged);
}
Super::EndPlay(EndPlayReason);
}
#if WITH_EDITOR
void UPS_StrategicDisplayComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
RefreshDisplayState();
}
#endif
void UPS_StrategicDisplayComponent::RefreshDisplayState()
{
OnDisplayStateChanged();
}
void UPS_StrategicDisplayComponent::HandleGlobalDisplayStateChanged()
{
RefreshDisplayState();
}

View File

@@ -0,0 +1,5 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, PS_StrategicVision)

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicVisionSettings.h"
#include "Materials/MaterialInterface.h"
namespace
{
FPS_StrategicTeamStyle MakeStyle(const FLinearColor& Color, float HatchAngleDegrees)
{
FPS_StrategicTeamStyle Style;
Style.Color = Color;
Style.HatchAngleDegrees = HatchAngleDegrees;
return Style;
}
}
UPS_StrategicVisionSettings::UPS_StrategicVisionSettings()
{
TeamStyles.Add(EPS_StrategicTeam::Player, MakeStyle(FLinearColor(0.05f, 0.45f, 1.0f), 45.f));
TeamStyles.Add(EPS_StrategicTeam::Ally, MakeStyle(FLinearColor(0.1f, 0.9f, 0.5f), 90.f));
TeamStyles.Add(EPS_StrategicTeam::Enemy, MakeStyle(FLinearColor(1.0f, 0.12f, 0.08f), 135.f));
TeamStyles.Add(EPS_StrategicTeam::Neutral, MakeStyle(FLinearColor(0.75f, 0.75f, 0.75f), 0.f));
// Material shipped in the plugin content (procedural hatching).
DefaultConeMaterial = TSoftObjectPtr<UMaterialInterface>(
FSoftObjectPath(TEXT("/PS_StrategicVision/Materials/M_VisionCone.M_VisionCone")));
}
FLinearColor UPS_StrategicVisionSettings::GetTeamColor(EPS_StrategicTeam Team) const
{
return GetTeamStyle(Team).Color;
}
FPS_StrategicTeamStyle UPS_StrategicVisionSettings::GetTeamStyle(EPS_StrategicTeam Team) const
{
if (const FPS_StrategicTeamStyle* Style = TeamStyles.Find(Team))
{
return *Style;
}
return FPS_StrategicTeamStyle();
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicVisionSubsystem.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "PS_StrategicVisionSettings.h"
void UPS_StrategicVisionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
bGlobalEnabled = GetDefault<UPS_StrategicVisionSettings>()->bEnabledAtStartup;
}
UPS_StrategicVisionSubsystem* UPS_StrategicVisionSubsystem::Get(const UObject* WorldContextObject)
{
if (GEngine)
{
if (const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull))
{
return World->GetSubsystem<UPS_StrategicVisionSubsystem>();
}
}
return nullptr;
}
void UPS_StrategicVisionSubsystem::SetGlobalDisplayEnabled(bool bEnabled)
{
if (bGlobalEnabled != bEnabled)
{
bGlobalEnabled = bEnabled;
NotifyStateChanged();
}
}
void UPS_StrategicVisionSubsystem::SetCategoryEnabled(FName Category, bool bEnabled)
{
const bool bChanged = bEnabled ? (DisabledCategories.Remove(Category) > 0) : !DisabledCategories.Contains(Category);
if (!bEnabled)
{
DisabledCategories.Add(Category);
}
if (bChanged)
{
NotifyStateChanged();
}
}
void UPS_StrategicVisionSubsystem::SetTeamEnabled(EPS_StrategicTeam Team, bool bEnabled)
{
const bool bChanged = bEnabled ? (DisabledTeams.Remove(Team) > 0) : !DisabledTeams.Contains(Team);
if (!bEnabled)
{
DisabledTeams.Add(Team);
}
if (bChanged)
{
NotifyStateChanged();
}
}
void UPS_StrategicVisionSubsystem::NotifyStateChanged()
{
OnDisplayStateChanged.Broadcast();
}

View File

@@ -0,0 +1,5 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicVisionTypes.h"
const FName FPS_StrategicDisplayCategories::VisionCone(TEXT("VisionCone"));

View File

@@ -0,0 +1,446 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_VisionConeComponent.h"
#include "CollisionQueryParams.h"
#include "Engine/HitResult.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "ProceduralMeshComponent.h"
#include "PS_StrategicVisionSettings.h"
#include "PS_VisionSourceInterface.h"
namespace
{
// Engine materials used as a last resort when no material is configured.
const TCHAR* GFallbackTranslucentMaterialPath = TEXT("/Engine/EngineDebugMaterials/M_SimpleUnlitTranslucent.M_SimpleUnlitTranslucent");
const TCHAR* GFallbackOpaqueMaterialPath = TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial");
const FName GColorParam(TEXT("Color"));
const FName GOpacityParam(TEXT("Opacity"));
const FName GHatchAngleParam(TEXT("HatchAngle"));
const FName GHatchScaleParam(TEXT("HatchScale"));
const FName GHatchIntensityParam(TEXT("HatchIntensity"));
// Small lift of the outline above the fill to avoid z-fighting.
constexpr float GOutlineZLift = 0.4f;
// Ground search distance below the vision origin.
constexpr float GGroundSearchDepth = 1000.f;
/** Inward inset of a closed polygon (XY), used to build the outline strip. */
void InsetPolygon(const TArray<FVector2D>& Points, float Width, TArray<FVector2D>& OutInner)
{
const int32 Num = Points.Num();
OutInner.SetNum(Num);
// Polygon orientation (signed area, shoelace formula).
double SignedArea = 0.0;
for (int32 Index = 0; Index < Num; ++Index)
{
const FVector2D& A = Points[Index];
const FVector2D& B = Points[(Index + 1) % Num];
SignedArea += A.X * B.Y - B.X * A.Y;
}
const float Orient = (SignedArea >= 0.0) ? 1.f : -1.f;
for (int32 Index = 0; Index < Num; ++Index)
{
const FVector2D& Prev = Points[(Index - 1 + Num) % Num];
const FVector2D& Cur = Points[Index];
const FVector2D& Next = Points[(Index + 1) % Num];
const FVector2D DirPrev = (Cur - Prev).GetSafeNormal();
const FVector2D DirNext = (Next - Cur).GetSafeNormal();
// Outward normal of each adjacent edge.
const FVector2D NormPrev = FVector2D(DirPrev.Y, -DirPrev.X) * Orient;
const FVector2D NormNext = FVector2D(DirNext.Y, -DirNext.X) * Orient;
FVector2D Bisector = (NormPrev + NormNext).GetSafeNormal();
if (Bisector.IsNearlyZero())
{
Bisector = !NormPrev.IsNearlyZero() ? NormPrev : NormNext;
}
if (Bisector.IsNearlyZero())
{
OutInner[Index] = Cur;
continue;
}
const FVector2D Reference = !NormPrev.IsNearlyZero() ? NormPrev : NormNext;
// Miter length, clamped for very sharp angles.
const float CosHalf = FMath::Max(0.35f, FVector2D::DotProduct(Bisector, Reference));
OutInner[Index] = Cur - Bisector * (Width / CosHalf);
}
}
/** Appends a double-sided quad (4 triangles) to the index list. */
void AddDoubleSidedQuad(TArray<int32>& Triangles, int32 Outer0, int32 Inner0, int32 Outer1, int32 Inner1)
{
Triangles.Append({ Outer0, Outer1, Inner0, Inner0, Outer1, Inner1 });
Triangles.Append({ Outer0, Inner0, Outer1, Inner0, Inner1, Outer1 });
}
}
UPS_VisionConeComponent::UPS_VisionConeComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
// After actor movement, so that the cone follows without lag.
PrimaryComponentTick.TickGroup = TG_PostUpdateWork;
bTickInEditor = true;
}
void UPS_VisionConeComponent::OnRegister()
{
Super::OnRegister();
EnsureConeMesh();
EnsureMaterial();
ApplyTeamStyle();
}
void UPS_VisionConeComponent::OnComponentDestroyed(bool bDestroyingHierarchy)
{
if (ConeMesh)
{
ConeMesh->DestroyComponent();
ConeMesh = nullptr;
}
Super::OnComponentDestroyed(bDestroyingHierarchy);
}
void UPS_VisionConeComponent::BeginPlay()
{
Super::BeginPlay();
SetComponentTickInterval(UpdateInterval);
}
void UPS_VisionConeComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (IsDisplayVisible())
{
RebuildCone();
}
}
void UPS_VisionConeComponent::OnDisplayStateChanged()
{
const bool bShouldDisplay = IsDisplayVisible();
if (ConeMesh)
{
ConeMesh->SetVisibility(bShouldDisplay);
}
// No traces nor rebuilds while the cone is hidden.
SetComponentTickEnabled(bShouldDisplay);
SetComponentTickInterval(UpdateInterval);
if (bShouldDisplay)
{
EnsureMaterial();
ApplyTeamStyle();
RebuildCone();
}
}
void UPS_VisionConeComponent::ResolveVisionSource(FVector& OutOrigin, FVector& OutForward) const
{
OutOrigin = GetComponentLocation();
// Only the yaw is used: the cone plane stays horizontal.
OutForward = FRotator(0.f, GetComponentRotation().Yaw, 0.f).Vector();
if (!bUseOwnerVisionSource)
{
return;
}
AActor* Owner = GetOwner();
const UWorld* World = GetWorld();
if (!Owner || !World || !World->IsGameWorld() || !Owner->Implements<UPS_VisionSource>())
{
return;
}
FVector SourceLocation = FVector::ZeroVector;
FVector SourceForward = FVector::ForwardVector;
if (!IPS_VisionSource::Execute_GetVisionViewPoint(Owner, SourceLocation, SourceForward))
{
return;
}
OutOrigin = SourceLocation;
const FVector FlatForward(SourceForward.X, SourceForward.Y, 0.f);
if (!FlatForward.IsNearlyZero())
{
OutForward = FlatForward.GetSafeNormal();
}
// Otherwise (near-vertical gaze): keep the component yaw.
}
void UPS_VisionConeComponent::EnsureConeMesh()
{
if (ConeMesh || !GetWorld())
{
return;
}
ConeMesh = NewObject<UProceduralMeshComponent>(this, NAME_None, RF_Transient);
ConeMesh->SetupAttachment(this);
// Absolute transform: the fan is built in world space around the vision
// origin, which can differ from the component transform.
ConeMesh->SetUsingAbsoluteLocation(true);
ConeMesh->SetUsingAbsoluteRotation(true);
ConeMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
ConeMesh->SetGenerateOverlapEvents(false);
ConeMesh->SetCastShadow(false);
ConeMesh->bReceivesDecals = false;
ConeMesh->SetTranslucentSortPriority(100);
ConeMesh->RegisterComponent();
ConeMesh->SetWorldLocationAndRotation(GetComponentLocation(), FQuat::Identity);
}
void UPS_VisionConeComponent::EnsureMaterial()
{
if (!ConeMesh)
{
return;
}
UMaterialInterface* BaseMaterial = MaterialOverride;
if (!BaseMaterial)
{
BaseMaterial = GetDefault<UPS_StrategicVisionSettings>()->DefaultConeMaterial.LoadSynchronous();
}
if (!BaseMaterial)
{
BaseMaterial = LoadObject<UMaterialInterface>(nullptr, GFallbackTranslucentMaterialPath);
}
if (!BaseMaterial)
{
BaseMaterial = LoadObject<UMaterialInterface>(nullptr, GFallbackOpaqueMaterialPath);
}
if (!BaseMaterial)
{
return;
}
if (!FillMID || FillMID->Parent != BaseMaterial)
{
FillMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
}
if (!OutlineMID || OutlineMID->Parent != BaseMaterial)
{
OutlineMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
}
ConeMesh->SetMaterial(0, FillMID);
ConeMesh->SetMaterial(1, OutlineMID);
}
void UPS_VisionConeComponent::ApplyTeamStyle()
{
const FPS_StrategicTeamStyle Style = GetDefault<UPS_StrategicVisionSettings>()->GetTeamStyle(GetTeam());
if (FillMID)
{
FLinearColor FillColor = Style.Color;
FillColor.A = Opacity;
FillMID->SetVectorParameterValue(GColorParam, FillColor);
FillMID->SetScalarParameterValue(GOpacityParam, Opacity);
FillMID->SetScalarParameterValue(GHatchAngleParam, Style.HatchAngleDegrees);
FillMID->SetScalarParameterValue(GHatchScaleParam, HatchingScale);
FillMID->SetScalarParameterValue(GHatchIntensityParam, bHatchingEnabled ? HatchingIntensity : 0.f);
}
if (OutlineMID)
{
FLinearColor OutlineColor = Style.Color;
OutlineColor.A = OutlineOpacity;
OutlineMID->SetVectorParameterValue(GColorParam, OutlineColor);
OutlineMID->SetScalarParameterValue(GOpacityParam, OutlineOpacity);
OutlineMID->SetScalarParameterValue(GHatchIntensityParam, 0.f);
}
}
void UPS_VisionConeComponent::RebuildCone()
{
UWorld* World = GetWorld();
if (!World || !ConeMesh)
{
return;
}
FCollisionQueryParams QueryParams(TEXT("PS_VisionCone"), bTraceComplex);
if (const AActor* Owner = GetOwner())
{
QueryParams.AddIgnoredActor(Owner);
}
FVector Origin, Forward;
ResolveVisionSource(Origin, Forward);
// Cone plane: always horizontal, projected onto the ground when requested.
float PlaneZ = GetComponentLocation().Z + GroundOffset;
if (bProjectToGround)
{
FHitResult GroundHit;
const FVector GroundStart(Origin.X, Origin.Y, Origin.Z + TraceHeight);
const FVector GroundEnd(Origin.X, Origin.Y, Origin.Z - GGroundSearchDepth);
if (World->LineTraceSingleByChannel(GroundHit, GroundStart, GroundEnd, TraceChannel, QueryParams))
{
PlaneZ = GroundHit.ImpactPoint.Z + GroundOffset;
}
}
// The mesh is anchored on the cone plane; vertices are local to that point.
ConeMesh->SetWorldLocation(FVector(Origin.X, Origin.Y, PlaneZ));
const int32 RayCount = FMath::Clamp(ArcSegments, 4, 256) + 1;
const float HalfAngle = FMath::Clamp(ViewAngleDegrees, 1.f, 360.f) * 0.5f;
const FVector TraceOrigin = Origin + FVector(0.f, 0.f, TraceHeight);
const FLinearColor TeamColor = GetTeamColor();
FLinearColor FillColor = TeamColor;
FillColor.A = Opacity;
TArray<FVector> Vertices;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FLinearColor> Colors;
Vertices.Reserve(RayCount + 1);
Normals.Reserve(RayCount + 1);
UVs.Reserve(RayCount + 1);
Colors.Reserve(RayCount + 1);
// Center vertex (cone origin).
Vertices.Add(FVector::ZeroVector);
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(0.5f, 0.f));
Colors.Add(FillColor);
for (int32 RayIndex = 0; RayIndex < RayCount; ++RayIndex)
{
const float Alpha = static_cast<float>(RayIndex) / static_cast<float>(RayCount - 1);
const float AngleDeg = FMath::Lerp(-HalfAngle, HalfAngle, Alpha);
const FVector Direction = Forward.RotateAngleAxis(AngleDeg, FVector::UpVector);
float HitDistance = ViewDistance;
FHitResult Hit;
if (World->LineTraceSingleByChannel(Hit, TraceOrigin, TraceOrigin + Direction * ViewDistance, TraceChannel, QueryParams))
{
HitDistance = Hit.Distance;
}
Vertices.Add(FVector(Direction.X * HitDistance, Direction.Y * HitDistance, 0.f));
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(Alpha, HitDistance / ViewDistance));
Colors.Add(FillColor);
}
const TArray<FProcMeshTangent> NoTangents;
if (Vertices.Num() == LastFillVertexCount)
{
// Same topology: only update the vertices (cheaper).
ConeMesh->UpdateMeshSection_LinearColor(0, Vertices, Normals, UVs, Colors, NoTangents);
}
else
{
// Triangles in both windings to stay visible from any camera angle.
TArray<int32> Triangles;
Triangles.Reserve((RayCount - 1) * 6);
for (int32 SegmentIndex = 0; SegmentIndex < RayCount - 1; ++SegmentIndex)
{
Triangles.Append({ 0, 1 + SegmentIndex, 2 + SegmentIndex });
Triangles.Append({ 0, 2 + SegmentIndex, 1 + SegmentIndex });
}
ConeMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UVs, Colors, NoTangents, false);
LastFillVertexCount = Vertices.Num();
}
if (bShowOutline && OutlineWidth > KINDA_SMALL_NUMBER)
{
BuildOutlineSection(Vertices, TeamColor);
}
else if (LastOutlineVertexCount > 0)
{
ConeMesh->ClearMeshSection(1);
LastOutlineVertexCount = 0;
}
}
void UPS_VisionConeComponent::BuildOutlineSection(const TArray<FVector>& FillVertices, const FLinearColor& TeamColor)
{
// Closed cone perimeter: center + arc (the fill already provides these points).
const int32 NumPerimeter = FillVertices.Num();
if (NumPerimeter < 3)
{
return;
}
TArray<FVector2D> Perimeter;
Perimeter.Reserve(NumPerimeter);
for (const FVector& Vertex : FillVertices)
{
Perimeter.Add(FVector2D(Vertex.X, Vertex.Y));
}
TArray<FVector2D> Inner;
InsetPolygon(Perimeter, OutlineWidth, Inner);
FLinearColor OutlineColor = TeamColor;
OutlineColor.A = OutlineOpacity;
TArray<FVector> Vertices;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FLinearColor> Colors;
Vertices.Reserve(NumPerimeter * 2);
Normals.Reserve(NumPerimeter * 2);
UVs.Reserve(NumPerimeter * 2);
Colors.Reserve(NumPerimeter * 2);
for (int32 Index = 0; Index < NumPerimeter; ++Index)
{
const float U = static_cast<float>(Index) / static_cast<float>(NumPerimeter - 1);
Vertices.Add(FVector(Perimeter[Index].X, Perimeter[Index].Y, GOutlineZLift));
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(U, 0.f));
Colors.Add(OutlineColor);
Vertices.Add(FVector(Inner[Index].X, Inner[Index].Y, GOutlineZLift));
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(U, 1.f));
Colors.Add(OutlineColor);
}
const TArray<FProcMeshTangent> NoTangents;
if (Vertices.Num() == LastOutlineVertexCount)
{
ConeMesh->UpdateMeshSection_LinearColor(1, Vertices, Normals, UVs, Colors, NoTangents);
}
else
{
TArray<int32> Triangles;
Triangles.Reserve(NumPerimeter * 12);
for (int32 Index = 0; Index < NumPerimeter; ++Index)
{
const int32 NextIndex = (Index + 1) % NumPerimeter;
AddDoubleSidedQuad(Triangles, Index * 2, Index * 2 + 1, NextIndex * 2, NextIndex * 2 + 1);
}
ConeMesh->CreateMeshSection_LinearColor(1, Vertices, Triangles, Normals, UVs, Colors, NoTangents, false);
LastOutlineVertexCount = Vertices.Num();
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "PS_StrategicVisionTypes.h"
#include "PS_StrategicDisplayComponent.generated.h"
/**
* Base class of the strategic displays placed on characters
* (vision cone, and other visualizations to come).
*
* Handles the team (color), the local activation of the component and the
* synchronization with the global state of the UPS_StrategicVisionSubsystem.
*/
UCLASS(Abstract, ClassGroup = (PS_StrategicVision), HideCategories = (Physics, Collision, Mobility))
class PS_STRATEGICVISION_API UPS_StrategicDisplayComponent : public USceneComponent
{
GENERATED_BODY()
public:
UPS_StrategicDisplayComponent();
/** Changes the team (updates the display color). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
void SetTeam(EPS_StrategicTeam NewTeam);
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
EPS_StrategicTeam GetTeam() const { return Team; }
/** Enables / disables this specific display (independently of the global state). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
void SetDisplayEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
bool IsDisplayEnabled() const { return bDisplayEnabled; }
/** Effective visibility: local activation AND global subsystem state. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
bool IsDisplayVisible() const;
/** Color of the current team, read from the plugin settings. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
FLinearColor GetTeamColor() const;
/** Category declared by each display type (see FPS_StrategicDisplayCategories). */
virtual FName GetDisplayCategory() const { return NAME_None; }
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
/** Called when the effective visibility or the team changes. */
virtual void OnDisplayStateChanged() {}
/** Re-evaluates the effective visibility and notifies the child class. */
void RefreshDisplayState();
/** Team of the owning character, drives the display color. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
EPS_StrategicTeam Team = EPS_StrategicTeam::Enemy;
/** Local activation state of this display. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
bool bDisplayEnabled = true;
private:
UFUNCTION()
void HandleGlobalDisplayStateChanged();
};

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "PS_StrategicVisionTypes.h"
#include "PS_StrategicVisionSettings.generated.h"
class UMaterialInterface;
/** Display style of a team. */
USTRUCT(BlueprintType)
struct PS_STRATEGICVISION_API FPS_StrategicTeamStyle
{
GENERATED_BODY()
/** Team color. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style")
FLinearColor Color = FLinearColor::White;
/**
* Procedural hatching angle, in degrees. Giving each team a different
* angle keeps overlapping cones readable.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (ClampMin = "0.0", ClampMax = "180.0"))
float HatchAngleDegrees = 0.f;
};
/**
* PS Strategic Vision plugin settings.
* Available in Project Settings > Plugins > PS Strategic Vision.
*/
UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "PS Strategic Vision"))
class PS_STRATEGICVISION_API UPS_StrategicVisionSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UPS_StrategicVisionSettings();
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
/**
* If false (default), no strategic display is visible when a game starts:
* activation is done explicitly through the UPS_StrategicVisionSubsystem
* (spectator view, replay...).
*/
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Activation")
bool bEnabledAtStartup = false;
/** Style (color, hatching) associated with each team. */
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Teams")
TMap<EPS_StrategicTeam, FPS_StrategicTeamStyle> TeamStyles;
/**
* Default material of the vision cones. The plugin ships
* /PS_StrategicVision/Materials/M_VisionCone (translucent, procedural
* hatching). Expected parameters: vector "Color", scalars "Opacity",
* "HatchAngle", "HatchScale", "HatchDuty", "HatchIntensity"; the color is
* also provided through the mesh vertex colors.
* If empty or missing, a translucent engine material is used instead.
*/
UPROPERTY(EditAnywhere, Config, BlueprintReadOnly, Category = "Rendering")
TSoftObjectPtr<UMaterialInterface> DefaultConeMaterial;
/** Color of the requested team (white if not configured). */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
FLinearColor GetTeamColor(EPS_StrategicTeam Team) const;
/** Full style of the requested team (default style if not configured). */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
FPS_StrategicTeamStyle GetTeamStyle(EPS_StrategicTeam Team) const;
};

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "PS_StrategicVisionTypes.h"
#include "PS_StrategicVisionSubsystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPS_StrategicDisplayStateChanged);
/**
* World subsystem driving the global strategic display state.
* Allows enabling / disabling every display, one category (e.g. vision cones)
* or a whole team depending on the game state (spectator view, replay...).
* Every UPS_StrategicDisplayComponent subscribes to it automatically.
*/
UCLASS()
class PS_STRATEGICVISION_API UPS_StrategicVisionSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
/** Shortcut to access the subsystem from any world context. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision", meta = (WorldContext = "WorldContextObject"))
static UPS_StrategicVisionSubsystem* Get(const UObject* WorldContextObject);
/** Master switch of every strategic display. */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
void SetGlobalDisplayEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsGlobalDisplayEnabled() const { return bGlobalEnabled; }
/** Enables / disables one display category (e.g. "VisionCone"). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
void SetCategoryEnabled(FName Category, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsCategoryEnabled(FName Category) const { return !DisabledCategories.Contains(Category); }
/** Enables / disables the displays of one team (e.g. only show enemies). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
void SetTeamEnabled(EPS_StrategicTeam Team, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsTeamEnabled(EPS_StrategicTeam Team) const { return !DisabledTeams.Contains(Team); }
/** Shortcut: enables / disables every vision cone. */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision|Vision Cone")
void SetVisionConesEnabled(bool bEnabled) { SetCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone, bEnabled); }
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision|Vision Cone")
bool AreVisionConesEnabled() const { return IsCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone); }
/** Combined state used by the components to know whether they should be shown. */
bool ShouldDisplay(FName Category, EPS_StrategicTeam Team) const
{
return bGlobalEnabled && IsCategoryEnabled(Category) && IsTeamEnabled(Team);
}
/** Fired on every global state change (components subscribe to it). */
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Strategic Vision")
FPS_StrategicDisplayStateChanged OnDisplayStateChanged;
private:
void NotifyStateChanged();
// Initial state read from UPS_StrategicVisionSettings::bEnabledAtStartup (false by default).
bool bGlobalEnabled = false;
TSet<FName> DisabledCategories;
TSet<EPS_StrategicTeam> DisabledTeams;
};

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "PS_StrategicVisionTypes.generated.h"
/** Team of a character, used to color / filter the strategic displays. */
UENUM(BlueprintType)
enum class EPS_StrategicTeam : uint8
{
Player,
Ally,
Enemy,
Neutral
};
/**
* Strategic display categories. Each component type declares its category,
* which allows enabling / disabling it globally through the subsystem.
*/
struct PS_STRATEGICVISION_API FPS_StrategicDisplayCategories
{
static const FName VisionCone;
};

View File

@@ -0,0 +1,156 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/EngineTypes.h"
#include "PS_StrategicDisplayComponent.h"
#include "PS_VisionConeComponent.generated.h"
class UMaterialInstanceDynamic;
class UMaterialInterface;
class UProceduralMeshComponent;
/**
* Displays the vision cone of a character as a procedural mesh.
* The cone is cut by obstacles (walls) through line traces, colored by team,
* and its plane always stays horizontal whatever the actor tilt is.
* Hidden by default: activation is done through the
* UPS_StrategicVisionSubsystem (spectator view, replay).
*
* The vision origin and direction come from the component transform, or from
* the PS_VisionSource interface when the owning actor implements it
* (e.g. a VR pawn whose camera moves under a static actor).
*/
UCLASS(ClassGroup = (PS_StrategicVision), meta = (BlueprintSpawnableComponent, DisplayName = "PS Vision Cone"))
class PS_STRATEGICVISION_API UPS_VisionConeComponent : public UPS_StrategicDisplayComponent
{
GENERATED_BODY()
public:
UPS_VisionConeComponent();
virtual FName GetDisplayCategory() const override { return FPS_StrategicDisplayCategories::VisionCone; }
/** Vision range, in cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "10.0", Units = "cm"))
float ViewDistance = 1500.f;
/** Total cone angle, in degrees (e.g. 90 = 45 on each side). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "1.0", ClampMax = "360.0", Units = "deg"))
float ViewAngleDegrees = 90.f;
/**
* If true and the owning actor implements the PS_VisionSource interface,
* the vision origin and direction come from the interface (e.g. VR camera)
* instead of this component transform.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone")
bool bUseOwnerVisionSource = true;
/** If true, the cone is drawn at the height of the ground detected under the vision origin. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone")
bool bProjectToGround = true;
/** Vertical offset of the mesh above the ground, to avoid z-fighting. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (Units = "cm"))
float GroundOffset = 3.f;
// --- Rendering ---
/** Opacity of the cone fill (0 = invisible, 1 = opaque). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float Opacity = 0.35f;
/** Draws a more opaque outline around the cone (readability when cones overlap). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
bool bShowOutline = true;
/** Outline width, in cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.5", Units = "cm", EditCondition = "bShowOutline"))
float OutlineWidth = 6.f;
/** Outline opacity. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bShowOutline"))
float OutlineOpacity = 0.9f;
/**
* Procedural hatching in the fill: the angle depends on the team
* (plugin settings), which keeps overlapping cones readable.
* Requires the plugin M_VisionCone material (or a compatible one).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
bool bHatchingEnabled = true;
/** Hatching spacing, in cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "1.0", Units = "cm", EditCondition = "bHatchingEnabled"))
float HatchingScale = 30.f;
/** Hatching intensity (0 = flat fill, 1 = hard stripes). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bHatchingEnabled"))
float HatchingIntensity = 0.5f;
/**
* Cone material for this component (takes precedence over the settings one).
* Provided parameters: vector "Color" (alpha = opacity), scalars "Opacity",
* "HatchAngle", "HatchScale", "HatchIntensity"; the color is also written
* into the mesh vertex colors.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
TObjectPtr<UMaterialInterface> MaterialOverride;
// --- Quality ---
/** Number of arc segments: higher = more precise but more traces. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "4", ClampMax = "256"))
int32 ArcSegments = 48;
/** Update interval in seconds (0 = every frame). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "0.0", Units = "s"))
float UpdateInterval = 0.f;
// --- Collision ---
/** Collision channel used to block the cone on obstacles. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
/** Traces against complex geometry (more precise, more expensive). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
bool bTraceComplex = false;
/** Height (relative to the vision origin) at which the traces are cast. Set to 0 if the origin is already at eye level (VR camera). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision", meta = (Units = "cm"))
float TraceHeight = 50.f;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
virtual void OnRegister() override;
virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override;
virtual void BeginPlay() override;
virtual void OnDisplayStateChanged() override;
private:
/** Internal procedural mesh: section 0 = fill, section 1 = outline. */
UPROPERTY(Transient)
TObjectPtr<UProceduralMeshComponent> ConeMesh;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> FillMID;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> OutlineMID;
int32 LastFillVertexCount = 0;
int32 LastOutlineVertexCount = 0;
/** Vision origin and (horizontal) direction: PS_VisionSource interface or component transform. */
void ResolveVisionSource(FVector& OutOrigin, FVector& OutForward) const;
void EnsureConeMesh();
void EnsureMaterial();
void ApplyTeamStyle();
void RebuildCone();
void BuildOutlineSection(const TArray<FVector>& FillVertices, const FLinearColor& TeamColor);
};

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PS_VisionSourceInterface.generated.h"
UINTERFACE(BlueprintType, Blueprintable)
class PS_STRATEGICVISION_API UPS_VisionSource : public UInterface
{
GENERATED_BODY()
};
/**
* Implement this (C++ or Blueprint) on an actor to provide the real vision
* origin and direction when the actor transform does not reflect them.
* Typical case: a VR pawn whose actor stays at the origin while the camera
* moves underneath — return the camera position/direction here.
*/
class PS_STRATEGICVISION_API IPS_VisionSource
{
GENERATED_BODY()
public:
/**
* Vision origin and direction to use for the strategic displays.
* Return false to fall back to the component transform (default behavior).
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|Strategic Vision")
bool GetVisionViewPoint(FVector& OutLocation, FVector& OutForwardDirection);
virtual bool GetVisionViewPoint_Implementation(FVector& OutLocation, FVector& OutForwardDirection) { return false; }
};

View File

@@ -0,0 +1,90 @@
# Copyright (c) 2026 ASTERION VR. All rights reserved.
#
# Generates the /PS_StrategicVision/Materials/M_VisionCone material:
# translucent, unlit, colored through vertex color, with world-space
# procedural hatching (parameters: HatchAngle, HatchScale, HatchDuty,
# HatchIntensity).
#
# Run from the editor (PythonScriptPlugin enabled):
# UnrealEditor-Cmd.exe <project.uproject> -run=pythonscript -script="<this file>"
import unreal
PKG_DIR = "/PS_StrategicVision/Materials"
NAME = "M_VisionCone"
FULL_PATH = f"{PKG_DIR}/{NAME}"
mel = unreal.MaterialEditingLibrary
asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
if asset_subsystem.does_asset_exist(FULL_PATH):
unreal.log(f"[PS_StrategicVision] Deleting previous {FULL_PATH}")
asset_subsystem.delete_asset(FULL_PATH)
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
material = asset_tools.create_asset(NAME, PKG_DIR, unreal.Material, unreal.MaterialFactoryNew())
if material is None:
raise SystemExit("[PS_StrategicVision] FAILED: create_asset returned None")
material.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_UNLIT)
material.set_editor_property("blend_mode", unreal.BlendMode.BLEND_TRANSLUCENT)
def scalar_param(name, value, x, y):
param = mel.create_material_expression(material, unreal.MaterialExpressionScalarParameter, x, y)
param.set_editor_property("parameter_name", name)
param.set_editor_property("default_value", value)
return param
vertex_color = mel.create_material_expression(material, unreal.MaterialExpressionVertexColor, -700, -200)
world_pos = mel.create_material_expression(material, unreal.MaterialExpressionWorldPosition, -1050, 40)
hatch_angle = scalar_param("HatchAngle", 45.0, -1050, 160)
hatch_scale = scalar_param("HatchScale", 30.0, -1050, 260)
hatch_duty = scalar_param("HatchDuty", 0.5, -1050, 360)
hatch_intensity = scalar_param("HatchIntensity", 0.0, -1050, 460)
# Procedural hatching: world-space (XY) oriented stripes.
custom = mel.create_material_expression(material, unreal.MaterialExpressionCustom, -650, 150)
custom.set_editor_property("description", "ProceduralHatch")
custom.set_editor_property("output_type", unreal.CustomMaterialOutputType.CMOT_FLOAT1)
custom.set_editor_property(
"code",
"float a = radians(AngleDeg);\n"
"float2 dir = float2(cos(a), sin(a));\n"
"float d = dot(WorldPos.xy, dir) / max(Scale, 1.0);\n"
"float stripe = step(frac(d), saturate(Duty));\n"
"return lerp(1.0, stripe, saturate(Intensity));\n",
)
inputs = []
for input_name in ("WorldPos", "AngleDeg", "Scale", "Duty", "Intensity"):
custom_input = unreal.CustomInput()
custom_input.set_editor_property("input_name", input_name)
inputs.append(custom_input)
custom.set_editor_property("inputs", inputs)
# Final opacity = vertex color alpha * hatching factor.
multiply = mel.create_material_expression(material, unreal.MaterialExpressionMultiply, -300, 60)
connections = [
mel.connect_material_expressions(world_pos, "", custom, "WorldPos"),
mel.connect_material_expressions(hatch_angle, "", custom, "AngleDeg"),
mel.connect_material_expressions(hatch_scale, "", custom, "Scale"),
mel.connect_material_expressions(hatch_duty, "", custom, "Duty"),
mel.connect_material_expressions(hatch_intensity, "", custom, "Intensity"),
mel.connect_material_expressions(vertex_color, "A", multiply, "A"),
mel.connect_material_expressions(custom, "", multiply, "B"),
mel.connect_material_property(vertex_color, "", unreal.MaterialProperty.MP_EMISSIVE_COLOR),
mel.connect_material_property(multiply, "", unreal.MaterialProperty.MP_OPACITY),
]
if not all(connections):
raise SystemExit(f"[PS_StrategicVision] FAILED: graph connections = {connections}")
mel.recompile_material(material)
if not asset_subsystem.save_asset(FULL_PATH, only_if_is_dirty=False):
raise SystemExit("[PS_StrategicVision] FAILED: save_asset")
unreal.log(f"[PS_StrategicVision] OK: {FULL_PATH} generated and saved")