Compare commits

..

2 Commits

Author SHA1 Message Date
2fd45af39a 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>
2026-07-22 13:24:05 +02:00
d2fc5a8fbb Projet de test UE 5.7 pour le plugin PS_StrategicVision
Projet C++ vide servant de banc de test au plugin (reference en depot
imbrique dans Plugins/, a convertir en submodule quand il aura un remote).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:22:14 +02:00
18 changed files with 649 additions and 216 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@
"FriendlyName": "PS Strategic Vision",
"Description": "Affichage strategique pour vues spectateur et replays : cones de vision, et autres visualisations a venir.",
"Category": "Visualization",
"CreatedBy": "j.foucher",
"CreatedBy": "ASTERION VR",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",

View File

@@ -19,12 +19,14 @@ avec le projet).
## Contenu
| Classe | Rôle |
| Élément | Rôle |
|---|---|
| `UPS_VisionConeComponent` | Component à poser sur un personnage : affiche son cône de vision (mesh procédural, bloqué par les murs). |
| `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 : couleurs des camps, matériau par défaut. |
| `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
@@ -33,60 +35,81 @@ avec le projet).
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
(configurable dans *Project Settings > Plugins > PS Strategic Vision*).
- **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).
- **Flatten On Ground** : le cône reste à plat et se projette au sol (recommandé
pour une vue spectateur top-down).
Le cône suit automatiquement la position et le yaw du component. Pour qu'il suive
le regard de la tête plutôt que le corps, attacher le component au bone/socket voulu.
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. Activer / désactiver selon l'état de jeu
### 2. Cas VR : injecter la position/direction de la caméra
Par component :
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** :
- `SetDisplayEnabled(bool)` — interrupteur local du component.
- `Out Location` = position monde de la caméra ;
- `Out Forward Direction` = direction du regard (seul le cap horizontal est utilisé) ;
- retourner `true`.
Globalement, via le subsystem (Blueprint : nœud `Get PS_StrategicVisionSubsystem`,
ou `UPS_StrategicVisionSubsystem::Get(WorldContext)` en C++) :
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.
- `SetGlobalDisplayEnabled(bool)` — tout l'affichage stratégique (ex : entrée/sortie
du 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).
### 3. Activer / désactiver selon l'état de jeu
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).
**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++) :
### 3. Matériau (optionnel)
- `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).
Sans configuration, le cône utilise un matériau translucide du moteur
(`M_SimpleUnlitTranslucent`) teinté à la couleur du camp — fonctionnel
immédiatement, suffisant en développement.
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).
Pour un rendu personnalisé (dégradé, scanlines, bord lumineux...), créer un
matériau dans le Content du plugin à `/PS_StrategicVision/Materials/M_VisionCone`
(chemin par défaut des settings) ou l'assigner via **Material Override** sur le
component. Le component fournit au matériau :
### 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ètre scalaire `Opacity` : opacité ;
- vertex colors : couleur du camp ;
- 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*, node **VertexColor** → Emissive
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 : si le matériau n'est référencé que par les settings (soft
> reference), l'ajouter à la liste des assets toujours cookés, ou le référencer en
> dur via Material Override.
> 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
@@ -97,5 +120,7 @@ déjà double face).
## 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

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicDisplayComponent.h"
@@ -47,7 +47,7 @@ bool UPS_StrategicDisplayComponent::IsDisplayVisible() const
}
}
// Monde editeur (preview) : on suit uniquement l'activation locale.
// Editor world (preview): only the local activation is considered.
return true;
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "Modules/ModuleManager.h"

View File

@@ -1,26 +1,42 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// 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()
{
TeamColors.Add(EPS_StrategicTeam::Player, FLinearColor(0.05f, 0.45f, 1.0f));
TeamColors.Add(EPS_StrategicTeam::Ally, FLinearColor(0.1f, 0.9f, 0.5f));
TeamColors.Add(EPS_StrategicTeam::Enemy, FLinearColor(1.0f, 0.12f, 0.08f));
TeamColors.Add(EPS_StrategicTeam::Neutral, FLinearColor(0.75f, 0.75f, 0.75f));
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));
// Materiau optionnel a creer dans le Content du plugin (voir README).
// 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
{
if (const FLinearColor* Color = TeamColors.Find(Team))
return GetTeamStyle(Team).Color;
}
FPS_StrategicTeamStyle UPS_StrategicVisionSettings::GetTeamStyle(EPS_StrategicTeam Team) const
{
return *Color;
if (const FPS_StrategicTeamStyle* Style = TeamStyles.Find(Team))
{
return *Style;
}
return FLinearColor::White;
return FPS_StrategicTeamStyle();
}

View File

@@ -1,9 +1,16 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// 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)
{

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_StrategicVisionTypes.h"

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#include "PS_VisionConeComponent.h"
@@ -6,27 +6,89 @@
#include "Engine/HitResult.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "ProceduralMeshComponent.h"
#include "PS_StrategicVisionSettings.h"
#include "PS_VisionSourceInterface.h"
namespace
{
// Materiaux moteur utilises en dernier recours si aucun materiau n'est configure.
// 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 GColorParamName(TEXT("Color"));
const FName GOpacityParamName(TEXT("Opacity"));
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;
// Apres le mouvement des acteurs, pour que le cone suive sans retard.
// After actor movement, so that the cone follows without lag.
PrimaryComponentTick.TickGroup = TG_PostUpdateWork;
bTickInEditor = true;
}
@@ -37,7 +99,7 @@ void UPS_VisionConeComponent::OnRegister()
EnsureConeMesh();
EnsureMaterial();
ApplyTeamColor();
ApplyTeamStyle();
}
void UPS_VisionConeComponent::OnComponentDestroyed(bool bDestroyingHierarchy)
@@ -76,18 +138,53 @@ void UPS_VisionConeComponent::OnDisplayStateChanged()
ConeMesh->SetVisibility(bShouldDisplay);
}
// Pas de traces ni de rebuild quand le cone est cache.
// No traces nor rebuilds while the cone is hidden.
SetComponentTickEnabled(bShouldDisplay);
SetComponentTickInterval(UpdateInterval);
if (bShouldDisplay)
{
EnsureMaterial();
ApplyTeamColor();
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())
@@ -97,8 +194,9 @@ void UPS_VisionConeComponent::EnsureConeMesh()
ConeMesh = NewObject<UProceduralMeshComponent>(this, NAME_None, RF_Transient);
ConeMesh->SetupAttachment(this);
// Rotation absolue : le fan est construit en espace monde, seul le point
// d'ancrage suit le personnage entre deux mises a jour.
// 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);
@@ -106,7 +204,7 @@ void UPS_VisionConeComponent::EnsureConeMesh()
ConeMesh->bReceivesDecals = false;
ConeMesh->SetTranslucentSortPriority(100);
ConeMesh->RegisterComponent();
ConeMesh->SetWorldRotation(FQuat::Identity);
ConeMesh->SetWorldLocationAndRotation(GetComponentLocation(), FQuat::Identity);
}
void UPS_VisionConeComponent::EnsureMaterial()
@@ -135,24 +233,41 @@ void UPS_VisionConeComponent::EnsureMaterial()
return;
}
if (!ConeMID || ConeMID->Parent != BaseMaterial)
if (!FillMID || FillMID->Parent != BaseMaterial)
{
ConeMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
FillMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
}
ConeMesh->SetMaterial(0, ConeMID);
if (!OutlineMID || OutlineMID->Parent != BaseMaterial)
{
OutlineMID = UMaterialInstanceDynamic::Create(BaseMaterial, this);
}
ConeMesh->SetMaterial(0, FillMID);
ConeMesh->SetMaterial(1, OutlineMID);
}
void UPS_VisionConeComponent::ApplyTeamColor()
void UPS_VisionConeComponent::ApplyTeamStyle()
{
if (!ConeMID)
const FPS_StrategicTeamStyle Style = GetDefault<UPS_StrategicVisionSettings>()->GetTeamStyle(GetTeam());
if (FillMID)
{
return;
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);
}
FLinearColor Color = GetTeamColor();
Color.A = Opacity;
ConeMID->SetVectorParameterValue(GColorParamName, Color);
ConeMID->SetScalarParameterValue(GOpacityParamName, Opacity);
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()
@@ -163,39 +278,38 @@ void UPS_VisionConeComponent::RebuildCone()
return;
}
const FVector CompLocation = GetComponentLocation();
const FRotator CompRotation = GetComponentRotation();
const FVector Forward = bFlattenOnGround
? FRotator(0.f, CompRotation.Yaw, 0.f).Vector()
: GetForwardVector();
const FVector Up = bFlattenOnGround ? FVector::UpVector : GetUpVector();
FCollisionQueryParams QueryParams(TEXT("PS_VisionCone"), bTraceComplex);
if (const AActor* Owner = GetOwner())
{
QueryParams.AddIgnoredActor(Owner);
}
// Hauteur de base du fan : projete au sol si demande, sinon a la hauteur du component.
float BaseZ = GroundOffset;
if (bFlattenOnGround)
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 = CompLocation + FVector(0.f, 0.f, TraceHeight);
const FVector GroundEnd = CompLocation - FVector(0.f, 0.f, 500.f);
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))
{
BaseZ = (GroundHit.ImpactPoint.Z - CompLocation.Z) + GroundOffset;
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 = CompLocation + FVector(0.f, 0.f, TraceHeight);
const FVector TraceOrigin = Origin + FVector(0.f, 0.f, TraceHeight);
FLinearColor VertexColor = GetTeamColor();
VertexColor.A = Opacity;
const FLinearColor TeamColor = GetTeamColor();
FLinearColor FillColor = TeamColor;
FillColor.A = Opacity;
TArray<FVector> Vertices;
TArray<FVector> Normals;
@@ -206,17 +320,17 @@ void UPS_VisionConeComponent::RebuildCone()
UVs.Reserve(RayCount + 1);
Colors.Reserve(RayCount + 1);
// Sommet central (origine du cone).
Vertices.Add(bFlattenOnGround ? FVector(0.f, 0.f, BaseZ) : Up * GroundOffset);
Normals.Add(Up);
// Center vertex (cone origin).
Vertices.Add(FVector::ZeroVector);
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(0.5f, 0.f));
Colors.Add(VertexColor);
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, Up);
const FVector Direction = Forward.RotateAngleAxis(AngleDeg, FVector::UpVector);
float HitDistance = ViewDistance;
FHitResult Hit;
@@ -225,46 +339,108 @@ void UPS_VisionConeComponent::RebuildCone()
HitDistance = Hit.Distance;
}
FVector LocalPoint = Direction * HitDistance;
if (bFlattenOnGround)
{
LocalPoint.Z = BaseZ;
}
else
{
LocalPoint += Up * GroundOffset;
}
Vertices.Add(LocalPoint);
Normals.Add(Up);
Vertices.Add(FVector(Direction.X * HitDistance, Direction.Y * HitDistance, 0.f));
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(Alpha, HitDistance / ViewDistance));
Colors.Add(VertexColor);
Colors.Add(FillColor);
}
const TArray<FProcMeshTangent> NoTangents;
if (Vertices.Num() == LastVertexCount)
if (Vertices.Num() == LastFillVertexCount)
{
// Meme topologie : mise a jour des sommets uniquement (moins couteux).
// Same topology: only update the vertices (cheaper).
ConeMesh->UpdateMeshSection_LinearColor(0, Vertices, Normals, UVs, Colors, NoTangents);
}
else
{
// Triangles dans les deux sens pour rester visible sous tous les angles de camera.
// 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.Add(0);
Triangles.Add(1 + SegmentIndex);
Triangles.Add(2 + SegmentIndex);
Triangles.Add(0);
Triangles.Add(2 + SegmentIndex);
Triangles.Add(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);
LastVertexCount = Vertices.Num();
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

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
@@ -8,11 +8,11 @@
#include "PS_StrategicDisplayComponent.generated.h"
/**
* Classe de base des affichages strategiques poses sur les personnages
* (cone de vision, et autres visualisations a venir).
* Base class of the strategic displays placed on characters
* (vision cone, and other visualizations to come).
*
* Gere le camp (couleur), l'activation locale du component et la
* synchronisation avec l'etat global du UPS_StrategicVisionSubsystem.
* 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
@@ -22,29 +22,29 @@ class PS_STRATEGICVISION_API UPS_StrategicDisplayComponent : public USceneCompon
public:
UPS_StrategicDisplayComponent();
/** Change le camp (met a jour la couleur de l'affichage). */
UFUNCTION(BlueprintCallable, Category = "Strategic Display")
/** Changes the team (updates the display color). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
void SetTeam(EPS_StrategicTeam NewTeam);
UFUNCTION(BlueprintPure, Category = "Strategic Display")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
EPS_StrategicTeam GetTeam() const { return Team; }
/** Active / desactive cet affichage precis (independamment de l'etat global). */
UFUNCTION(BlueprintCallable, Category = "Strategic Display")
/** Enables / disables this specific display (independently of the global state). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Display")
void SetDisplayEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "Strategic Display")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
bool IsDisplayEnabled() const { return bDisplayEnabled; }
/** Visibilite effective : activation locale ET etat global du subsystem. */
UFUNCTION(BlueprintPure, Category = "Strategic Display")
/** Effective visibility: local activation AND global subsystem state. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
bool IsDisplayVisible() const;
/** Couleur du camp courant, lue dans les settings du plugin. */
UFUNCTION(BlueprintPure, Category = "Strategic Display")
/** Color of the current team, read from the plugin settings. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Display")
FLinearColor GetTeamColor() const;
/** Categorie declaree par chaque type d'affichage (cf. FPS_StrategicDisplayCategories). */
/** Category declared by each display type (see FPS_StrategicDisplayCategories). */
virtual FName GetDisplayCategory() const { return NAME_None; }
protected:
@@ -55,17 +55,17 @@ protected:
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
/** Appele quand la visibilite effective ou le camp change. */
/** Called when the effective visibility or the team changes. */
virtual void OnDisplayStateChanged() {}
/** Reevalue la visibilite effective et previent la classe fille. */
/** Re-evaluates the effective visibility and notifies the child class. */
void RefreshDisplayState();
/** Camp du personnage porteur, determine la couleur de l'affichage. */
/** Team of the owning character, drives the display color. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
EPS_StrategicTeam Team = EPS_StrategicTeam::Enemy;
/** Etat d'activation local de cet affichage. */
/** Local activation state of this display. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strategic Display")
bool bDisplayEnabled = true;

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
@@ -9,9 +9,27 @@
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;
/**
* Reglages du plugin PS Strategic Vision.
* Accessibles dans Project Settings > Plugins > PS Strategic Vision.
* 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
@@ -23,20 +41,34 @@ public:
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
/** Couleur associee a chaque camp. */
/**
* 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, FLinearColor> TeamColors;
TMap<EPS_StrategicTeam, FPS_StrategicTeamStyle> TeamStyles;
/**
* Materiau par defaut des cones de vision. La couleur est appliquee via le
* parametre vecteur "Color" (et le parametre scalaire "Opacity" s'il existe),
* ainsi que via les vertex colors du mesh.
* Si vide ou introuvable, un materiau translucide du moteur est utilise.
* 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;
/** Couleur du camp demande (blanc si non configuree). */
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
/** 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

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
@@ -10,10 +10,10 @@
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPS_StrategicDisplayStateChanged);
/**
* Subsystem monde pilotant l'affichage strategique global.
* Permet d'activer / desactiver tous les affichages, une categorie (ex : cones
* de vision) ou un camp entier selon l'etat de jeu (vue spectateur, replay...).
* Tous les UPS_StrategicDisplayComponent s'y abonnent automatiquement.
* 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
@@ -21,52 +21,55 @@ class PS_STRATEGICVISION_API UPS_StrategicVisionSubsystem : public UWorldSubsyst
GENERATED_BODY()
public:
/** Raccourci d'acces au subsystem depuis n'importe quel contexte monde. */
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision", meta = (WorldContext = "WorldContextObject"))
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);
/** Interrupteur general de tous les affichages strategiques. */
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
/** Master switch of every strategic display. */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
void SetGlobalDisplayEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsGlobalDisplayEnabled() const { return bGlobalEnabled; }
/** Active / desactive une categorie d'affichage (ex : "VisionCone"). */
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
/** Enables / disables one display category (e.g. "VisionCone"). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|Strategic Vision")
void SetCategoryEnabled(FName Category, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "PS Strategic Vision")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsCategoryEnabled(FName Category) const { return !DisabledCategories.Contains(Category); }
/** Active / desactive les affichages d'un camp (ex : ne montrer que les ennemis). */
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision")
/** 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 = "PS Strategic Vision")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision")
bool IsTeamEnabled(EPS_StrategicTeam Team) const { return !DisabledTeams.Contains(Team); }
/** Raccourci : active / desactive tous les cones de vision. */
UFUNCTION(BlueprintCallable, Category = "PS Strategic Vision|Vision Cone")
/** 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 = "PS Strategic Vision|Vision Cone")
UFUNCTION(BlueprintPure, Category = "ASTERION|Strategic Vision|Vision Cone")
bool AreVisionConesEnabled() const { return IsCategoryEnabled(FPS_StrategicDisplayCategories::VisionCone); }
/** Etat combine utilise par les components pour savoir s'ils doivent s'afficher. */
/** 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);
}
/** Notifie a chaque changement d'etat global (les components s'y abonnent). */
UPROPERTY(BlueprintAssignable, Category = "PS Strategic Vision")
/** Fired on every global state change (components subscribe to it). */
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Strategic Vision")
FPS_StrategicDisplayStateChanged OnDisplayStateChanged;
private:
void NotifyStateChanged();
bool bGlobalEnabled = true;
// Initial state read from UPS_StrategicVisionSettings::bEnabledAtStartup (false by default).
bool bGlobalEnabled = false;
TSet<FName> DisabledCategories;
TSet<EPS_StrategicTeam> DisabledTeams;
};

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "PS_StrategicVisionTypes.generated.h"
/** Camp d'un personnage, utilise pour colorer / filtrer les affichages strategiques. */
/** Team of a character, used to color / filter the strategic displays. */
UENUM(BlueprintType)
enum class EPS_StrategicTeam : uint8
{
@@ -16,8 +16,8 @@ enum class EPS_StrategicTeam : uint8
};
/**
* Categories d'affichage strategique. Chaque type de component declare sa categorie,
* ce qui permet de l'activer / desactiver globalement via le subsystem.
* Strategic display categories. Each component type declares its category,
* which allows enabling / disabling it globally through the subsystem.
*/
struct PS_STRATEGICVISION_API FPS_StrategicDisplayCategories
{

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2026 j.foucher. All rights reserved.
// Copyright (c) 2026 ASTERION VR. All rights reserved.
#pragma once
@@ -12,13 +12,15 @@ class UMaterialInterface;
class UProceduralMeshComponent;
/**
* Affiche le cone de vision d'un personnage sous forme de mesh procedural.
* Le cone est decoupe par les obstacles (murs) via des line traces, colore
* selon le camp, et activable / desactivable localement ou globalement
* (vue spectateur, replay) via le UPS_StrategicVisionSubsystem.
* 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).
*
* A attacher sur le personnage : le cone suit la position du component et
* s'oriente selon son yaw (rotation horizontale).
* 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
@@ -30,57 +32,97 @@ public:
virtual FName GetDisplayCategory() const override { return FPS_StrategicDisplayCategories::VisionCone; }
/** Portee de vision, en cm. */
/** Vision range, in cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "10.0", Units = "cm"))
float ViewDistance = 1500.f;
/** Angle total du cone, en degres (ex : 90 = 45 de chaque cote). */
/** 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;
/** Opacite du cone (0 = invisible, 1 = opaque). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float Opacity = 0.35f;
/**
* Si vrai, le cone reste a plat au sol : seul le yaw du component est pris
* en compte et le mesh est projete a la hauteur du sol detecte.
* 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 bFlattenOnGround = true;
bool bUseOwnerVisionSource = true;
/** Decalage vertical du mesh au-dessus du sol, pour eviter le z-fighting. */
/** 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;
/** Nombre de segments de l'arc : plus eleve = plus precis mais plus de traces. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "4", ClampMax = "256"))
int32 ArcSegments = 48;
// --- Rendering ---
/** Intervalle de mise a jour en secondes (0 = chaque frame). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Quality", meta = (ClampMin = "0.0", Units = "s"))
float UpdateInterval = 0.f;
/** 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;
/** Canal de collision utilise pour bloquer le cone sur les obstacles. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
/** Draws a more opaque outline around the cone (readability when cones overlap). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering")
bool bShowOutline = true;
/** Trace contre la geometrie complexe (plus precis, plus couteux). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision")
bool bTraceComplex = false;
/** Outline width, in cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.5", Units = "cm", EditCondition = "bShowOutline"))
float OutlineWidth = 6.f;
/** Hauteur (relative au component) a laquelle les traces de vision sont lancees. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Collision", meta = (Units = "cm"))
float TraceHeight = 50.f;
/** Outline opacity. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision Cone|Rendering", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bShowOutline"))
float OutlineOpacity = 0.9f;
/**
* Materiau du cone pour ce component (prioritaire sur celui des settings).
* La couleur du camp est envoyee dans le parametre vecteur "Color", l'opacite
* dans le parametre scalaire "Opacity", et dans les vertex colors du mesh.
* 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:
@@ -90,17 +132,25 @@ protected:
virtual void OnDisplayStateChanged() override;
private:
/** Mesh procedural interne portant le fan du cone. */
/** Internal procedural mesh: section 0 = fill, section 1 = outline. */
UPROPERTY(Transient)
TObjectPtr<UProceduralMeshComponent> ConeMesh;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> ConeMID;
TObjectPtr<UMaterialInstanceDynamic> FillMID;
int32 LastVertexCount = 0;
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 ApplyTeamColor();
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")