From 0327abb1a01e0c5e31224c38c57e5548864977a6 Mon Sep 17 00:00:00 2001 From: John Date: Wed, 11 Mar 2026 11:25:15 +0530 Subject: [PATCH] init --- .DS_Store | Bin 0 -> 8196 bytes .gitignore | 15 + .vsconfig | 17 + AudioVideoRecord.uproject | 39 ++ Config/DefaultEditor.ini | 14 + .../DefaultEditorPerProjectUserSettings.ini | 2 + Config/DefaultEngine.ini | 271 +++++++++ Config/DefaultGame.ini | 6 + Config/DefaultInput.ini | 109 ++++ .../GameplayRecorder/GameplayRecorder.uplugin | 19 + .../GameplayRecorder/AudioMixerRecorder.cpp | 255 +++++++++ .../GameplayRecorder/AudioMixerRecorder.h | 98 ++++ .../Source/GameplayRecorder/AudioRecorder.cpp | 265 +++++++++ .../Source/GameplayRecorder/AudioRecorder.h | 70 +++ .../Source/GameplayRecorder/FFmpegPipe.cpp | 266 +++++++++ .../Source/GameplayRecorder/FFmpegPipe.h | 85 +++ .../GameplayRecorder.Build.cs | 29 + .../GameplayRecorder/ParticipantManager.cpp | 113 ++++ .../GameplayRecorder/ParticipantManager.h | 77 +++ .../GameplayRecorder/RecorderManager.cpp | 377 +++++++++++++ .../Source/GameplayRecorder/RecorderManager.h | 161 ++++++ .../GameplayRecorder/RecorderModule.cpp | 20 + .../Source/GameplayRecorder/RecorderModule.h | 22 + .../VoicePermissionSystem.cpp | 106 ++++ .../GameplayRecorder/VoicePermissionSystem.h | 60 ++ .../GameplayRecorder/VoiceSessionManager.cpp | 355 ++++++++++++ .../GameplayRecorder/VoiceSessionManager.h | 151 +++++ .../Source/GameplayRecorder/VoiceTypes.h | 73 +++ Source/AudioVideoRecord.Target.cs | 15 + .../AudioVideoRecord.Build.cs | 48 ++ Source/AudioVideoRecord/AudioVideoRecord.cpp | 8 + Source/AudioVideoRecord/AudioVideoRecord.h | 8 + .../AudioVideoRecordCameraManager.cpp | 11 + .../AudioVideoRecordCameraManager.h | 22 + .../AudioVideoRecordCharacter.cpp | 120 ++++ .../AudioVideoRecordCharacter.h | 94 ++++ .../AudioVideoRecordGameMode.cpp | 8 + .../AudioVideoRecordGameMode.h | 22 + .../AudioVideoRecordPlayerController.cpp | 70 +++ .../AudioVideoRecordPlayerController.h | 50 ++ Source/AudioVideoRecord/SimpleRecorder.cpp | 518 ++++++++++++++++++ Source/AudioVideoRecord/SimpleRecorder.h | 125 +++++ .../Variant_Horror/HorrorCharacter.cpp | 143 +++++ .../Variant_Horror/HorrorCharacter.h | 104 ++++ .../Variant_Horror/HorrorGameMode.cpp | 9 + .../Variant_Horror/HorrorGameMode.h | 21 + .../Variant_Horror/HorrorPlayerController.cpp | 92 ++++ .../Variant_Horror/HorrorPlayerController.h | 62 +++ .../Variant_Horror/UI/HorrorUI.cpp | 23 + .../Variant_Horror/UI/HorrorUI.h | 42 ++ .../AI/EnvQueryContext_Target.cpp | 27 + .../AI/EnvQueryContext_Target.h | 22 + .../AI/ShooterAIController.cpp | 74 +++ .../Variant_Shooter/AI/ShooterAIController.h | 85 +++ .../Variant_Shooter/AI/ShooterNPC.cpp | 208 +++++++ .../Variant_Shooter/AI/ShooterNPC.h | 153 ++++++ .../AI/ShooterStateTreeUtility.cpp | 366 +++++++++++++ .../AI/ShooterStateTreeUtility.h | 309 +++++++++++ .../Variant_Shooter/ShooterCharacter.cpp | 283 ++++++++++ .../Variant_Shooter/ShooterCharacter.h | 166 ++++++ .../Variant_Shooter/ShooterGameMode.cpp | 33 ++ .../Variant_Shooter/ShooterGameMode.h | 42 ++ .../ShooterPlayerController.cpp | 142 +++++ .../Variant_Shooter/ShooterPlayerController.h | 77 +++ .../UI/ShooterBulletCounterUI.cpp | 5 + .../UI/ShooterBulletCounterUI.h | 26 + .../Variant_Shooter/UI/ShooterUI.cpp | 5 + .../Variant_Shooter/UI/ShooterUI.h | 22 + .../Variant_Shooter/Weapons/ShooterPickup.cpp | 108 ++++ .../Variant_Shooter/Weapons/ShooterPickup.h | 96 ++++ .../Weapons/ShooterProjectile.cpp | 167 ++++++ .../Weapons/ShooterProjectile.h | 109 ++++ .../Variant_Shooter/Weapons/ShooterWeapon.cpp | 218 ++++++++ .../Variant_Shooter/Weapons/ShooterWeapon.h | 180 ++++++ .../Weapons/ShooterWeaponHolder.cpp | 6 + .../Weapons/ShooterWeaponHolder.h | 55 ++ Source/AudioVideoRecordEditor.Target.cs | 15 + 77 files changed, 7689 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 .vsconfig create mode 100644 AudioVideoRecord.uproject create mode 100644 Config/DefaultEditor.ini create mode 100644 Config/DefaultEditorPerProjectUserSettings.ini create mode 100644 Config/DefaultEngine.ini create mode 100644 Config/DefaultGame.ini create mode 100644 Config/DefaultInput.ini create mode 100644 Plugins/GameplayRecorder/GameplayRecorder.uplugin create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/GameplayRecorder.Build.cs create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.cpp create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.h create mode 100644 Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceTypes.h create mode 100644 Source/AudioVideoRecord.Target.cs create mode 100644 Source/AudioVideoRecord/AudioVideoRecord.Build.cs create mode 100644 Source/AudioVideoRecord/AudioVideoRecord.cpp create mode 100644 Source/AudioVideoRecord/AudioVideoRecord.h create mode 100644 Source/AudioVideoRecord/AudioVideoRecordCameraManager.cpp create mode 100644 Source/AudioVideoRecord/AudioVideoRecordCameraManager.h create mode 100644 Source/AudioVideoRecord/AudioVideoRecordCharacter.cpp create mode 100644 Source/AudioVideoRecord/AudioVideoRecordCharacter.h create mode 100644 Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp create mode 100644 Source/AudioVideoRecord/AudioVideoRecordGameMode.h create mode 100644 Source/AudioVideoRecord/AudioVideoRecordPlayerController.cpp create mode 100644 Source/AudioVideoRecord/AudioVideoRecordPlayerController.h create mode 100644 Source/AudioVideoRecord/SimpleRecorder.cpp create mode 100644 Source/AudioVideoRecord/SimpleRecorder.h create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.cpp create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.h create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.cpp create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.h create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.cpp create mode 100644 Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.h create mode 100644 Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.cpp create mode 100644 Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.h create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.cpp create mode 100644 Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.h create mode 100644 Source/AudioVideoRecordEditor.Target.cs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d02cf09fee1efe096ddf6ce573ae70d5d0b97380 GIT binary patch literal 8196 zcmeHMU2GIp6u#fIg&8~0DHJFpunQ{?VS%MoLP2zC3knp9Y)eavEW0}+OqkA;o!Ko~ z(W*~IiGR@;qsBKis4t2!{%ay3{yb=q1T>oXpggESAN0is&z(C%oC<0Lg#z%nH?lmmKa;(hEQT6+q`9a(A{8Z{Jq^jzgDO2UCa;<0F!VQE+0h>0) zt>dG8GQ5JFr#*7@%Urd+a+~;6QMJC}>iLSR%2l=MG`)LIPp{gS)n=5*r_Mp&avg7v zS@eizwK|V{-&$~;T}6}6_Y_>aG~jqyt-g@63ne?}o11JqW8Gnrtj;SGUEAK}dKO=1 zZ?A72_4(Ymsop-%E&6T2Vp&ZqdFF1D*0RcGJFV#TT69Zp*f7RxD6dSPGjHMIWviN7 zZfLt{d*_)tt$v2upw8|oTb`A-&F&$~@DJ_Hc?HukEazY!1>f7c}mw|LV_?|t*((ZL+Q42dD9l6wdy)q+phGKMXZHzgSt_! zM^fo6xB~;UZ%>HRdaJCRmiO~}cjP?3bI^1uyGrX@WVz2flq;H@Lj=W&#Wq2<&WbF&G(JZHI5%}o|iX4y=m8XzMFB8MW`v;#3 zMnO20L^>6!$ZS4qV(qMh9biQ^%#O1s*$MU<`-q)lU$Sr5_v}aZ8vqG4n1Wg~U_N4q zVNQ zp?neS@?jid zQ!qcrzGD~IMfL~5d^%>L5lfN6T3k<{-ickvpbOmu>;uT75P;hyXcyt(PTWQCzJ~z* z0FL7!Jd8*16rRR20mxqrK>jjb#~XMHZ{r=D!fAYrPw*)|!&!Wb3-}2?j{)<}3XBEC z@1wz7&J-Qnbq)80x76CHEB z){fJ4fG*y+-iV-q33Wj^PBbXTiC+4{kj8ORV%jtEK;YXAC&fWP;fI(q*{@BfJt KBS7%v?EVDL7HD?> literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0b4c62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +Binaries +Content +DerivedDataCache +Intermediate +Saved +.vscode +.vs +*.VC.db +*.opensdf +*.opendb +*.sdf +*.sln +*.suo +*.xcodeproj +*.xcworkspace \ No newline at end of file diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..3b919ea --- /dev/null +++ b/.vsconfig @@ -0,0 +1,17 @@ +{ + "version": "1.0", + "components": [ + "Component.Unreal.Debugger", + "Component.Unreal.Ide", + "Microsoft.Net.Component.4.6.2.TargetingPack", + "Microsoft.VisualStudio.Component.VC.14.38.17.8.ATL", + "Microsoft.VisualStudio.Component.VC.14.38.17.8.x86.x64", + "Microsoft.VisualStudio.Component.VC.Llvm.Clang", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows11SDK.22621", + "Microsoft.VisualStudio.Workload.CoreEditor", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Workload.NativeGame" + ] +} diff --git a/AudioVideoRecord.uproject b/AudioVideoRecord.uproject new file mode 100644 index 0000000..43aa4a4 --- /dev/null +++ b/AudioVideoRecord.uproject @@ -0,0 +1,39 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.6", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "AudioVideoRecord", + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine", + "AIModule", + "UMG" + ] + } + ], + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "StateTree", + "Enabled": true + }, + { + "Name": "GameplayStateTree", + "Enabled": true + }, + { + "Name": "GameplayRecorder", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Config/DefaultEditor.ini b/Config/DefaultEditor.ini new file mode 100644 index 0000000..497e554 --- /dev/null +++ b/Config/DefaultEditor.ini @@ -0,0 +1,14 @@ +[UnrealEd.SimpleMap] +SimpleMapName=/Game/FirstPerson/Maps/FirstPersonExampleMap + +[EditoronlyBP] +bAllowClassAndBlueprintPinMatching=true +bReplaceBlueprintWithClass= true +bDontLoadBlueprintOutsideEditor= true +bBlueprintIsNotBlueprintType= true + +[/Script/AdvancedPreviewScene.SharedProfiles] ++Profiles=(ProfileName="Epic Headquarters",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=1.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=1.000000,bRotateLightingRig=False,bShowEnvironment=True,bShowFloor=True,bShowGrid=False,EnvironmentColor=(R=0.200000,G=0.200000,B=0.200000,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/EpicQuadPanorama_CC+EV1.EpicQuadPanorama_CC+EV1",bPostProcessingEnabled=True,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=False,bOverride_AutoExposureMaxBrightness=False,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOverride_FilmGrainIntensityMidtones=False,bOverride_FilmGrainIntensityHighlights=False,bOverride_FilmGrainShadowsMax=False,bOverride_FilmGrainHighlightsMin=False,bOverride_FilmGrainHighlightsMax=False,bOverride_FilmGrainTexelSize=False,bOverride_FilmGrainTexture=False,bOverride_AmbientOcclusionIntensity=False,bOverride_AmbientOcclusionStaticFraction=False,bOverride_AmbientOcclusionRadius=False,bOverride_AmbientOcclusionFadeDistance=False,bOverride_AmbientOcclusionFadeRadius=False,bOverride_AmbientOcclusionRadiusInWS=False,bOverride_AmbientOcclusionPower=False,bOverride_AmbientOcclusionBias=False,bOverride_AmbientOcclusionQuality=False,bOverride_AmbientOcclusionMipBlend=False,bOverride_AmbientOcclusionMipScale=False,bOverride_AmbientOcclusionMipThreshold=False,bOverride_AmbientOcclusionTemporalBlendWeight=False,bOverride_RayTracingAO=False,bOverride_RayTracingAOSamplesPerPixel=False,bOverride_RayTracingAOIntensity=False,bOverride_RayTracingAORadius=False,bOverride_IndirectLightingColor=False,bOverride_IndirectLightingIntensity=False,bOverride_ColorGradingIntensity=False,bOverride_ColorGradingLUT=False,bOverride_DepthOfFieldFocalDistance=False,bOverride_DepthOfFieldFstop=False,bOverride_DepthOfFieldMinFstop=False,bOverride_DepthOfFieldBladeCount=False,bOverride_DepthOfFieldSensorWidth=False,bOverride_DepthOfFieldSqueezeFactor=False,bOverride_DepthOfFieldDepthBlurRadius=False,bOverride_DepthOfFieldUseHairDepth=False,bOverride_DepthOfFieldPetzvalBokeh=False,bOverride_DepthOfFieldPetzvalBokehFalloff=False,bOverride_DepthOfFieldPetzvalExclusionBoxExtents=False,bOverride_DepthOfFieldPetzvalExclusionBoxRadius=False,bOverride_DepthOfFieldAspectRatioScalar=False,bOverride_DepthOfFieldMatteBoxFlags=False,bOverride_DepthOfFieldBarrelRadius=False,bOverride_DepthOfFieldBarrelLength=False,bOverride_DepthOfFieldDepthBlurAmount=False,bOverride_DepthOfFieldFocalRegion=False,bOverride_DepthOfFieldNearTransitionRegion=False,bOverride_DepthOfFieldFarTransitionRegion=False,bOverride_DepthOfFieldScale=False,bOverride_DepthOfFieldNearBlurSize=False,bOverride_DepthOfFieldFarBlurSize=False,bOverride_MobileHQGaussian=False,bOverride_DepthOfFieldOcclusion=False,bOverride_DepthOfFieldSkyFocusDistance=False,bOverride_DepthOfFieldVignetteSize=False,bOverride_MotionBlurAmount=False,bOverride_MotionBlurMax=False,bOverride_MotionBlurTargetFPS=False,bOverride_MotionBlurPerObjectSize=False,bOverride_ReflectionMethod=False,bOverride_LumenReflectionQuality=False,bOverride_ScreenSpaceReflectionIntensity=False,bOverride_ScreenSpaceReflectionQuality=False,bOverride_ScreenSpaceReflectionMaxRoughness=False,bOverride_ScreenSpaceReflectionRoughnessScale=False,bOverride_UserFlags=False,bOverride_RayTracingReflectionsMaxRoughness=False,bOverride_RayTracingReflectionsMaxBounces=False,bOverride_RayTracingReflectionsSamplesPerPixel=False,bOverride_RayTracingReflectionsShadows=False,bOverride_RayTracingReflectionsTranslucency=False,bOverride_TranslucencyType=False,bOverride_RayTracingTranslucencyMaxRoughness=False,bOverride_RayTracingTranslucencyRefractionRays=False,bOverride_RayTracingTranslucencySamplesPerPixel=False,bOverride_RayTracingTranslucencyShadows=False,bOverride_RayTracingTranslucencyRefraction=False,bOverride_RayTracingTranslucencyMaxPrimaryHitEvents=False,bOverride_RayTracingTranslucencyMaxSecondaryHitEvents=False,bOverride_RayTracingTranslucencyUseRayTracedRefraction=False,bOverride_DynamicGlobalIlluminationMethod=False,bOverride_LumenSceneLightingQuality=False,bOverride_LumenSceneDetail=False,bOverride_LumenSceneViewDistance=False,bOverride_LumenSceneLightingUpdateSpeed=False,bOverride_LumenFinalGatherQuality=False,bOverride_LumenFinalGatherLightingUpdateSpeed=False,bOverride_LumenFinalGatherScreenTraces=False,bOverride_LumenMaxTraceDistance=False,bOverride_LumenDiffuseColorBoost=False,bOverride_LumenSkylightLeaking=False,bOverride_LumenSkylightLeakingTint=False,bOverride_LumenFullSkylightLeakingDistance=False,bOverride_LumenRayLightingMode=False,bOverride_LumenReflectionsScreenTraces=False,bOverride_LumenFrontLayerTranslucencyReflections=False,bOverride_LumenMaxRoughnessToTraceReflections=False,bOverride_LumenMaxReflectionBounces=False,bOverride_LumenMaxRefractionBounces=False,bOverride_LumenSurfaceCacheResolution=False,bOverride_RayTracingGI=False,bOverride_RayTracingGIMaxBounces=False,bOverride_RayTracingGISamplesPerPixel=False,bOverride_PathTracingMaxBounces=False,bOverride_PathTracingSamplesPerPixel=False,bOverride_PathTracingMaxPathIntensity=False,bOverride_PathTracingEnableEmissiveMaterials=False,bOverride_PathTracingEnableReferenceDOF=False,bOverride_PathTracingEnableReferenceAtmosphere=False,bOverride_PathTracingEnableDenoiser=False,bOverride_PathTracingIncludeEmissive=False,bOverride_PathTracingIncludeDiffuse=False,bOverride_PathTracingIncludeIndirectDiffuse=False,bOverride_PathTracingIncludeSpecular=False,bOverride_PathTracingIncludeIndirectSpecular=False,bOverride_PathTracingIncludeVolume=False,bOverride_PathTracingIncludeIndirectVolume=False,bMobileHQGaussian=False,BloomMethod=BM_SOG,AutoExposureMethod=AEM_Histogram,TemperatureType=TEMP_WhiteBalance,WhiteTemp=6500.000000,WhiteTint=0.000000,ColorSaturation=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrast=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGamma=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGain=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffset=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetShadows=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetMidtones=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetHighlights=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorCorrectionHighlightsMin=0.500000,ColorCorrectionHighlightsMax=1.000000,ColorCorrectionShadowsMax=0.090000,BlueCorrection=0.600000,ExpandGamut=1.000000,ToneCurveAmount=1.000000,FilmSlope=0.880000,FilmToe=0.550000,FilmShoulder=0.260000,FilmBlackClip=0.000000,FilmWhiteClip=0.040000,SceneColorTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SceneFringeIntensity=0.000000,ChromaticAberrationStartOffset=0.000000,BloomIntensity=0.675000,BloomThreshold=-1.000000,BloomSizeScale=4.000000,Bloom1Size=0.300000,Bloom2Size=1.000000,Bloom3Size=2.000000,Bloom4Size=10.000000,Bloom5Size=30.000000,Bloom6Size=64.000000,Bloom1Tint=(R=0.346500,G=0.346500,B=0.346500,A=1.000000),Bloom2Tint=(R=0.138000,G=0.138000,B=0.138000,A=1.000000),Bloom3Tint=(R=0.117600,G=0.117600,B=0.117600,A=1.000000),Bloom4Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom5Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom6Tint=(R=0.061000,G=0.061000,B=0.061000,A=1.000000),BloomConvolutionScatterDispersion=1.000000,BloomConvolutionSize=1.000000,BloomConvolutionTexture=None,BloomConvolutionCenterUV=(X=0.500000,Y=0.500000),BloomConvolutionPreFilterMin=7.000000,BloomConvolutionPreFilterMax=15000.000000,BloomConvolutionPreFilterMult=15.000000,BloomConvolutionBufferScale=0.133000,BloomDirtMask=None,BloomDirtMaskIntensity=0.000000,BloomDirtMaskTint=(R=0.500000,G=0.500000,B=0.500000,A=1.000000),DynamicGlobalIlluminationMethod=Lumen,IndirectLightingColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),IndirectLightingIntensity=1.000000,LumenRayLightingMode=Default,LumenSceneLightingQuality=1.000000,LumenSceneDetail=1.000000,LumenSceneViewDistance=20000.000000,LumenSceneLightingUpdateSpeed=1.000000,LumenFinalGatherQuality=1.000000,LumenFinalGatherLightingUpdateSpeed=1.000000,LumenFinalGatherScreenTraces=True,LumenMaxTraceDistance=20000.000000,LumenDiffuseColorBoost=1.000000,LumenSkylightLeaking=0.000000,LumenSkylightLeakingTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LumenFullSkylightLeakingDistance=1000.000000,LumenSurfaceCacheResolution=1.000000,ReflectionMethod=Lumen,LumenReflectionQuality=1.000000,LumenReflectionsScreenTraces=True,LumenFrontLayerTranslucencyReflections=False,LumenMaxRoughnessToTraceReflections=0.400000,LumenMaxReflectionBounces=1,LumenMaxRefractionBounces=0,ScreenSpaceReflectionIntensity=100.000000,ScreenSpaceReflectionQuality=50.000000,ScreenSpaceReflectionMaxRoughness=0.600000,bMegaLights=True,AmbientCubemapTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),AmbientCubemapIntensity=1.000000,AmbientCubemap=None,CameraShutterSpeed=60.000000,CameraISO=100.000000,DepthOfFieldFstop=4.000000,DepthOfFieldMinFstop=1.200000,DepthOfFieldBladeCount=5,AutoExposureBias=1.000000,AutoExposureBiasBackup=0.000000,bOverride_AutoExposureBiasBackup=False,AutoExposureApplyPhysicalCameraExposure=True,AutoExposureBiasCurve=None,AutoExposureMeterMask=None,AutoExposureLowPercent=10.000000,AutoExposureHighPercent=90.000000,AutoExposureMinBrightness=-10.000000,AutoExposureMaxBrightness=20.000000,AutoExposureSpeedUp=3.000000,AutoExposureSpeedDown=1.000000,HistogramLogMin=-10.000000,HistogramLogMax=20.000000,LocalExposureMethod=Bilateral,LocalExposureHighlightContrastScale=1.000000,LocalExposureShadowContrastScale=1.000000,LocalExposureHighlightContrastCurve=None,LocalExposureShadowContrastCurve=None,LocalExposureHighlightThreshold=0.000000,LocalExposureShadowThreshold=0.000000,LocalExposureDetailStrength=1.000000,LocalExposureBlurredLuminanceBlend=0.600000,LocalExposureBlurredLuminanceKernelSizePercent=50.000000,LocalExposureHighlightThresholdStrength=1.000000,LocalExposureShadowThresholdStrength=1.000000,LocalExposureMiddleGreyBias=0.000000,LensFlareIntensity=1.000000,LensFlareTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LensFlareBokehSize=3.000000,LensFlareThreshold=8.000000,LensFlareBokehShape=None,LensFlareTints[0]=(R=1.000000,G=0.800000,B=0.400000,A=0.600000),LensFlareTints[1]=(R=1.000000,G=1.000000,B=0.600000,A=0.530000),LensFlareTints[2]=(R=0.800000,G=0.800000,B=1.000000,A=0.460000),LensFlareTints[3]=(R=0.500000,G=1.000000,B=0.400000,A=0.390000),LensFlareTints[4]=(R=0.500000,G=0.800000,B=1.000000,A=0.310000),LensFlareTints[5]=(R=0.900000,G=1.000000,B=0.800000,A=0.270000),LensFlareTints[6]=(R=1.000000,G=0.800000,B=0.400000,A=0.220000),LensFlareTints[7]=(R=0.900000,G=0.700000,B=0.700000,A=0.150000),VignetteIntensity=0.400000,Sharpen=0.000000,FilmGrainIntensity=0.000000,FilmGrainIntensityShadows=1.000000,FilmGrainIntensityMidtones=1.000000,FilmGrainIntensityHighlights=1.000000,FilmGrainShadowsMax=0.090000,FilmGrainHighlightsMin=0.500000,FilmGrainHighlightsMax=1.000000,FilmGrainTexelSize=1.000000,FilmGrainTexture=None,AmbientOcclusionIntensity=0.500000,AmbientOcclusionStaticFraction=1.000000,AmbientOcclusionRadius=200.000000,AmbientOcclusionRadiusInWS=False,AmbientOcclusionFadeDistance=8000.000000,AmbientOcclusionFadeRadius=5000.000000,AmbientOcclusionPower=2.000000,AmbientOcclusionBias=3.000000,AmbientOcclusionQuality=50.000000,AmbientOcclusionMipBlend=0.600000,AmbientOcclusionMipScale=1.700000,AmbientOcclusionMipThreshold=0.010000,AmbientOcclusionTemporalBlendWeight=0.100000,RayTracingAO=False,RayTracingAOSamplesPerPixel=1,RayTracingAOIntensity=1.000000,RayTracingAORadius=200.000000,ColorGradingIntensity=1.000000,ColorGradingLUT=None,DepthOfFieldSensorWidth=24.576000,DepthOfFieldSqueezeFactor=1.000000,DepthOfFieldFocalDistance=0.000000,DepthOfFieldDepthBlurAmount=1.000000,DepthOfFieldDepthBlurRadius=0.000000,DepthOfFieldUseHairDepth=False,DepthOfFieldPetzvalBokeh=0.000000,DepthOfFieldPetzvalBokehFalloff=1.000000,DepthOfFieldPetzvalExclusionBoxExtents=(X=0.000000,Y=0.000000),DepthOfFieldPetzvalExclusionBoxRadius=0.000000,DepthOfFieldAspectRatioScalar=1.000000,DepthOfFieldBarrelRadius=5.000000,DepthOfFieldBarrelLength=0.000000,DepthOfFieldMatteBoxFlags[0]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[1]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[2]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldFocalRegion=0.000000,DepthOfFieldNearTransitionRegion=300.000000,DepthOfFieldFarTransitionRegion=500.000000,DepthOfFieldScale=0.000000,DepthOfFieldNearBlurSize=15.000000,DepthOfFieldFarBlurSize=15.000000,DepthOfFieldOcclusion=0.400000,DepthOfFieldSkyFocusDistance=0.000000,DepthOfFieldVignetteSize=200.000000,MotionBlurAmount=0.500000,MotionBlurMax=5.000000,MotionBlurTargetFPS=30,MotionBlurPerObjectSize=0.000000,TranslucencyType=Raster,RayTracingTranslucencyMaxRoughness=0.600000,RayTracingTranslucencyRefractionRays=3,RayTracingTranslucencySamplesPerPixel=1,RayTracingTranslucencyMaxPrimaryHitEvents=4,RayTracingTranslucencyMaxSecondaryHitEvents=2,RayTracingTranslucencyShadows=Hard_shadows,RayTracingTranslucencyRefraction=True,RayTracingTranslucencyUseRayTracedRefraction=False,PathTracingMaxBounces=32,PathTracingSamplesPerPixel=2048,PathTracingMaxPathIntensity=24.000000,PathTracingEnableEmissiveMaterials=True,PathTracingEnableReferenceDOF=False,PathTracingEnableReferenceAtmosphere=False,PathTracingEnableDenoiser=True,PathTracingIncludeEmissive=True,PathTracingIncludeDiffuse=True,PathTracingIncludeIndirectDiffuse=True,PathTracingIncludeSpecular=True,PathTracingIncludeIndirectSpecular=True,PathTracingIncludeVolume=True,PathTracingIncludeIndirectVolume=True,UserFlags=0,WeightedBlendables=(Array=)),LightingRigRotation=0.000000,RotationSpeed=2.000000,DirectionalLightRotation=(Pitch=-40.000000,Yaw=-67.500000,Roll=0.000000),bEnableToneMapping=True,bShowMeshEdges=False) ++Profiles=(ProfileName="Grey Wireframe",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=1.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=1.000000,bRotateLightingRig=False,bShowEnvironment=False,bShowFloor=False,bShowGrid=True,EnvironmentColor=(R=0.039216,G=0.039216,B=0.039216,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/EpicQuadPanorama_CC+EV1.EpicQuadPanorama_CC+EV1",bPostProcessingEnabled=False,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=False,bOverride_AutoExposureMaxBrightness=False,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOverride_FilmGrainIntensityMidtones=False,bOverride_FilmGrainIntensityHighlights=False,bOverride_FilmGrainShadowsMax=False,bOverride_FilmGrainHighlightsMin=False,bOverride_FilmGrainHighlightsMax=False,bOverride_FilmGrainTexelSize=False,bOverride_FilmGrainTexture=False,bOverride_AmbientOcclusionIntensity=False,bOverride_AmbientOcclusionStaticFraction=False,bOverride_AmbientOcclusionRadius=False,bOverride_AmbientOcclusionFadeDistance=False,bOverride_AmbientOcclusionFadeRadius=False,bOverride_AmbientOcclusionRadiusInWS=False,bOverride_AmbientOcclusionPower=False,bOverride_AmbientOcclusionBias=False,bOverride_AmbientOcclusionQuality=False,bOverride_AmbientOcclusionMipBlend=False,bOverride_AmbientOcclusionMipScale=False,bOverride_AmbientOcclusionMipThreshold=False,bOverride_AmbientOcclusionTemporalBlendWeight=False,bOverride_RayTracingAO=False,bOverride_RayTracingAOSamplesPerPixel=False,bOverride_RayTracingAOIntensity=False,bOverride_RayTracingAORadius=False,bOverride_IndirectLightingColor=False,bOverride_IndirectLightingIntensity=False,bOverride_ColorGradingIntensity=False,bOverride_ColorGradingLUT=False,bOverride_DepthOfFieldFocalDistance=False,bOverride_DepthOfFieldFstop=False,bOverride_DepthOfFieldMinFstop=False,bOverride_DepthOfFieldBladeCount=False,bOverride_DepthOfFieldSensorWidth=False,bOverride_DepthOfFieldSqueezeFactor=False,bOverride_DepthOfFieldDepthBlurRadius=False,bOverride_DepthOfFieldUseHairDepth=False,bOverride_DepthOfFieldPetzvalBokeh=False,bOverride_DepthOfFieldPetzvalBokehFalloff=False,bOverride_DepthOfFieldPetzvalExclusionBoxExtents=False,bOverride_DepthOfFieldPetzvalExclusionBoxRadius=False,bOverride_DepthOfFieldAspectRatioScalar=False,bOverride_DepthOfFieldMatteBoxFlags=False,bOverride_DepthOfFieldBarrelRadius=False,bOverride_DepthOfFieldBarrelLength=False,bOverride_DepthOfFieldDepthBlurAmount=False,bOverride_DepthOfFieldFocalRegion=False,bOverride_DepthOfFieldNearTransitionRegion=False,bOverride_DepthOfFieldFarTransitionRegion=False,bOverride_DepthOfFieldScale=False,bOverride_DepthOfFieldNearBlurSize=False,bOverride_DepthOfFieldFarBlurSize=False,bOverride_MobileHQGaussian=False,bOverride_DepthOfFieldOcclusion=False,bOverride_DepthOfFieldSkyFocusDistance=False,bOverride_DepthOfFieldVignetteSize=False,bOverride_MotionBlurAmount=False,bOverride_MotionBlurMax=False,bOverride_MotionBlurTargetFPS=False,bOverride_MotionBlurPerObjectSize=False,bOverride_ReflectionMethod=False,bOverride_LumenReflectionQuality=False,bOverride_ScreenSpaceReflectionIntensity=False,bOverride_ScreenSpaceReflectionQuality=False,bOverride_ScreenSpaceReflectionMaxRoughness=False,bOverride_ScreenSpaceReflectionRoughnessScale=False,bOverride_UserFlags=False,bOverride_RayTracingReflectionsMaxRoughness=False,bOverride_RayTracingReflectionsMaxBounces=False,bOverride_RayTracingReflectionsSamplesPerPixel=False,bOverride_RayTracingReflectionsShadows=False,bOverride_RayTracingReflectionsTranslucency=False,bOverride_TranslucencyType=False,bOverride_RayTracingTranslucencyMaxRoughness=False,bOverride_RayTracingTranslucencyRefractionRays=False,bOverride_RayTracingTranslucencySamplesPerPixel=False,bOverride_RayTracingTranslucencyShadows=False,bOverride_RayTracingTranslucencyRefraction=False,bOverride_RayTracingTranslucencyMaxPrimaryHitEvents=False,bOverride_RayTracingTranslucencyMaxSecondaryHitEvents=False,bOverride_RayTracingTranslucencyUseRayTracedRefraction=False,bOverride_DynamicGlobalIlluminationMethod=False,bOverride_LumenSceneLightingQuality=False,bOverride_LumenSceneDetail=False,bOverride_LumenSceneViewDistance=False,bOverride_LumenSceneLightingUpdateSpeed=False,bOverride_LumenFinalGatherQuality=False,bOverride_LumenFinalGatherLightingUpdateSpeed=False,bOverride_LumenFinalGatherScreenTraces=False,bOverride_LumenMaxTraceDistance=False,bOverride_LumenDiffuseColorBoost=False,bOverride_LumenSkylightLeaking=False,bOverride_LumenSkylightLeakingTint=False,bOverride_LumenFullSkylightLeakingDistance=False,bOverride_LumenRayLightingMode=False,bOverride_LumenReflectionsScreenTraces=False,bOverride_LumenFrontLayerTranslucencyReflections=False,bOverride_LumenMaxRoughnessToTraceReflections=False,bOverride_LumenMaxReflectionBounces=False,bOverride_LumenMaxRefractionBounces=False,bOverride_LumenSurfaceCacheResolution=False,bOverride_RayTracingGI=False,bOverride_RayTracingGIMaxBounces=False,bOverride_RayTracingGISamplesPerPixel=False,bOverride_PathTracingMaxBounces=False,bOverride_PathTracingSamplesPerPixel=False,bOverride_PathTracingMaxPathIntensity=False,bOverride_PathTracingEnableEmissiveMaterials=False,bOverride_PathTracingEnableReferenceDOF=False,bOverride_PathTracingEnableReferenceAtmosphere=False,bOverride_PathTracingEnableDenoiser=False,bOverride_PathTracingIncludeEmissive=False,bOverride_PathTracingIncludeDiffuse=False,bOverride_PathTracingIncludeIndirectDiffuse=False,bOverride_PathTracingIncludeSpecular=False,bOverride_PathTracingIncludeIndirectSpecular=False,bOverride_PathTracingIncludeVolume=False,bOverride_PathTracingIncludeIndirectVolume=False,bMobileHQGaussian=False,BloomMethod=BM_SOG,AutoExposureMethod=AEM_Histogram,TemperatureType=TEMP_WhiteBalance,WhiteTemp=6500.000000,WhiteTint=0.000000,ColorSaturation=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrast=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGamma=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGain=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffset=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetShadows=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetMidtones=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetHighlights=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorCorrectionHighlightsMin=0.500000,ColorCorrectionHighlightsMax=1.000000,ColorCorrectionShadowsMax=0.090000,BlueCorrection=0.600000,ExpandGamut=1.000000,ToneCurveAmount=1.000000,FilmSlope=0.880000,FilmToe=0.550000,FilmShoulder=0.260000,FilmBlackClip=0.000000,FilmWhiteClip=0.040000,SceneColorTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SceneFringeIntensity=0.000000,ChromaticAberrationStartOffset=0.000000,BloomIntensity=0.675000,BloomThreshold=-1.000000,BloomSizeScale=4.000000,Bloom1Size=0.300000,Bloom2Size=1.000000,Bloom3Size=2.000000,Bloom4Size=10.000000,Bloom5Size=30.000000,Bloom6Size=64.000000,Bloom1Tint=(R=0.346500,G=0.346500,B=0.346500,A=1.000000),Bloom2Tint=(R=0.138000,G=0.138000,B=0.138000,A=1.000000),Bloom3Tint=(R=0.117600,G=0.117600,B=0.117600,A=1.000000),Bloom4Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom5Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom6Tint=(R=0.061000,G=0.061000,B=0.061000,A=1.000000),BloomConvolutionScatterDispersion=1.000000,BloomConvolutionSize=1.000000,BloomConvolutionTexture=None,BloomConvolutionCenterUV=(X=0.500000,Y=0.500000),BloomConvolutionPreFilterMin=7.000000,BloomConvolutionPreFilterMax=15000.000000,BloomConvolutionPreFilterMult=15.000000,BloomConvolutionBufferScale=0.133000,BloomDirtMask=None,BloomDirtMaskIntensity=0.000000,BloomDirtMaskTint=(R=0.500000,G=0.500000,B=0.500000,A=1.000000),DynamicGlobalIlluminationMethod=Lumen,IndirectLightingColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),IndirectLightingIntensity=1.000000,LumenRayLightingMode=Default,LumenSceneLightingQuality=1.000000,LumenSceneDetail=1.000000,LumenSceneViewDistance=20000.000000,LumenSceneLightingUpdateSpeed=1.000000,LumenFinalGatherQuality=1.000000,LumenFinalGatherLightingUpdateSpeed=1.000000,LumenFinalGatherScreenTraces=True,LumenMaxTraceDistance=20000.000000,LumenDiffuseColorBoost=1.000000,LumenSkylightLeaking=0.000000,LumenSkylightLeakingTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LumenFullSkylightLeakingDistance=1000.000000,LumenSurfaceCacheResolution=1.000000,ReflectionMethod=Lumen,LumenReflectionQuality=1.000000,LumenReflectionsScreenTraces=True,LumenFrontLayerTranslucencyReflections=False,LumenMaxRoughnessToTraceReflections=0.400000,LumenMaxReflectionBounces=1,LumenMaxRefractionBounces=0,ScreenSpaceReflectionIntensity=100.000000,ScreenSpaceReflectionQuality=50.000000,ScreenSpaceReflectionMaxRoughness=0.600000,bMegaLights=True,AmbientCubemapTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),AmbientCubemapIntensity=1.000000,AmbientCubemap=None,CameraShutterSpeed=60.000000,CameraISO=100.000000,DepthOfFieldFstop=4.000000,DepthOfFieldMinFstop=1.200000,DepthOfFieldBladeCount=5,AutoExposureBias=1.000000,AutoExposureBiasBackup=0.000000,bOverride_AutoExposureBiasBackup=False,AutoExposureApplyPhysicalCameraExposure=True,AutoExposureBiasCurve=None,AutoExposureMeterMask=None,AutoExposureLowPercent=10.000000,AutoExposureHighPercent=90.000000,AutoExposureMinBrightness=-10.000000,AutoExposureMaxBrightness=20.000000,AutoExposureSpeedUp=3.000000,AutoExposureSpeedDown=1.000000,HistogramLogMin=-10.000000,HistogramLogMax=20.000000,LocalExposureMethod=Bilateral,LocalExposureHighlightContrastScale=1.000000,LocalExposureShadowContrastScale=1.000000,LocalExposureHighlightContrastCurve=None,LocalExposureShadowContrastCurve=None,LocalExposureHighlightThreshold=0.000000,LocalExposureShadowThreshold=0.000000,LocalExposureDetailStrength=1.000000,LocalExposureBlurredLuminanceBlend=0.600000,LocalExposureBlurredLuminanceKernelSizePercent=50.000000,LocalExposureHighlightThresholdStrength=1.000000,LocalExposureShadowThresholdStrength=1.000000,LocalExposureMiddleGreyBias=0.000000,LensFlareIntensity=1.000000,LensFlareTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LensFlareBokehSize=3.000000,LensFlareThreshold=8.000000,LensFlareBokehShape=None,LensFlareTints[0]=(R=1.000000,G=0.800000,B=0.400000,A=0.600000),LensFlareTints[1]=(R=1.000000,G=1.000000,B=0.600000,A=0.530000),LensFlareTints[2]=(R=0.800000,G=0.800000,B=1.000000,A=0.460000),LensFlareTints[3]=(R=0.500000,G=1.000000,B=0.400000,A=0.390000),LensFlareTints[4]=(R=0.500000,G=0.800000,B=1.000000,A=0.310000),LensFlareTints[5]=(R=0.900000,G=1.000000,B=0.800000,A=0.270000),LensFlareTints[6]=(R=1.000000,G=0.800000,B=0.400000,A=0.220000),LensFlareTints[7]=(R=0.900000,G=0.700000,B=0.700000,A=0.150000),VignetteIntensity=0.400000,Sharpen=0.000000,FilmGrainIntensity=0.000000,FilmGrainIntensityShadows=1.000000,FilmGrainIntensityMidtones=1.000000,FilmGrainIntensityHighlights=1.000000,FilmGrainShadowsMax=0.090000,FilmGrainHighlightsMin=0.500000,FilmGrainHighlightsMax=1.000000,FilmGrainTexelSize=1.000000,FilmGrainTexture=None,AmbientOcclusionIntensity=0.500000,AmbientOcclusionStaticFraction=1.000000,AmbientOcclusionRadius=200.000000,AmbientOcclusionRadiusInWS=False,AmbientOcclusionFadeDistance=8000.000000,AmbientOcclusionFadeRadius=5000.000000,AmbientOcclusionPower=2.000000,AmbientOcclusionBias=3.000000,AmbientOcclusionQuality=50.000000,AmbientOcclusionMipBlend=0.600000,AmbientOcclusionMipScale=1.700000,AmbientOcclusionMipThreshold=0.010000,AmbientOcclusionTemporalBlendWeight=0.100000,RayTracingAO=False,RayTracingAOSamplesPerPixel=1,RayTracingAOIntensity=1.000000,RayTracingAORadius=200.000000,ColorGradingIntensity=1.000000,ColorGradingLUT=None,DepthOfFieldSensorWidth=24.576000,DepthOfFieldSqueezeFactor=1.000000,DepthOfFieldFocalDistance=0.000000,DepthOfFieldDepthBlurAmount=1.000000,DepthOfFieldDepthBlurRadius=0.000000,DepthOfFieldUseHairDepth=False,DepthOfFieldPetzvalBokeh=0.000000,DepthOfFieldPetzvalBokehFalloff=1.000000,DepthOfFieldPetzvalExclusionBoxExtents=(X=0.000000,Y=0.000000),DepthOfFieldPetzvalExclusionBoxRadius=0.000000,DepthOfFieldAspectRatioScalar=1.000000,DepthOfFieldBarrelRadius=5.000000,DepthOfFieldBarrelLength=0.000000,DepthOfFieldMatteBoxFlags[0]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[1]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[2]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldFocalRegion=0.000000,DepthOfFieldNearTransitionRegion=300.000000,DepthOfFieldFarTransitionRegion=500.000000,DepthOfFieldScale=0.000000,DepthOfFieldNearBlurSize=15.000000,DepthOfFieldFarBlurSize=15.000000,DepthOfFieldOcclusion=0.400000,DepthOfFieldSkyFocusDistance=0.000000,DepthOfFieldVignetteSize=200.000000,MotionBlurAmount=0.500000,MotionBlurMax=5.000000,MotionBlurTargetFPS=30,MotionBlurPerObjectSize=0.000000,TranslucencyType=Raster,RayTracingTranslucencyMaxRoughness=0.600000,RayTracingTranslucencyRefractionRays=3,RayTracingTranslucencySamplesPerPixel=1,RayTracingTranslucencyMaxPrimaryHitEvents=4,RayTracingTranslucencyMaxSecondaryHitEvents=2,RayTracingTranslucencyShadows=Hard_shadows,RayTracingTranslucencyRefraction=True,RayTracingTranslucencyUseRayTracedRefraction=False,PathTracingMaxBounces=32,PathTracingSamplesPerPixel=2048,PathTracingMaxPathIntensity=24.000000,PathTracingEnableEmissiveMaterials=True,PathTracingEnableReferenceDOF=False,PathTracingEnableReferenceAtmosphere=False,PathTracingEnableDenoiser=True,PathTracingIncludeEmissive=True,PathTracingIncludeDiffuse=True,PathTracingIncludeIndirectDiffuse=True,PathTracingIncludeSpecular=True,PathTracingIncludeIndirectSpecular=True,PathTracingIncludeVolume=True,PathTracingIncludeIndirectVolume=True,UserFlags=0,WeightedBlendables=(Array=)),LightingRigRotation=0.000000,RotationSpeed=2.000000,DirectionalLightRotation=(Pitch=-40.000000,Yaw=-67.500000,Roll=0.000000),bEnableToneMapping=False,bShowMeshEdges=True) ++Profiles=(ProfileName="Grey Ambient",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=4.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=2.000000,bRotateLightingRig=False,bShowEnvironment=True,bShowFloor=True,bShowGrid=True,EnvironmentColor=(R=0.200000,G=0.200000,B=0.200000,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/T_GreyAmbient",bPostProcessingEnabled=False,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=False,bOverride_AutoExposureMaxBrightness=False,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOverride_FilmGrainIntensityMidtones=False,bOverride_FilmGrainIntensityHighlights=False,bOverride_FilmGrainShadowsMax=False,bOverride_FilmGrainHighlightsMin=False,bOverride_FilmGrainHighlightsMax=False,bOverride_FilmGrainTexelSize=False,bOverride_FilmGrainTexture=False,bOverride_AmbientOcclusionIntensity=False,bOverride_AmbientOcclusionStaticFraction=False,bOverride_AmbientOcclusionRadius=False,bOverride_AmbientOcclusionFadeDistance=False,bOverride_AmbientOcclusionFadeRadius=False,bOverride_AmbientOcclusionRadiusInWS=False,bOverride_AmbientOcclusionPower=False,bOverride_AmbientOcclusionBias=False,bOverride_AmbientOcclusionQuality=False,bOverride_AmbientOcclusionMipBlend=False,bOverride_AmbientOcclusionMipScale=False,bOverride_AmbientOcclusionMipThreshold=False,bOverride_AmbientOcclusionTemporalBlendWeight=False,bOverride_RayTracingAO=False,bOverride_RayTracingAOSamplesPerPixel=False,bOverride_RayTracingAOIntensity=False,bOverride_RayTracingAORadius=False,bOverride_IndirectLightingColor=False,bOverride_IndirectLightingIntensity=False,bOverride_ColorGradingIntensity=False,bOverride_ColorGradingLUT=False,bOverride_DepthOfFieldFocalDistance=False,bOverride_DepthOfFieldFstop=False,bOverride_DepthOfFieldMinFstop=False,bOverride_DepthOfFieldBladeCount=False,bOverride_DepthOfFieldSensorWidth=False,bOverride_DepthOfFieldSqueezeFactor=False,bOverride_DepthOfFieldDepthBlurRadius=False,bOverride_DepthOfFieldUseHairDepth=False,bOverride_DepthOfFieldPetzvalBokeh=False,bOverride_DepthOfFieldPetzvalBokehFalloff=False,bOverride_DepthOfFieldPetzvalExclusionBoxExtents=False,bOverride_DepthOfFieldPetzvalExclusionBoxRadius=False,bOverride_DepthOfFieldAspectRatioScalar=False,bOverride_DepthOfFieldMatteBoxFlags=False,bOverride_DepthOfFieldBarrelRadius=False,bOverride_DepthOfFieldBarrelLength=False,bOverride_DepthOfFieldDepthBlurAmount=False,bOverride_DepthOfFieldFocalRegion=False,bOverride_DepthOfFieldNearTransitionRegion=False,bOverride_DepthOfFieldFarTransitionRegion=False,bOverride_DepthOfFieldScale=False,bOverride_DepthOfFieldNearBlurSize=False,bOverride_DepthOfFieldFarBlurSize=False,bOverride_MobileHQGaussian=False,bOverride_DepthOfFieldOcclusion=False,bOverride_DepthOfFieldSkyFocusDistance=False,bOverride_DepthOfFieldVignetteSize=False,bOverride_MotionBlurAmount=False,bOverride_MotionBlurMax=False,bOverride_MotionBlurTargetFPS=False,bOverride_MotionBlurPerObjectSize=False,bOverride_ReflectionMethod=False,bOverride_LumenReflectionQuality=False,bOverride_ScreenSpaceReflectionIntensity=False,bOverride_ScreenSpaceReflectionQuality=False,bOverride_ScreenSpaceReflectionMaxRoughness=False,bOverride_ScreenSpaceReflectionRoughnessScale=False,bOverride_UserFlags=False,bOverride_RayTracingReflectionsMaxRoughness=False,bOverride_RayTracingReflectionsMaxBounces=False,bOverride_RayTracingReflectionsSamplesPerPixel=False,bOverride_RayTracingReflectionsShadows=False,bOverride_RayTracingReflectionsTranslucency=False,bOverride_TranslucencyType=False,bOverride_RayTracingTranslucencyMaxRoughness=False,bOverride_RayTracingTranslucencyRefractionRays=False,bOverride_RayTracingTranslucencySamplesPerPixel=False,bOverride_RayTracingTranslucencyShadows=False,bOverride_RayTracingTranslucencyRefraction=False,bOverride_RayTracingTranslucencyMaxPrimaryHitEvents=False,bOverride_RayTracingTranslucencyMaxSecondaryHitEvents=False,bOverride_RayTracingTranslucencyUseRayTracedRefraction=False,bOverride_DynamicGlobalIlluminationMethod=False,bOverride_LumenSceneLightingQuality=False,bOverride_LumenSceneDetail=False,bOverride_LumenSceneViewDistance=False,bOverride_LumenSceneLightingUpdateSpeed=False,bOverride_LumenFinalGatherQuality=False,bOverride_LumenFinalGatherLightingUpdateSpeed=False,bOverride_LumenFinalGatherScreenTraces=False,bOverride_LumenMaxTraceDistance=False,bOverride_LumenDiffuseColorBoost=False,bOverride_LumenSkylightLeaking=False,bOverride_LumenSkylightLeakingTint=False,bOverride_LumenFullSkylightLeakingDistance=False,bOverride_LumenRayLightingMode=False,bOverride_LumenReflectionsScreenTraces=False,bOverride_LumenFrontLayerTranslucencyReflections=False,bOverride_LumenMaxRoughnessToTraceReflections=False,bOverride_LumenMaxReflectionBounces=False,bOverride_LumenMaxRefractionBounces=False,bOverride_LumenSurfaceCacheResolution=False,bOverride_RayTracingGI=False,bOverride_RayTracingGIMaxBounces=False,bOverride_RayTracingGISamplesPerPixel=False,bOverride_PathTracingMaxBounces=False,bOverride_PathTracingSamplesPerPixel=False,bOverride_PathTracingMaxPathIntensity=False,bOverride_PathTracingEnableEmissiveMaterials=False,bOverride_PathTracingEnableReferenceDOF=False,bOverride_PathTracingEnableReferenceAtmosphere=False,bOverride_PathTracingEnableDenoiser=False,bOverride_PathTracingIncludeEmissive=False,bOverride_PathTracingIncludeDiffuse=False,bOverride_PathTracingIncludeIndirectDiffuse=False,bOverride_PathTracingIncludeSpecular=False,bOverride_PathTracingIncludeIndirectSpecular=False,bOverride_PathTracingIncludeVolume=False,bOverride_PathTracingIncludeIndirectVolume=False,bMobileHQGaussian=False,BloomMethod=BM_SOG,AutoExposureMethod=AEM_Histogram,TemperatureType=TEMP_WhiteBalance,WhiteTemp=6500.000000,WhiteTint=0.000000,ColorSaturation=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrast=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGamma=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGain=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffset=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainShadows=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetShadows=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainMidtones=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetMidtones=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorSaturationHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorContrastHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGammaHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorGainHighlights=(X=1.000000,Y=1.000000,Z=1.000000,W=1.000000),ColorOffsetHighlights=(X=0.000000,Y=0.000000,Z=0.000000,W=0.000000),ColorCorrectionHighlightsMin=0.500000,ColorCorrectionHighlightsMax=1.000000,ColorCorrectionShadowsMax=0.090000,BlueCorrection=0.600000,ExpandGamut=1.000000,ToneCurveAmount=1.000000,FilmSlope=0.880000,FilmToe=0.550000,FilmShoulder=0.260000,FilmBlackClip=0.000000,FilmWhiteClip=0.040000,SceneColorTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SceneFringeIntensity=0.000000,ChromaticAberrationStartOffset=0.000000,BloomIntensity=0.675000,BloomThreshold=-1.000000,BloomSizeScale=4.000000,Bloom1Size=0.300000,Bloom2Size=1.000000,Bloom3Size=2.000000,Bloom4Size=10.000000,Bloom5Size=30.000000,Bloom6Size=64.000000,Bloom1Tint=(R=0.346500,G=0.346500,B=0.346500,A=1.000000),Bloom2Tint=(R=0.138000,G=0.138000,B=0.138000,A=1.000000),Bloom3Tint=(R=0.117600,G=0.117600,B=0.117600,A=1.000000),Bloom4Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom5Tint=(R=0.066000,G=0.066000,B=0.066000,A=1.000000),Bloom6Tint=(R=0.061000,G=0.061000,B=0.061000,A=1.000000),BloomConvolutionScatterDispersion=1.000000,BloomConvolutionSize=1.000000,BloomConvolutionTexture=None,BloomConvolutionCenterUV=(X=0.500000,Y=0.500000),BloomConvolutionPreFilterMin=7.000000,BloomConvolutionPreFilterMax=15000.000000,BloomConvolutionPreFilterMult=15.000000,BloomConvolutionBufferScale=0.133000,BloomDirtMask=None,BloomDirtMaskIntensity=0.000000,BloomDirtMaskTint=(R=0.500000,G=0.500000,B=0.500000,A=1.000000),DynamicGlobalIlluminationMethod=Lumen,IndirectLightingColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),IndirectLightingIntensity=1.000000,LumenRayLightingMode=Default,LumenSceneLightingQuality=1.000000,LumenSceneDetail=1.000000,LumenSceneViewDistance=20000.000000,LumenSceneLightingUpdateSpeed=1.000000,LumenFinalGatherQuality=1.000000,LumenFinalGatherLightingUpdateSpeed=1.000000,LumenFinalGatherScreenTraces=True,LumenMaxTraceDistance=20000.000000,LumenDiffuseColorBoost=1.000000,LumenSkylightLeaking=0.000000,LumenSkylightLeakingTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LumenFullSkylightLeakingDistance=1000.000000,LumenSurfaceCacheResolution=1.000000,ReflectionMethod=Lumen,LumenReflectionQuality=1.000000,LumenReflectionsScreenTraces=True,LumenFrontLayerTranslucencyReflections=False,LumenMaxRoughnessToTraceReflections=0.400000,LumenMaxReflectionBounces=1,LumenMaxRefractionBounces=0,ScreenSpaceReflectionIntensity=100.000000,ScreenSpaceReflectionQuality=50.000000,ScreenSpaceReflectionMaxRoughness=0.600000,bMegaLights=True,AmbientCubemapTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),AmbientCubemapIntensity=1.000000,AmbientCubemap=None,CameraShutterSpeed=60.000000,CameraISO=100.000000,DepthOfFieldFstop=4.000000,DepthOfFieldMinFstop=1.200000,DepthOfFieldBladeCount=5,AutoExposureBias=1.000000,AutoExposureBiasBackup=0.000000,bOverride_AutoExposureBiasBackup=False,AutoExposureApplyPhysicalCameraExposure=True,AutoExposureBiasCurve=None,AutoExposureMeterMask=None,AutoExposureLowPercent=10.000000,AutoExposureHighPercent=90.000000,AutoExposureMinBrightness=-10.000000,AutoExposureMaxBrightness=20.000000,AutoExposureSpeedUp=3.000000,AutoExposureSpeedDown=1.000000,HistogramLogMin=-10.000000,HistogramLogMax=20.000000,LocalExposureMethod=Bilateral,LocalExposureHighlightContrastScale=1.000000,LocalExposureShadowContrastScale=1.000000,LocalExposureHighlightContrastCurve=None,LocalExposureShadowContrastCurve=None,LocalExposureHighlightThreshold=0.000000,LocalExposureShadowThreshold=0.000000,LocalExposureDetailStrength=1.000000,LocalExposureBlurredLuminanceBlend=0.600000,LocalExposureBlurredLuminanceKernelSizePercent=50.000000,LocalExposureHighlightThresholdStrength=1.000000,LocalExposureShadowThresholdStrength=1.000000,LocalExposureMiddleGreyBias=0.000000,LensFlareIntensity=1.000000,LensFlareTint=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),LensFlareBokehSize=3.000000,LensFlareThreshold=8.000000,LensFlareBokehShape=None,LensFlareTints[0]=(R=1.000000,G=0.800000,B=0.400000,A=0.600000),LensFlareTints[1]=(R=1.000000,G=1.000000,B=0.600000,A=0.530000),LensFlareTints[2]=(R=0.800000,G=0.800000,B=1.000000,A=0.460000),LensFlareTints[3]=(R=0.500000,G=1.000000,B=0.400000,A=0.390000),LensFlareTints[4]=(R=0.500000,G=0.800000,B=1.000000,A=0.310000),LensFlareTints[5]=(R=0.900000,G=1.000000,B=0.800000,A=0.270000),LensFlareTints[6]=(R=1.000000,G=0.800000,B=0.400000,A=0.220000),LensFlareTints[7]=(R=0.900000,G=0.700000,B=0.700000,A=0.150000),VignetteIntensity=0.400000,Sharpen=0.000000,FilmGrainIntensity=0.000000,FilmGrainIntensityShadows=1.000000,FilmGrainIntensityMidtones=1.000000,FilmGrainIntensityHighlights=1.000000,FilmGrainShadowsMax=0.090000,FilmGrainHighlightsMin=0.500000,FilmGrainHighlightsMax=1.000000,FilmGrainTexelSize=1.000000,FilmGrainTexture=None,AmbientOcclusionIntensity=0.500000,AmbientOcclusionStaticFraction=1.000000,AmbientOcclusionRadius=200.000000,AmbientOcclusionRadiusInWS=False,AmbientOcclusionFadeDistance=8000.000000,AmbientOcclusionFadeRadius=5000.000000,AmbientOcclusionPower=2.000000,AmbientOcclusionBias=3.000000,AmbientOcclusionQuality=50.000000,AmbientOcclusionMipBlend=0.600000,AmbientOcclusionMipScale=1.700000,AmbientOcclusionMipThreshold=0.010000,AmbientOcclusionTemporalBlendWeight=0.100000,RayTracingAO=False,RayTracingAOSamplesPerPixel=1,RayTracingAOIntensity=1.000000,RayTracingAORadius=200.000000,ColorGradingIntensity=1.000000,ColorGradingLUT=None,DepthOfFieldSensorWidth=24.576000,DepthOfFieldSqueezeFactor=1.000000,DepthOfFieldFocalDistance=0.000000,DepthOfFieldDepthBlurAmount=1.000000,DepthOfFieldDepthBlurRadius=0.000000,DepthOfFieldUseHairDepth=False,DepthOfFieldPetzvalBokeh=0.000000,DepthOfFieldPetzvalBokehFalloff=1.000000,DepthOfFieldPetzvalExclusionBoxExtents=(X=0.000000,Y=0.000000),DepthOfFieldPetzvalExclusionBoxRadius=0.000000,DepthOfFieldAspectRatioScalar=1.000000,DepthOfFieldBarrelRadius=5.000000,DepthOfFieldBarrelLength=0.000000,DepthOfFieldMatteBoxFlags[0]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[1]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldMatteBoxFlags[2]=(Pitch=0.000000,Roll=0.000000,Length=0.000000),DepthOfFieldFocalRegion=0.000000,DepthOfFieldNearTransitionRegion=300.000000,DepthOfFieldFarTransitionRegion=500.000000,DepthOfFieldScale=0.000000,DepthOfFieldNearBlurSize=15.000000,DepthOfFieldFarBlurSize=15.000000,DepthOfFieldOcclusion=0.400000,DepthOfFieldSkyFocusDistance=0.000000,DepthOfFieldVignetteSize=200.000000,MotionBlurAmount=0.500000,MotionBlurMax=5.000000,MotionBlurTargetFPS=30,MotionBlurPerObjectSize=0.000000,TranslucencyType=Raster,RayTracingTranslucencyMaxRoughness=0.600000,RayTracingTranslucencyRefractionRays=3,RayTracingTranslucencySamplesPerPixel=1,RayTracingTranslucencyMaxPrimaryHitEvents=4,RayTracingTranslucencyMaxSecondaryHitEvents=2,RayTracingTranslucencyShadows=Hard_shadows,RayTracingTranslucencyRefraction=True,RayTracingTranslucencyUseRayTracedRefraction=False,PathTracingMaxBounces=32,PathTracingSamplesPerPixel=2048,PathTracingMaxPathIntensity=24.000000,PathTracingEnableEmissiveMaterials=True,PathTracingEnableReferenceDOF=False,PathTracingEnableReferenceAtmosphere=False,PathTracingEnableDenoiser=True,PathTracingIncludeEmissive=True,PathTracingIncludeDiffuse=True,PathTracingIncludeIndirectDiffuse=True,PathTracingIncludeSpecular=True,PathTracingIncludeIndirectSpecular=True,PathTracingIncludeVolume=True,PathTracingIncludeIndirectVolume=True,UserFlags=0,WeightedBlendables=(Array=)),LightingRigRotation=0.000000,RotationSpeed=2.000000,DirectionalLightRotation=(Pitch=-40.000000,Yaw=-67.500000,Roll=0.000000),bEnableToneMapping=False,bShowMeshEdges=False) + diff --git a/Config/DefaultEditorPerProjectUserSettings.ini b/Config/DefaultEditorPerProjectUserSettings.ini new file mode 100644 index 0000000..220a551 --- /dev/null +++ b/Config/DefaultEditorPerProjectUserSettings.ini @@ -0,0 +1,2 @@ +[ContentBrowser] +ContentBrowserTab1.SelectedPaths=/Game/FirstPerson \ No newline at end of file diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini new file mode 100644 index 0000000..82ff0de --- /dev/null +++ b/Config/DefaultEngine.ini @@ -0,0 +1,271 @@ +[/Script/Engine.CollisionProfile] ++Profiles=(Name="Projectile",CollisionEnabled=QueryOnly,ObjectTypeName="Projectile",CustomResponses=,HelpMessage="Preset for projectiles",bCanModify=True) ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,Name="Projectile",DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False) ++EditProfiles=(Name="Trigger",CustomResponses=((Channel=Projectile, Response=ECR_Ignore))) + +[/Script/EngineSettings.GameMapsSettings] +EditorStartupMap=/Game/FirstPerson/Lvl_FirstPerson.Lvl_FirstPerson +LocalMapOptions= +TransitionMap= +bUseSplitscreen=True +TwoPlayerSplitscreenLayout=Horizontal +ThreePlayerSplitscreenLayout=FavorTop +GameInstanceClass=/Script/Engine.GameInstance +GameDefaultMap=/Game/FirstPerson/Lvl_FirstPerson.Lvl_FirstPerson +ServerDefaultMap=/Engine/Maps/Entry +GlobalDefaultGameMode=/Game/FirstPerson/Blueprints/BP_FirstPersonGameMode.BP_FirstPersonGameMode_C +GlobalDefaultServerGameMode=None + +[/Script/Engine.RendererSettings] +r.Mobile.ShadingPath=0 +r.Mobile.AllowDeferredShadingOpenGL=False +r.Mobile.SupportGPUScene=True +r.Mobile.AntiAliasing=1 +r.Mobile.FloatPrecisionMode=0 +r.Mobile.AllowDitheredLODTransition=False +r.Mobile.VirtualTextures=False +r.DiscardUnusedQuality=False +r.AllowOcclusionQueries=True +r.MinScreenRadiusForLights=0.030000 +r.MinScreenRadiusForDepthPrepass=0.030000 +r.PrecomputedVisibilityWarning=False +r.TextureStreaming=True +Compat.UseDXT5NormalMaps=False +r.VirtualTextures=True +r.VirtualTexturedLightmaps=False +r.VT.TileSize=128 +r.VT.TileBorderSize=4 +r.VT.AnisotropicFiltering=False +r.VT.EnableAutoImport=False +bEnableVirtualTextureOpacityMask=True +r.MeshPaintVirtualTexture.Support=True +r.MeshPaintVirtualTexture.TileSize=32 +r.MeshPaintVirtualTexture.TileBorderSize=2 +r.MeshPaintVirtualTexture.UseCompression=True +r.StaticMesh.DefaultMeshPaintTextureSupport=True +r.MeshPaintVirtualTexture.DefaultTexelsPerVertex=4 +r.MeshPaintVirtualTexture.MaxTextureSize=4096 +r.vt.rvt.EnableBaseColor=True +r.vt.rvt.EnableBaseColorRoughness=True +r.vt.rvt.EnableBaseColorSpecular=True +r.vt.rvt.EnableMask4=True +r.vt.rvt.EnableWorldHeight=True +r.vt.rvt.EnableDisplacement=True +r.vt.rvt.HighQualityPerPixelHeight=True +WorkingColorSpaceChoice=sRGB +RedChromaticityCoordinate=(X=0.640000,Y=0.330000) +GreenChromaticityCoordinate=(X=0.300000,Y=0.600000) +BlueChromaticityCoordinate=(X=0.150000,Y=0.060000) +WhiteChromaticityCoordinate=(X=0.312700,Y=0.329000) +r.LegacyLuminanceFactors=False +r.ClearCoatNormal=False +r.DynamicGlobalIlluminationMethod=1 +r.ReflectionMethod=1 +r.ReflectionCaptureResolution=128 +r.ReflectionEnvironmentLightmapMixBasedOnRoughness=True +r.Lumen.HardwareRayTracing=True +r.Lumen.HardwareRayTracing.LightingMode=0 +r.Lumen.TranslucencyReflections.FrontLayer.EnableForProject=False +r.Lumen.TraceMeshSDFs=0 +r.Lumen.ScreenTracingSource=0 +r.Lumen.Reflections.HardwareRayTracing.Translucent.Refraction.EnableForProject=True +r.MegaLights.EnableForProject=False +r.RayTracing.Shadows=False +r.Shadow.Virtual.Enable=1 +r.RayTracing=True +r.RayTracing.RayTracingProxies.ProjectEnabled=True +r.RayTracing.UseTextureLod=False +r.PathTracing=True +r.GenerateMeshDistanceFields=True +r.DistanceFields.DefaultVoxelDensity=0.200000 +r.Nanite.ProjectEnabled=True +r.AllowStaticLighting=False +r.NormalMapsForStaticLighting=False +r.ForwardShading=False +r.VertexFoggingForOpaque=True +r.SeparateTranslucency=True +r.TranslucentSortPolicy=0 +TranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000) +r.LocalFogVolume.ApplyOnTranslucent=False +xr.VRS.FoveationLevel=0 +xr.VRS.DynamicFoveation=False +r.CustomDepth=1 +r.CustomDepthTemporalAAJitter=True +r.PostProcessing.PropagateAlpha=False +r.Deferred.SupportPrimitiveAlphaHoldout=False +r.DefaultFeature.Bloom=True +r.DefaultFeature.AmbientOcclusion=True +r.DefaultFeature.AmbientOcclusionStaticFraction=True +r.DefaultFeature.AutoExposure=False +r.DefaultFeature.AutoExposure.Method=0 +r.DefaultFeature.AutoExposure.Bias=1.000000 +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.800000 +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.800000 +r.DefaultFeature.MotionBlur=False +r.DefaultFeature.LensFlare=False +r.TemporalAA.Upsampling=True +r.AntiAliasingMethod=0 +r.MSAACount=4 +r.DefaultFeature.LightUnits=1 +r.DefaultBackBufferPixelFormat=4 +r.ScreenPercentage.Default=100.000000 +r.ScreenPercentage.Default.Desktop.Mode=1 +r.ScreenPercentage.Default.Mobile.Mode=0 +r.ScreenPercentage.Default.VR.Mode=0 +r.ScreenPercentage.Default.PathTracer.Mode=0 +r.Shadow.UnbuiltPreviewInGame=True +r.StencilForLODDither=False +r.EarlyZPass=3 +r.EarlyZPassOnlyMaterialMasking=False +r.Shadow.CSMCaching=False +r.DBuffer=True +r.ClearSceneMethod=1 +r.VelocityOutputPass=0 +r.Velocity.EnableVertexDeformation=2 +r.SelectiveBasePassOutputs=False +bDefaultParticleCutouts=False +fx.GPUSimulationTextureSizeX=1024 +fx.GPUSimulationTextureSizeY=1024 +r.AllowGlobalClipPlane=False +r.GBufferFormat=1 +r.MorphTarget.Mode=True +r.MorphTarget.MaxBlendWeight=5.000000 +r.SupportSkyAtmosphere=True +r.SupportSkyAtmosphereAffectsHeightFog=True +r.SupportExpFogMatchesVolumetricFog=False +r.SupportLocalFogVolumes=True +r.SupportCloudShadowOnForwardLitTranslucent=False +r.LightFunctionAtlas.Format=0 +r.VolumetricFog.LightFunction=True +r.Deferred.UsesLightFunctionAtlas=True +r.SingleLayerWater.UsesLightFunctionAtlas=False +r.Translucent.UsesLightFunctionAtlas=False +r.Translucent.UsesIESProfiles=False +r.Translucent.UsesRectLights=False +r.Translucent.UsesShadowedLocalLights=False +r.GPUCrashDebugging=False +vr.InstancedStereo=False +r.MobileHDR=True +vr.MobileMultiView=False +r.Mobile.UseHWsRGBEncoding=False +vr.RoundRobinOcclusion=False +r.MeshStreaming=False +r.HeterogeneousVolumes=True +r.HeterogeneousVolumes.Shadows=False +r.Translucency.HeterogeneousVolumes=False +r.WireframeCullThreshold=5.000000 +r.SupportStationarySkylight=True +r.SupportLowQualityLightmaps=True +r.SupportPointLightWholeSceneShadows=True +r.Shadow.TranslucentPerObject.ProjectEnabled=False +r.Water.SingleLayerWater.SupportCloudShadow=False +r.Substrate=False +r.Substrate.OpaqueMaterialRoughRefraction=False +r.Refraction.Blur=True +r.Substrate.Debug.AdvancedVisualizationShaders=False +r.Substrate.EnableLayerSupport=False +r.Material.RoughDiffuse=False +r.Material.EnergyConservation=False +r.Material.DefaultAutoMaterialUsage=True +r.OIT.SortedPixels=False +r.HairStrands.LODMode=True +r.SkinCache.CompileShaders=True +r.VRS.Support=True +r.SkinCache.SkipCompilingGPUSkinVF=False +r.SkinCache.DefaultBehavior=1 +r.SkinCache.SceneMemoryLimitInMB=128.000000 +r.Mobile.EnableStaticAndCSMShadowReceivers=True +r.Mobile.EnableMovableLightCSMShaderCulling=True +r.Mobile.Forward.EnableLocalLights=1 +r.Mobile.Forward.EnableClusteredReflections=False +r.Mobile.AllowDistanceFieldShadows=True +r.Mobile.EnableMovableSpotlightsShadow=False +r.GPUSkin.Support16BitBoneIndex=False +r.GPUSkin.Limit2BoneInfluences=False +r.SupportDepthOnlyIndexBuffers=False +r.SupportReversedIndexBuffers=False +r.Mobile.AmbientOcclusion=False +r.Mobile.DBuffer=False +r.GPUSkin.UnlimitedBoneInfluences=False +r.GPUSkin.AlwaysUseDeformerForUnlimitedBoneInfluences=False +r.GPUSkin.UnlimitedBoneInfluencesThreshold=8 +DefaultBoneInfluenceLimit=(Default=0,PerPlatform=()) +MaxSkinBones=(Default=65536,PerPlatform=(("Mobile", 256))) +r.Mobile.ScreenSpaceReflections=False +r.Mobile.SupportsGen4TAA=True +bStreamSkeletalMeshLODs=(Default=False,PerPlatform=()) +bDiscardSkeletalMeshOptionalLODs=(Default=False,PerPlatform=()) +VisualizeCalibrationColorMaterialPath=/Engine/EngineMaterials/PPM_DefaultCalibrationColor.PPM_DefaultCalibrationColor +VisualizeCalibrationCustomMaterialPath=None +VisualizeCalibrationGrayscaleMaterialPath=/Engine/EngineMaterials/PPM_DefaultCalibrationGrayscale.PPM_DefaultCalibrationGrayscale + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +-D3D12TargetedShaderFormats=PCD3D_SM5 ++D3D12TargetedShaderFormats=PCD3D_SM6 +-D3D11TargetedShaderFormats=PCD3D_SM5 ++D3D11TargetedShaderFormats=PCD3D_SM5 +Compiler=Default +AudioSampleRate=48000 +AudioCallbackBufferFrameSize=1024 +AudioNumBuffersToEnqueue=1 +AudioMaxChannels=0 +AudioNumSourceWorkers=4 +SpatializationPlugin= +SourceDataOverridePlugin= +ReverbPlugin= +OcclusionPlugin= +CompressionOverrides=(bOverrideCompressionTimes=False,DurationThreshold=5.000000,MaxNumRandomBranches=0,SoundCueQualityIndex=0) +CacheSizeKB=65536 +MaxChunkSizeOverrideKB=0 +bResampleForDevice=False +MaxSampleRate=48000.000000 +HighSampleRate=32000.000000 +MedSampleRate=24000.000000 +LowSampleRate=12000.000000 +MinSampleRate=8000.000000 +CompressionQualityModifier=1.000000 +AutoStreamingThreshold=0.000000 +SoundCueCookQualityIndex=-1 + +[/Script/LinuxTargetPlatform.LinuxTargetSettings] +-TargetedRHIs=SF_VULKAN_SM5 ++TargetedRHIs=SF_VULKAN_SM6 + +[/Script/AIModule.AISystem] +bForgetStaleActors=True + +[/Script/Engine.Engine] +NearClipPlane=5.000000 + + ++ActiveGameNameRedirects=(OldGameName="TP_FirstPerson",NewGameName="/Script/AudioVideoRecord") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_FirstPerson",NewGameName="/Script/AudioVideoRecord") + ++ActiveClassRedirects=(OldClassName="TP_FirstPersonPlayerController",NewClassName="AudioVideoRecordPlayerController") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonGameMode",NewClassName="AudioVideoRecordGameMode") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonCharacter",NewClassName="AudioVideoRecordCharacter") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonCameraManager",NewClassName="AudioVideoRecordCameraManager") + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Scalable +AppliedDefaultGraphicsPerformance=Scalable + +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=25B552C94F7528D1B0113AAF7D49281C +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= + diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini new file mode 100644 index 0000000..017a152 --- /dev/null +++ b/Config/DefaultGame.ini @@ -0,0 +1,6 @@ +[ProjectSettings] +ProjectID=(A=1823396784,B=1298598689,C=1743498150,D=-2048051708) +ProjectName=First Person Template + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=2A034C0D43D2E43A5B2F0DBD36F59FF2 diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini new file mode 100644 index 0000000..0d0b0e5 --- /dev/null +++ b/Config/DefaultInput.ini @@ -0,0 +1,109 @@ +[/Script/Engine.InputSettings] +-AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) ++AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Touch",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bAltEnterTogglesFullscreen=True +bF11TogglesFullscreen=True +bUseMouseForTouch=False +bEnableMouseSmoothing=True +bEnableFOVScaling=True +bCaptureMouseOnLaunch=True +bEnableLegacyInputScales=True +bEnableMotionControls=True +bFilterInputByPlatformUser=False +bShouldFlushPressedKeysOnViewportFocusLost=True +bAlwaysShowTouchInterface=False +bShowConsoleOnFourFingerTap=True +bEnableGestureRecognizer=False +bUseAutocorrect=False +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +DefaultViewportMouseLockMode=LockOnCapture +FOVScale=0.011110 +DoubleClickTime=0.200000 +DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput +DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent +DefaultTouchInterface=/Game/FirstPerson/Input/MobileControls.MobileControls +-ConsoleKeys=Tilde ++ConsoleKeys=Tilde + diff --git a/Plugins/GameplayRecorder/GameplayRecorder.uplugin b/Plugins/GameplayRecorder/GameplayRecorder.uplugin new file mode 100644 index 0000000..38beda3 --- /dev/null +++ b/Plugins/GameplayRecorder/GameplayRecorder.uplugin @@ -0,0 +1,19 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Gameplay Recorder", + "Description": "Cross-GPU gameplay recording plugin. Captures video (NVENC/AMF/QSV/x264) and audio, muxes via FFmpeg.", + "Category": "Recording", + "CreatedBy": "AudioVideoRecord Project", + "CanContainContent": false, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "GameplayRecorder", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ] +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.cpp new file mode 100644 index 0000000..4459b24 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.cpp @@ -0,0 +1,255 @@ +// AudioMixerRecorder.cpp +// ───────────────────────────────────────────────────────────────────── +// Per-participant + mixed voice audio recording. +// ───────────────────────────────────────────────────────────────────── + +#include "AudioMixerRecorder.h" +#include "RecorderModule.h" + +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFileManager.h" + +// ===================================================================== +// Construction / Destruction +// ===================================================================== + +FAudioMixerRecorder::FAudioMixerRecorder() = default; +FAudioMixerRecorder::~FAudioMixerRecorder() = default; + +// ===================================================================== +// Start / Stop +// ===================================================================== + +void FAudioMixerRecorder::Start() +{ + bActive = true; + + { + FScopeLock Lock(&MixedLock); + MixedBuffer.Empty(); + } + { + FScopeLock Lock(&ParticipantLock); + ParticipantBuffers.Empty(); + } + + UE_LOG(LogGameplayRecorder, Log, TEXT("AudioMixerRecorder: Started.")); +} + +void FAudioMixerRecorder::Stop() +{ + bActive = false; + UE_LOG(LogGameplayRecorder, Log, TEXT("AudioMixerRecorder: Stopped.")); +} + +// ===================================================================== +// FeedParticipantAudio — called per voice chunk per participant +// ===================================================================== + +void FAudioMixerRecorder::FeedParticipantAudio( + const FString& UserID, + const float* Samples, + int32 InNumSamples, + bool bIsMuted) +{ + if (!bActive || !Samples || InNumSamples <= 0) + { + return; + } + + // ── 1. Always save to the individual track (even if muted) ────── + // This enables post-session analysis of what a muted person said. + { + FScopeLock Lock(&ParticipantLock); + + TArray& IndividualBuf = ParticipantBuffers.FindOrAdd(UserID); + IndividualBuf.Append(Samples, InNumSamples); + } + + // ── 2. If NOT muted, also mix into the combined buffer ────────── + if (!bIsMuted) + { + FScopeLock Lock(&MixedLock); + + const int32 CurrentLen = MixedBuffer.Num(); + if (CurrentLen < CurrentLen + InNumSamples) + { + // Extend the buffer to fit + const int32 Needed = (CurrentLen + InNumSamples) - MixedBuffer.Num(); + if (Needed > 0) + { + MixedBuffer.AddZeroed(Needed); + } + } + + // Additive mix (sum the samples on top of what's already there) + // This correctly overlaps voices that speak simultaneously. + // + // IMPORTANT: We add from the END of the existing mixed buffer. + // The mixed buffer grows as chunks arrive; each participant's + // chunk is appended at the tail so they stay time-aligned. + // + // A more precise approach would use timestamps, but for a + // synchronous in-process scenario the append order is correct. + const int32 WriteOffset = CurrentLen; + for (int32 i = 0; i < InNumSamples; ++i) + { + if (WriteOffset + i < MixedBuffer.Num()) + { + MixedBuffer[WriteOffset + i] += Samples[i]; + } + } + } +} + +// ===================================================================== +// FeedGameAudio — game-world audio from the submix +// ===================================================================== + +void FAudioMixerRecorder::FeedGameAudio(const float* Samples, int32 InNumSamples) +{ + if (!bActive || !Samples || InNumSamples <= 0) + { + return; + } + + FScopeLock Lock(&MixedLock); + + const int32 CurrentLen = MixedBuffer.Num(); + const int32 NewLen = CurrentLen + InNumSamples; + + if (MixedBuffer.Num() < NewLen) + { + MixedBuffer.AddZeroed(NewLen - MixedBuffer.Num()); + } + + // Additive mix at the tail + for (int32 i = 0; i < InNumSamples; ++i) + { + MixedBuffer[CurrentLen + i] += Samples[i]; + } +} + +// ===================================================================== +// SaveMixedWav — combined voice + game audio +// ===================================================================== + +bool FAudioMixerRecorder::SaveMixedWav(const FString& FilePath) const +{ + FScopeLock Lock(&const_cast(this)->MixedLock); + + if (MixedBuffer.Num() == 0) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("AudioMixerRecorder: No mixed audio data to save.")); + return false; + } + + UE_LOG(LogGameplayRecorder, Log, + TEXT("AudioMixerRecorder: Saving mixed WAV (%d samples) → %s"), + MixedBuffer.Num(), *FilePath); + + return WriteWavFile(FilePath, MixedBuffer, SampleRate, NumChannels); +} + +// ===================================================================== +// SaveIndividualWavs — one file per participant +// ===================================================================== + +int32 FAudioMixerRecorder::SaveIndividualWavs(const FString& OutputDir) const +{ + FScopeLock Lock(&const_cast(this)->ParticipantLock); + + // Ensure directory exists + IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile(); + if (!PF.DirectoryExists(*OutputDir)) + { + PF.CreateDirectoryTree(*OutputDir); + } + + int32 SavedCount = 0; + for (const auto& Pair : ParticipantBuffers) + { + const FString& UserID = Pair.Key; + const TArray& Buf = Pair.Value; + + if (Buf.Num() == 0) + { + continue; + } + + // Sanitize UserID for filename (replace spaces, special chars) + FString SafeID = UserID; + SafeID.ReplaceCharInline(TEXT(' '), TEXT('_')); + + FString Path = FPaths::Combine(OutputDir, SafeID + TEXT("_audio.wav")); + + if (WriteWavFile(Path, Buf, SampleRate, NumChannels)) + { + UE_LOG(LogGameplayRecorder, Log, + TEXT(" Individual WAV: %s (%d samples)"), *Path, Buf.Num()); + ++SavedCount; + } + } + + UE_LOG(LogGameplayRecorder, Log, + TEXT("AudioMixerRecorder: Saved %d individual track(s)."), SavedCount); + + return SavedCount; +} + +// ===================================================================== +// WriteWavFile — static helper: float[] → 16-bit PCM WAV +// ===================================================================== + +bool FAudioMixerRecorder::WriteWavFile( + const FString& Path, + const TArray& Samples, + int32 InSampleRate, + int32 InNumChannels) +{ + // Convert float [-1,1] → int16 + TArray PCM16; + PCM16.SetNumUninitialized(Samples.Num()); + for (int32 i = 0; i < Samples.Num(); ++i) + { + float Clamped = FMath::Clamp(Samples[i], -1.0f, 1.0f); + PCM16[i] = static_cast(Clamped * 32767.0f); + } + + const int32 BitsPerSample = 16; + const int32 BytesPerSample = BitsPerSample / 8; + const int32 DataSize = PCM16.Num() * BytesPerSample; + const int32 ByteRate = InSampleRate * InNumChannels * BytesPerSample; + const int16 BlockAlign = static_cast(InNumChannels * BytesPerSample); + + TArray Wav; + Wav.Reserve(44 + DataSize); + + auto Write4CC = [&](const char* T) { Wav.Append(reinterpret_cast(T), 4); }; + auto WriteI32 = [&](int32 V) { Wav.Append(reinterpret_cast(&V), 4); }; + auto WriteI16 = [&](int16 V) { Wav.Append(reinterpret_cast(&V), 2); }; + + // RIFF header + Write4CC("RIFF"); + WriteI32(36 + DataSize); + Write4CC("WAVE"); + + // fmt chunk + Write4CC("fmt "); + WriteI32(16); // PCM sub-chunk size + WriteI16(1); // audio format = PCM + WriteI16(static_cast(InNumChannels)); + WriteI32(InSampleRate); + WriteI32(ByteRate); + WriteI16(BlockAlign); + WriteI16(static_cast(BitsPerSample)); + + // data chunk + Write4CC("data"); + WriteI32(DataSize); + Wav.Append(reinterpret_cast(PCM16.GetData()), DataSize); + + return FFileHelper::SaveArrayToFile(Wav, *Path); +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.h new file mode 100644 index 0000000..c911b07 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioMixerRecorder.h @@ -0,0 +1,98 @@ +// AudioMixerRecorder.h +// ───────────────────────────────────────────────────────────────────── +// Records voice audio on a per-participant AND mixed basis. +// +// Two recording modes happen simultaneously: +// 1. PER-PARTICIPANT — each participant's mic feed is saved to its +// own buffer → individual WAV files (pilot_audio.wav, etc.) +// 2. MIXED — all non-muted participants' audio is summed into a +// single buffer → the combined audio_only.wav +// +// How audio arrives here: +// VoiceSessionManager captures mic data per participant and calls +// FeedParticipantAudio(). This class simply stores the samples. +// +// JS analogy: imagine multiple MediaStreams, each pushing data into +// their own ArrayBuffer, while you also mix them into one via a +// GainNode → destination pattern. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "VoiceTypes.h" + +// Forward declaration +class UParticipantManager; + +class GAMEPLAYRECORDER_API FAudioMixerRecorder +{ +public: + FAudioMixerRecorder(); + ~FAudioMixerRecorder(); + + // ── Configuration ─────────────────────────────────────────────── + int32 SampleRate = 48000; + int32 NumChannels = 2; // stereo + + // ── Lifecycle ─────────────────────────────────────────────────── + + /** Start a new recording session. Clears all buffers. */ + void Start(); + + /** Mark recording as stopped. No more samples accepted. */ + void Stop(); + + bool IsActive() const { return bActive; } + + // ── Feeding audio data ────────────────────────────────────────── + + /** + * Push a chunk of float PCM samples for a specific participant. + * Called from the voice capture thread or audio thread. + * + * @param UserID Participant's unique ID. + * @param Samples Interleaved float PCM data. + * @param NumSamples Total number of float values (frames × channels). + * @param bIsMuted If true, samples go to the individual track but + * NOT the mixed track (muted participants are still + * recorded individually for post-analysis). + */ + void FeedParticipantAudio(const FString& UserID, const float* Samples, + int32 NumSamples, bool bIsMuted); + + /** + * Push game-world audio (submix) into the mixed buffer. + * This lets the final audio_only.wav contain gameplay sound too. + */ + void FeedGameAudio(const float* Samples, int32 NumSamples); + + // ── Saving ────────────────────────────────────────────────────── + + /** Save the combined (mixed) buffer as a WAV. Returns true on success. */ + bool SaveMixedWav(const FString& FilePath) const; + + /** + * Save each participant's individual track as a separate WAV. + * Files are named: /_audio.wav + * Returns the number of tracks saved. + */ + int32 SaveIndividualWavs(const FString& OutputDir) const; + +private: + bool bActive = false; + + // ── Buffers ───────────────────────────────────────────────────── + + /** Combined mix of all non-muted voices + game audio. */ + TArray MixedBuffer; + FCriticalSection MixedLock; + + /** Per-participant buffers: UserID → float samples. */ + TMap> ParticipantBuffers; + FCriticalSection ParticipantLock; + + // ── WAV writing helper ────────────────────────────────────────── + static bool WriteWavFile(const FString& Path, const TArray& Samples, + int32 InSampleRate, int32 InNumChannels); +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.cpp new file mode 100644 index 0000000..241d499 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.cpp @@ -0,0 +1,265 @@ +// AudioRecorder.cpp +// ───────────────────────────────────────────────────────────────────── +// Audio capture via Unreal's submix listener + WAV export. +// ───────────────────────────────────────────────────────────────────── + +#include "AudioRecorder.h" +#include "RecorderModule.h" // LogGameplayRecorder + +#include "AudioDevice.h" +#include "AudioMixerDevice.h" +#include "Engine/Engine.h" +#include "Misc/FileHelper.h" + +// ===================================================================== +// FSubmixBridge — wraps callback in TSharedRef for UE 5.6 API +// ===================================================================== +class FAudioRecorderSubmixBridge + : public ISubmixBufferListener +{ +public: + FAudioRecorder* Owner; + + FAudioRecorderSubmixBridge(FAudioRecorder* InOwner) : Owner(InOwner) {} + + virtual void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock) override + { + if (Owner) + { + Owner->OnNewSubmixBuffer(OwningSubmix, AudioData, NumSamples, NumChannels, SampleRate, AudioClock); + } + } +}; + +// ===================================================================== +// Construction / Destruction +// ===================================================================== + +FAudioRecorder::FAudioRecorder() = default; + +FAudioRecorder::~FAudioRecorder() +{ + Stop(); +} + +// ===================================================================== +// Start — register as a submix listener +// ===================================================================== + +void FAudioRecorder::Start(USoundSubmix* OptionalSubmix) +{ + if (bActive) + { + UE_LOG(LogGameplayRecorder, Warning, TEXT("AudioRecorder: Already active.")); + return; + } + + Reset(); + bActive = true; + + // Grab the main audio device + if (!GEngine || !GEngine->GetMainAudioDevice()) + { + UE_LOG(LogGameplayRecorder, Error, TEXT("AudioRecorder: No audio device available.")); + bActive = false; + return; + } + + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (!AudioDevice) + { + UE_LOG(LogGameplayRecorder, Error, TEXT("AudioRecorder: GetAudioDevice() returned null.")); + bActive = false; + return; + } + + // Resolve the submix to listen on. + // If the caller didn't pass one, use the master submix (captures everything). + USoundSubmix* Submix = OptionalSubmix; + if (!Submix) + { + Submix = &AudioDevice->GetMainSubmixObject(); + } + + if (!Submix) + { + UE_LOG(LogGameplayRecorder, Error, TEXT("AudioRecorder: Could not find a submix.")); + bActive = false; + return; + } + + RegisteredSubmix = Submix; + + // Create a bridge and register it with the audio device. + SubmixBridge = MakeShared(this); + AudioDevice->RegisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *Submix); + UE_LOG(LogGameplayRecorder, Log, TEXT("AudioRecorder: Listening on submix '%s'."), + *Submix->GetName()); +} + +// ===================================================================== +// Stop — unregister from the audio device +// ===================================================================== + +void FAudioRecorder::Stop() +{ + if (!bActive) + { + return; + } + + bActive = false; + + if (GEngine && GEngine->GetMainAudioDevice()) + { + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (AudioDevice && RegisteredSubmix.IsValid() && SubmixBridge.IsValid()) + { + AudioDevice->UnregisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *RegisteredSubmix.Get()); + SubmixBridge.Reset(); + UE_LOG(LogGameplayRecorder, Log, TEXT("AudioRecorder: Unregistered from submix.")); + } + } + + RegisteredSubmix = nullptr; +} + +// ===================================================================== +// ISubmixBufferListener callback — runs on the AUDIO RENDER thread +// ===================================================================== + +void FAudioRecorder::OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 InNumSamples, + int32 InNumChannels, + const int32 InSampleRate, + double AudioClock) +{ + if (!bActive) + { + return; + } + + FScopeLock Lock(&BufferLock); + + // Capture format info from the first callback + SampleRate = InSampleRate; + NumChannels = InNumChannels; + + // Append raw float samples + Buffer.Append(AudioData, InNumSamples); +} + +// ===================================================================== +// SaveWav — write accumulated float audio as a 16-bit PCM WAV file +// ===================================================================== + +bool FAudioRecorder::SaveWav(const FString& FilePath) +{ + FScopeLock Lock(&BufferLock); + + if (Buffer.Num() == 0) + { + UE_LOG(LogGameplayRecorder, Warning, TEXT("AudioRecorder: No audio data to save.")); + return false; + } + + UE_LOG(LogGameplayRecorder, Log, + TEXT("AudioRecorder: Saving %d samples (%d ch, %d Hz) → %s"), + Buffer.Num(), NumChannels, SampleRate, *FilePath); + + // ── Convert float [-1, 1] to int16 ───────────────────────────── + TArray PCM16; + PCM16.SetNumUninitialized(Buffer.Num()); + for (int32 i = 0; i < Buffer.Num(); ++i) + { + float Clamped = FMath::Clamp(Buffer[i], -1.0f, 1.0f); + PCM16[i] = static_cast(Clamped * 32767.0f); + } + + // ── Build WAV file in memory ──────────────────────────────────── + // + // WAV format (44-byte header + raw PCM data): + // + // Offset Size Field + // ------ ---- ----- + // 0 4 "RIFF" + // 4 4 file size - 8 + // 8 4 "WAVE" + // 12 4 "fmt " + // 16 4 16 (sub-chunk size for PCM) + // 20 2 1 (audio format = PCM) + // 22 2 number of channels + // 24 4 sample rate + // 28 4 byte rate (SampleRate * NumChannels * BytesPerSample) + // 32 2 block align (NumChannels * BytesPerSample) + // 34 2 bits per sample (16) + // 36 4 "data" + // 40 4 data size in bytes + // 44 … raw PCM samples + // + const int32 BitsPerSample = 16; + const int32 BytesPerSample = BitsPerSample / 8; + const int32 DataSize = PCM16.Num() * BytesPerSample; + const int32 ByteRate = SampleRate * NumChannels * BytesPerSample; + const int16 BlockAlign = static_cast(NumChannels * BytesPerSample); + + TArray Wav; + Wav.Reserve(44 + DataSize); + + auto Write4CC = [&](const char* Tag) { Wav.Append(reinterpret_cast(Tag), 4); }; + auto WriteInt32 = [&](int32 V) { Wav.Append(reinterpret_cast(&V), 4); }; + auto WriteInt16 = [&](int16 V) { Wav.Append(reinterpret_cast(&V), 2); }; + + Write4CC("RIFF"); + WriteInt32(36 + DataSize); + Write4CC("WAVE"); + + Write4CC("fmt "); + WriteInt32(16); + WriteInt16(1); // PCM format + WriteInt16(static_cast(NumChannels)); + WriteInt32(SampleRate); + WriteInt32(ByteRate); + WriteInt16(BlockAlign); + WriteInt16(static_cast(BitsPerSample)); + + Write4CC("data"); + WriteInt32(DataSize); + Wav.Append(reinterpret_cast(PCM16.GetData()), DataSize); + + // ── Write to disk ─────────────────────────────────────────────── + if (FFileHelper::SaveArrayToFile(Wav, *FilePath)) + { + UE_LOG(LogGameplayRecorder, Log, + TEXT("AudioRecorder: WAV saved (%d bytes, %.1f sec)."), + Wav.Num(), + static_cast(Buffer.Num()) / (SampleRate * NumChannels)); + return true; + } + + UE_LOG(LogGameplayRecorder, Error, TEXT("AudioRecorder: Failed to write %s"), *FilePath); + return false; +} + +// ===================================================================== +// Reset / GetSampleCount +// ===================================================================== + +void FAudioRecorder::Reset() +{ + FScopeLock Lock(&BufferLock); + Buffer.Empty(); +} + +int32 FAudioRecorder::GetSampleCount() const +{ + return Buffer.Num(); +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.h new file mode 100644 index 0000000..8d7b971 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/AudioRecorder.h @@ -0,0 +1,70 @@ +// AudioRecorder.h +// ───────────────────────────────────────────────────────────────────── +// Listens to an Unreal Audio Mixer submix and accumulates raw PCM +// samples in memory. When recording stops, writes a standard WAV file. +// +// JS analogy: imagine an AudioWorkletProcessor that pushes every +// incoming audio buffer into a big array, then at the end converts +// the whole thing to a .wav Blob and downloads it. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "ISubmixBufferListener.h" +#include "Sound/SoundSubmix.h" + +// ───────────────────────────────────────────────────────────────────── +// FAudioRecorder (plain C++ — no UObject overhead) +// +// Uses a submix bridge to listen on the audio engine. +// ───────────────────────────────────────────────────────────────────── +class GAMEPLAYRECORDER_API FAudioRecorder +{ +public: + FAudioRecorder(); + virtual ~FAudioRecorder(); + + // ── Public API ────────────────────────────────────────────────── + + /** Register with the audio device and start accumulating samples. */ + void Start(USoundSubmix* OptionalSubmix = nullptr); + + /** Unregister from the audio device. */ + void Stop(); + + /** Write accumulated samples to a 16-bit PCM WAV file. Returns true on success. */ + bool SaveWav(const FString& FilePath); + + /** Discard all accumulated audio data. */ + void Reset(); + + /** Number of float samples captured so far. */ + int32 GetSampleCount() const; + + // ── Audio callback (forwarded from bridge) ───────────────────── + void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock); + +private: + bool bActive = false; + + // Format (updated from the first callback) + int32 SampleRate = 48000; + int32 NumChannels = 2; + + // Accumulated interleaved float samples + TArray Buffer; + FCriticalSection BufferLock; + + // Keep track of the submix we registered with so we can unregister later. + TWeakObjectPtr RegisteredSubmix; + + // Submix bridge (UE 5.6 requires TSharedRef) + TSharedPtr SubmixBridge; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.cpp new file mode 100644 index 0000000..119567b --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.cpp @@ -0,0 +1,266 @@ +// FFmpegPipe.cpp +// ───────────────────────────────────────────────────────────────────── +// Implementation of the FFmpeg video-encoding pipe and mux helper. +// ───────────────────────────────────────────────────────────────────── + +#include "FFmpegPipe.h" +#include "RecorderModule.h" // LogGameplayRecorder + +#include "GenericPlatform/GenericPlatformMisc.h" +#include "HAL/PlatformProcess.h" +#include "RHI.h" // GRHIAdapterName + +#include // _popen / _pclose (Windows) + +// ===================================================================== +// Construction / Destruction +// ===================================================================== + +FFFmpegPipe::FFFmpegPipe() = default; + +FFFmpegPipe::~FFFmpegPipe() +{ + // Safety net — close the pipe if the caller forgot. + Close(); +} + +// ===================================================================== +// GPU Detection +// ===================================================================== + +EHardwareEncoder FFFmpegPipe::DetectEncoder() +{ + // GRHIAdapterName is a global FString set by Unreal at RHI init. + // It contains the GPU product name, e.g.: + // "NVIDIA GeForce RTX 4090" + // "AMD Radeon RX 7900 XTX" + // "Intel(R) Arc(TM) A770" + + FString Adapter = GRHIAdapterName.ToUpper(); + + UE_LOG(LogGameplayRecorder, Log, TEXT("GPU adapter: %s"), *GRHIAdapterName); + + if (Adapter.Contains(TEXT("NVIDIA"))) + { + UE_LOG(LogGameplayRecorder, Log, TEXT(" → Selected encoder: NVENC (h264_nvenc)")); + return EHardwareEncoder::NVENC; + } + if (Adapter.Contains(TEXT("AMD")) || Adapter.Contains(TEXT("RADEON"))) + { + UE_LOG(LogGameplayRecorder, Log, TEXT(" → Selected encoder: AMF (h264_amf)")); + return EHardwareEncoder::AMF; + } + if (Adapter.Contains(TEXT("INTEL"))) + { + UE_LOG(LogGameplayRecorder, Log, TEXT(" → Selected encoder: QSV (h264_qsv)")); + return EHardwareEncoder::QSV; + } + + UE_LOG(LogGameplayRecorder, Warning, + TEXT(" → No known HW encoder for this GPU. Falling back to libx264 (CPU).")); + return EHardwareEncoder::Software; +} + +FString FFFmpegPipe::EncoderToString(EHardwareEncoder Enc) +{ + switch (Enc) + { + case EHardwareEncoder::NVENC: return TEXT("NVENC"); + case EHardwareEncoder::AMF: return TEXT("AMF"); + case EHardwareEncoder::QSV: return TEXT("QSV"); + case EHardwareEncoder::Software: return TEXT("Software (libx264)"); + } + return TEXT("Unknown"); +} + +FString FFFmpegPipe::EncoderToCodecName(EHardwareEncoder Enc) +{ + switch (Enc) + { + case EHardwareEncoder::NVENC: return TEXT("h264_nvenc"); + case EHardwareEncoder::AMF: return TEXT("h264_amf"); + case EHardwareEncoder::QSV: return TEXT("h264_qsv"); + case EHardwareEncoder::Software: return TEXT("libx264"); + } + return TEXT("libx264"); +} + +// ===================================================================== +// Build the full FFmpeg command string +// ===================================================================== + +FString FFFmpegPipe::BuildCommand() const +{ + // Resolve the executable path + FString Exe = FFmpegExe.IsEmpty() ? TEXT("ffmpeg") : FFmpegExe; + FString Codec = EncoderToCodecName(ChosenEncoder); + + // ── Encoder-specific flags ────────────────────────────────────── + // Each hardware encoder family uses slightly different option names. + FString EncoderFlags; + switch (ChosenEncoder) + { + case EHardwareEncoder::NVENC: + // NVENC presets: p1 (fastest) … p7 (best quality) + EncoderFlags = FString::Printf(TEXT("-c:v %s -preset p5 -b:v %s"), *Codec, *Bitrate); + break; + + case EHardwareEncoder::AMF: + // AMF uses -quality (speed / balanced / quality) + EncoderFlags = FString::Printf(TEXT("-c:v %s -quality balanced -b:v %s"), *Codec, *Bitrate); + break; + + case EHardwareEncoder::QSV: + // QSV uses standard -preset + EncoderFlags = FString::Printf(TEXT("-c:v %s -preset medium -b:v %s"), *Codec, *Bitrate); + break; + + case EHardwareEncoder::Software: + default: + // libx264 software fallback + EncoderFlags = FString::Printf(TEXT("-c:v %s -preset medium -b:v %s"), *Codec, *Bitrate); + break; + } + + // Full command: + // ffmpeg -y + // -f rawvideo -pix_fmt bgra -video_size WxH -framerate FPS + // -i - ← reads raw frames from stdin + // + // output.mp4 + FString Cmd = FString::Printf( + TEXT("\"%s\" -y -f rawvideo -pix_fmt bgra -video_size %dx%d -framerate %d -i - %s \"%s\""), + *Exe, + Width, Height, Framerate, + *EncoderFlags, + *VideoOutPath + ); + + return Cmd; +} + +// ===================================================================== +// Open — detect GPU, build command, launch FFmpeg child process +// ===================================================================== + +bool FFFmpegPipe::Open() +{ + if (Pipe) + { + UE_LOG(LogGameplayRecorder, Warning, TEXT("FFmpegPipe: Already open.")); + return true; + } + + ChosenEncoder = DetectEncoder(); + + FString Cmd = BuildCommand(); + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: Opening pipe...")); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Command: %s"), *Cmd); + + // _popen on Windows creates a child process and returns a FILE* + // whose write side is connected to the child's stdin. + // "wb" = write, binary mode (no newline translation). +#if PLATFORM_WINDOWS + Pipe = _popen(TCHAR_TO_ANSI(*Cmd), "wb"); +#else + Pipe = popen(TCHAR_TO_ANSI(*Cmd), "w"); +#endif + + if (!Pipe) + { + UE_LOG(LogGameplayRecorder, Error, + TEXT("FFmpegPipe: _popen failed! Is ffmpeg.exe on your system PATH?")); + return false; + } + + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: Pipe opened successfully.")); + return true; +} + +// ===================================================================== +// WriteFrame — push one BGRA frame into FFmpeg's stdin +// ===================================================================== + +bool FFFmpegPipe::WriteFrame(const void* Data, int32 SizeBytes) +{ + if (!Pipe || !Data || SizeBytes <= 0) + { + return false; + } + + // Lock so multiple threads don't interleave partial frames. + FScopeLock Lock(&WriteLock); + + size_t Written = fwrite(Data, 1, SizeBytes, Pipe); + return (Written == static_cast(SizeBytes)); +} + +// ===================================================================== +// Close — flush the pipe, wait for FFmpeg to finish encoding +// ===================================================================== + +void FFFmpegPipe::Close() +{ + if (!Pipe) + { + return; + } + + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: Closing pipe (waiting for FFmpeg)...")); + + // _pclose waits for FFmpeg to finish writing the file and returns + // the child process exit code. +#if PLATFORM_WINDOWS + int32 ExitCode = _pclose(Pipe); +#else + int32 ExitCode = pclose(Pipe); +#endif + Pipe = nullptr; + + if (ExitCode == 0) + { + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: video_only.mp4 written successfully.")); + } + else + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("FFmpegPipe: FFmpeg exited with code %d. Video file may be incomplete."), ExitCode); + } +} + +// ===================================================================== +// Mux — combine video + audio into a single MP4 +// ===================================================================== + +bool FFFmpegPipe::Mux(const FString& VideoPath, + const FString& AudioPath, + const FString& OutputPath) const +{ + FString Exe = FFmpegExe.IsEmpty() ? TEXT("ffmpeg") : FFmpegExe; + + // Arguments: -y (overwrite) -i video -i audio -c:v copy -c:a aac output + FString Args = FString::Printf( + TEXT("-y -i \"%s\" -i \"%s\" -c:v copy -c:a aac \"%s\""), + *VideoPath, *AudioPath, *OutputPath + ); + + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: Muxing...")); + UE_LOG(LogGameplayRecorder, Log, TEXT(" %s %s"), *Exe, *Args); + + int32 ReturnCode = -1; + FString StdOut, StdErr; + + // FPlatformProcess::ExecProcess runs a child process synchronously + // and captures its stdout/stderr. Blocks until done. + FPlatformProcess::ExecProcess(*Exe, *Args, &ReturnCode, &StdOut, &StdErr); + + if (ReturnCode == 0) + { + UE_LOG(LogGameplayRecorder, Log, TEXT("FFmpegPipe: Mux complete → %s"), *OutputPath); + return true; + } + + UE_LOG(LogGameplayRecorder, Error, + TEXT("FFmpegPipe: Mux failed (exit %d). stderr:\n%s"), ReturnCode, *StdErr); + return false; +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.h new file mode 100644 index 0000000..d0bf9f4 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/FFmpegPipe.h @@ -0,0 +1,85 @@ +// FFmpegPipe.h +// ───────────────────────────────────────────────────────────────────── +// Owns the FFmpeg child-process pipe for video encoding and the +// post-recording mux step. +// +// Responsibilities: +// 1. Detect the GPU vendor → pick the best hardware encoder +// 2. Open an FFmpeg process whose stdin accepts raw BGRA frames +// 3. Accept WriteFrame() calls from the render thread +// 4. Close the pipe (finishes encoding video_only.mp4) +// 5. Run a second FFmpeg pass to mux video + audio → final .mp4 +// +// Think of this like a Node.js child_process.spawn('ffmpeg', ...) +// where you pipe raw bytes into stdin. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" + +// ── Encoder enum ───────────────────────────────────────────────────── +// Each value maps to a specific FFmpeg codec name. +enum class EHardwareEncoder : uint8 +{ + NVENC, // NVIDIA → h264_nvenc + AMF, // AMD → h264_amf + QSV, // Intel → h264_qsv + Software // Fallback → libx264 +}; + +// ───────────────────────────────────────────────────────────────────── +// FFFmpegPipe (plain C++ — no UObject / no garbage collection) +// ───────────────────────────────────────────────────────────────────── +class GAMEPLAYRECORDER_API FFFmpegPipe +{ +public: + FFFmpegPipe(); + ~FFFmpegPipe(); + + // ── Configuration (set BEFORE calling Open) ───────────────────── + int32 Width = 1920; + int32 Height = 1080; + int32 Framerate = 60; + FString Bitrate = TEXT("8M"); + FString FFmpegExe; // empty → "ffmpeg" on PATH + FString VideoOutPath; // e.g. ".../Saved/Recordings/video_only.mp4" + + // ── Lifecycle ─────────────────────────────────────────────────── + /** Detect GPU, build FFmpeg command, open the pipe. Returns true on success. */ + bool Open(); + + /** Write one raw BGRA frame (Width*Height*4 bytes). Thread-safe. */ + bool WriteFrame(const void* Data, int32 SizeBytes); + + /** Flush and close the pipe. Blocks until FFmpeg finishes writing the file. */ + void Close(); + + /** Returns true while the pipe is open. */ + bool IsOpen() const { return (Pipe != nullptr); } + + // ── Mux helper ────────────────────────────────────────────────── + /** Runs: ffmpeg -y -i video -i audio -c:v copy -c:a aac output.mp4 + * Call AFTER Close(). Blocks until done. + * Returns true if FFmpeg exits with code 0. */ + bool Mux(const FString& VideoPath, + const FString& AudioPath, + const FString& OutputPath) const; + + // ── GPU detection (public so you can query it) ────────────────── + /** Reads GRHIAdapterName and returns the best encoder. */ + static EHardwareEncoder DetectEncoder(); + + /** Human-readable name of the encoder enum. */ + static FString EncoderToString(EHardwareEncoder Enc); + + /** FFmpeg codec string: "h264_nvenc", "h264_amf", etc. */ + static FString EncoderToCodecName(EHardwareEncoder Enc); + +private: + FString BuildCommand() const; + + FILE* Pipe = nullptr; + EHardwareEncoder ChosenEncoder = EHardwareEncoder::Software; + FCriticalSection WriteLock; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/GameplayRecorder.Build.cs b/Plugins/GameplayRecorder/Source/GameplayRecorder/GameplayRecorder.Build.cs new file mode 100644 index 0000000..9ff9bca --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/GameplayRecorder.Build.cs @@ -0,0 +1,29 @@ +// GameplayRecorder.Build.cs +// Module build rules for the GameplayRecorder plugin. + +using UnrealBuildTool; + +public class GameplayRecorder : ModuleRules +{ + public GameplayRecorder(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + "AudioMixer", // Submix buffer listener (audio capture) + "RHI", // FRHICommandListImmediate, ReadSurfaceData + "RenderCore", // Render-thread utilities + "Slate", // FSlateApplication (back-buffer hook) + "SlateCore" // Slate renderer types + }); + + PrivateDependencyModuleNames.AddRange(new string[] + { + "RHICore" // Additional RHI helpers + }); + } +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.cpp new file mode 100644 index 0000000..5ba4aca --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.cpp @@ -0,0 +1,113 @@ +// ParticipantManager.cpp +// ───────────────────────────────────────────────────────────────────── +// Roster management for voice-session participants. +// ───────────────────────────────────────────────────────────────────── + +#include "ParticipantManager.h" +#include "RecorderModule.h" + +// ===================================================================== +// Add / Remove +// ===================================================================== + +bool UParticipantManager::AddParticipant( + const FString& UserID, + const FString& DisplayName, + EVoiceRole Role) +{ + if (Contains(UserID)) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("ParticipantManager: '%s' already exists — skipping add."), *UserID); + return false; + } + + Participants.Emplace(UserID, DisplayName, Role); + + UE_LOG(LogGameplayRecorder, Log, + TEXT("ParticipantManager: Added '%s' (%s) as %s."), + *DisplayName, *UserID, + Role == EVoiceRole::Pilot ? TEXT("Pilot") : TEXT("Instructor")); + + return true; +} + +bool UParticipantManager::RemoveParticipant(const FString& UserID) +{ + const int32 Idx = Participants.IndexOfByPredicate( + [&](const FVoiceParticipant& P) { return P.UserID == UserID; }); + + if (Idx == INDEX_NONE) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("ParticipantManager: '%s' not found — cannot remove."), *UserID); + return false; + } + + UE_LOG(LogGameplayRecorder, Log, + TEXT("ParticipantManager: Removed '%s'."), *Participants[Idx].DisplayName); + + Participants.RemoveAt(Idx); + return true; +} + +void UParticipantManager::RemoveAll() +{ + UE_LOG(LogGameplayRecorder, Log, + TEXT("ParticipantManager: Clearing all %d participants."), Participants.Num()); + Participants.Empty(); +} + +// ===================================================================== +// Lookups +// ===================================================================== + +FVoiceParticipant* UParticipantManager::FindParticipant(const FString& UserID) +{ + return Participants.FindByPredicate( + [&](const FVoiceParticipant& P) { return P.UserID == UserID; }); +} + +const FVoiceParticipant* UParticipantManager::FindParticipant(const FString& UserID) const +{ + return Participants.FindByPredicate( + [&](const FVoiceParticipant& P) { return P.UserID == UserID; }); +} + +bool UParticipantManager::GetParticipant(const FString& UserID, FVoiceParticipant& OutParticipant) const +{ + const FVoiceParticipant* Found = FindParticipant(UserID); + if (Found) + { + OutParticipant = *Found; + return true; + } + return false; +} + +bool UParticipantManager::Contains(const FString& UserID) const +{ + return FindParticipant(UserID) != nullptr; +} + +// ===================================================================== +// Filtered queries +// ===================================================================== + +TArray UParticipantManager::GetParticipantsByRole(EVoiceRole Role) const +{ + TArray Result; + for (const FVoiceParticipant& P : Participants) + { + if (P.Role == Role) + { + Result.Add(P); + } + } + return Result; +} + +TArray UParticipantManager::GetAllParticipants() const +{ + return Participants; +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.h new file mode 100644 index 0000000..3e2a1ec --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/ParticipantManager.h @@ -0,0 +1,77 @@ +// ParticipantManager.h +// ───────────────────────────────────────────────────────────────────── +// Manages the roster of voice-session participants. +// +// Responsibilities: +// • Add / remove participants +// • Look up by UserID +// • List all participants, filter by role +// +// JS analogy: this is like a Map with helper +// methods — new Map(), map.set(id, p), map.get(id), map.delete(id). +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "VoiceTypes.h" +#include "ParticipantManager.generated.h" + +UCLASS(BlueprintType) +class GAMEPLAYRECORDER_API UParticipantManager : public UObject +{ + GENERATED_BODY() + +public: + // ── Roster management ─────────────────────────────────────────── + + /** Add a participant. Returns false if UserID already exists. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + bool AddParticipant(const FString& UserID, const FString& DisplayName, EVoiceRole Role); + + /** Remove a participant by UserID. Returns false if not found. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + bool RemoveParticipant(const FString& UserID); + + /** Remove everyone. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + void RemoveAll(); + + // ── Lookups ───────────────────────────────────────────────────── + + /** Find a participant by UserID. Returns nullptr if not found (C++ only). */ + FVoiceParticipant* FindParticipant(const FString& UserID); + const FVoiceParticipant* FindParticipant(const FString& UserID) const; + + /** Blueprint-friendly version: returns true if found and fills OutParticipant. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + bool GetParticipant(const FString& UserID, FVoiceParticipant& OutParticipant) const; + + /** True if UserID is in the roster. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Voice|Participants") + bool Contains(const FString& UserID) const; + + /** Total participant count. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Voice|Participants") + int32 GetCount() const { return Participants.Num(); } + + // ── Filtered queries ──────────────────────────────────────────── + + /** Get all participants with a given role. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + TArray GetParticipantsByRole(EVoiceRole Role) const; + + /** Get a flat copy of every participant. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Participants") + TArray GetAllParticipants() const; + + // ── Direct access (C++ only, for audio recording) ─────────────── + TArray& GetParticipantsRef() { return Participants; } + +private: + /** Internal storage — a simple TArray. For < 20 participants a + * linear search is faster than a TMap due to cache locality. */ + UPROPERTY() + TArray Participants; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.cpp new file mode 100644 index 0000000..5768991 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.cpp @@ -0,0 +1,377 @@ +// RecorderManager.cpp +// ───────────────────────────────────────────────────────────────────── +// Orchestrates the full recording pipeline WITH voice: +// 1. Open FFmpeg pipe (video) +// 2. Hook Unreal back-buffer (frames → pipe) +// 3. Start voice session + audio capture +// 4. On stop: close pipe, save WAVs (mixed + individual), mux final MP4 +// ───────────────────────────────────────────────────────────────────── + +#include "RecorderManager.h" +#include "RecorderModule.h" // LogGameplayRecorder +#include "FFmpegPipe.h" +#include "AudioRecorder.h" +#include "VoiceSessionManager.h" +#include "AudioMixerRecorder.h" + +#include "Framework/Application/SlateApplication.h" +#include "HAL/PlatformFileManager.h" +#include "Misc/Paths.h" +#include "RHI.h" +#include "RHICommandList.h" +#include "RHIResources.h" +#include "RenderingThread.h" + +// ===================================================================== +// Constructor +// ===================================================================== + +URecorderManager::URecorderManager() +{ + // VoiceSession is created lazily on first use +} + +// ===================================================================== +// BeginDestroy +// ===================================================================== + +void URecorderManager::BeginDestroy() +{ + if (bRecording) + { + StopRecording(); + } + + delete VideoPipe; + VideoPipe = nullptr; + + delete AudioCapture; + AudioCapture = nullptr; + + // VoiceSession is a UObject sub-object — GC handles it, + // but we should shut it down cleanly. + if (VoiceSession) + { + VoiceSession->ShutdownSession(); + } + + Super::BeginDestroy(); +} + +// ===================================================================== +// Resolve output directory & file paths +// ===================================================================== + +void URecorderManager::ResolveOutputPaths() +{ + FString Dir = OutputDirectory; + if (Dir.IsEmpty()) + { + Dir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Recordings")); + } + if (FPaths::IsRelative(Dir)) + { + Dir = FPaths::ConvertRelativePathToFull(Dir); + } + + IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile(); + if (!PF.DirectoryExists(*Dir)) + { + PF.CreateDirectoryTree(*Dir); + } + + OutputDir = Dir; + VideoPath = FPaths::Combine(Dir, TEXT("video_only.mp4")); + AudioPath = FPaths::Combine(Dir, TEXT("audio_only.wav")); + FinalPath = FPaths::Combine(Dir, TEXT("final_recording.mp4")); +} + +// ===================================================================== +// Voice / Participant wrappers +// ===================================================================== + +bool URecorderManager::AddParticipant( + const FString& UserID, const FString& DisplayName, EVoiceRole Role) +{ + // Lazily create the voice session manager + if (!VoiceSession) + { + VoiceSession = NewObject(this); + } + // Auto-initialize the session if the user forgot + VoiceSession->InitializeSession(); + return VoiceSession->AddParticipant(UserID, DisplayName, Role); +} + +bool URecorderManager::RemoveParticipant(const FString& UserID) +{ + if (!VoiceSession) return false; + return VoiceSession->RemoveParticipant(UserID); +} + +TArray URecorderManager::GetAllParticipants() const +{ + if (!VoiceSession) return {}; + return VoiceSession->GetAllParticipants(); +} + +EVoicePermissionResult URecorderManager::MuteParticipant( + const FString& RequestorUserID, const FString& TargetUserID) +{ + if (!VoiceSession) return EVoicePermissionResult::Denied; + return VoiceSession->MuteParticipant(RequestorUserID, TargetUserID); +} + +EVoicePermissionResult URecorderManager::UnmuteParticipant( + const FString& RequestorUserID, const FString& TargetUserID) +{ + if (!VoiceSession) return EVoicePermissionResult::Denied; + return VoiceSession->UnmuteParticipant(RequestorUserID, TargetUserID); +} + +EVoicePermissionResult URecorderManager::ToggleMuteParticipant( + const FString& RequestorUserID, const FString& TargetUserID) +{ + if (!VoiceSession) return EVoicePermissionResult::Denied; + return VoiceSession->ToggleMuteParticipant(RequestorUserID, TargetUserID); +} + +// ===================================================================== +// StartRecording +// ===================================================================== + +void URecorderManager::StartRecording() +{ + if (bRecording) + { + UE_LOG(LogGameplayRecorder, Warning, TEXT("RecorderManager: Already recording.")); + return; + } + + ResolveOutputPaths(); + + UE_LOG(LogGameplayRecorder, Log, TEXT("═══════ TRAINING RECORDING START ═══════")); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Resolution : %d x %d @ %d fps"), Width, Height, Framerate); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Bitrate : %s"), *Bitrate); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Video : %s"), *VideoPath); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Audio : %s"), *AudioPath); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Final : %s"), *FinalPath); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Individual : %s"), + bSaveIndividualVoiceTracks ? TEXT("YES") : TEXT("NO")); + + // ─── 1. Video: FFmpeg pipe ───────────────────────────────────── + delete VideoPipe; + VideoPipe = new FFFmpegPipe(); + VideoPipe->Width = Width; + VideoPipe->Height = Height; + VideoPipe->Framerate = Framerate; + VideoPipe->Bitrate = Bitrate; + VideoPipe->FFmpegExe = FFmpegPath; + VideoPipe->VideoOutPath = VideoPath; + + if (!VideoPipe->Open()) + { + UE_LOG(LogGameplayRecorder, Error, + TEXT("RecorderManager: FFmpeg pipe failed — aborting.")); + delete VideoPipe; + VideoPipe = nullptr; + return; + } + + // ─── 2. Back-buffer hook ─────────────────────────────────────── + if (FSlateApplication::IsInitialized()) + { + BackBufferHandle = + FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent() + .AddUObject(this, &URecorderManager::OnBackBufferReady); + } + + // ─── 3. Voice session + audio capture ────────────────────────── + if (VoiceSession) + { + VoiceSession->InitializeSession(); // safe to call twice + VoiceSession->StartVoiceCapture(AudioSubmix); + + // Log participants + TArray All = VoiceSession->GetAllParticipants(); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Participants: %d"), All.Num()); + for (const FVoiceParticipant& P : All) + { + UE_LOG(LogGameplayRecorder, Log, TEXT(" [%s] %s (%s)%s"), + *P.UserID, *P.DisplayName, + P.Role == EVoiceRole::Pilot ? TEXT("Pilot") : TEXT("Instructor"), + P.bMuted ? TEXT(" MUTED") : TEXT("")); + } + } + + // ─── 4. Also start the legacy game-audio recorder as a fallback ─ + // (captures game-world sounds even if VoiceSession has no participants) + delete AudioCapture; + AudioCapture = new FAudioRecorder(); + AudioCapture->Start(AudioSubmix); + + bRecording = true; + UE_LOG(LogGameplayRecorder, Log, TEXT("RecorderManager: Recording is LIVE.")); +} + +// ===================================================================== +// StopRecording +// ===================================================================== + +void URecorderManager::StopRecording() +{ + if (!bRecording) + { + UE_LOG(LogGameplayRecorder, Warning, TEXT("RecorderManager: Not recording.")); + return; + } + + bRecording = false; + UE_LOG(LogGameplayRecorder, Log, TEXT("═══════ TRAINING RECORDING STOP ═══════")); + + // ─── 1. Unhook back-buffer ──────────────────────────────────── + if (FSlateApplication::IsInitialized() && BackBufferHandle.IsValid()) + { + FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent() + .Remove(BackBufferHandle); + BackBufferHandle.Reset(); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Back-buffer hook removed.")); + } + + // ─── 2. Close video pipe ────────────────────────────────────── + if (VideoPipe) + { + VideoPipe->Close(); + UE_LOG(LogGameplayRecorder, Log, TEXT(" Video pipe closed.")); + } + + // ─── 3. Stop voice capture + save voice audio ───────────────── + bool bVoiceAudioSaved = false; + if (VoiceSession) + { + VoiceSession->StopVoiceCapture(); + + FAudioMixerRecorder* MixRec = VoiceSession->GetAudioMixerRecorder(); + if (MixRec) + { + // Save the combined (mixed) voice + game audio + bVoiceAudioSaved = MixRec->SaveMixedWav(AudioPath); + + // Optionally save individual participant tracks + if (bSaveIndividualVoiceTracks) + { + int32 Count = MixRec->SaveIndividualWavs(OutputDir); + UE_LOG(LogGameplayRecorder, Log, + TEXT(" Saved %d individual voice track(s)."), Count); + } + } + } + + // ─── 4. Fallback: if voice recorder had no data, use game-audio ─ + if (!bVoiceAudioSaved && AudioCapture) + { + AudioCapture->Stop(); + AudioCapture->SaveWav(AudioPath); + UE_LOG(LogGameplayRecorder, Log, + TEXT(" Game audio saved (no voice data available).")); + } + else if (AudioCapture) + { + AudioCapture->Stop(); + } + + // ─── 5. Mux video + audio → final_recording.mp4 ────────────── + if (VideoPipe) + { + VideoPipe->Mux(VideoPath, AudioPath, FinalPath); + } + + // ─── 6. Clean up ───────────────────────────────────────────── + delete VideoPipe; + VideoPipe = nullptr; + + delete AudioCapture; + AudioCapture = nullptr; + + UE_LOG(LogGameplayRecorder, Log, TEXT("RecorderManager: All files saved. Done.")); +} + +// ===================================================================== +// OnBackBufferReady — RENDER THREAD callback +// ===================================================================== +// +// This function runs on Unreal's render thread — NOT the game thread. +// It is called once per presented frame. +// +// Pipeline: +// GPU back-buffer texture +// → ReadSurfaceData (GPU → CPU copy, returns TArray) +// → fwrite into FFmpeg stdin pipe +// → FFmpeg encodes the frame with the chosen HW encoder +// +// Performance notes: +// • ReadSurfaceData stalls the GPU pipeline (sync readback). +// At 1080p60 this is ~8 MB/frame × 60 = ~480 MB/s of PCI-e traffic. +// Hardware encoding (NVENC/AMF/QSV) adds <2% GPU load. +// • For higher performance, you could use async readback via +// RHIAsyncReadback — but that adds significant complexity. +// This synchronous approach is simpler and fine for most use-cases. + +void URecorderManager::OnBackBufferReady( + SWindow& Window, + const FTextureRHIRef& BackBuffer) +{ + if (!bRecording || !VideoPipe || !VideoPipe->IsOpen()) + { + return; + } + + FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); + + // The back-buffer might be larger or smaller than our target resolution + // (e.g. if the window is resized). Clamp the read region. + const FIntPoint BBSize = BackBuffer->GetSizeXY(); + const int32 ReadW = FMath::Min(Width, static_cast(BBSize.X)); + const int32 ReadH = FMath::Min(Height, static_cast(BBSize.Y)); + const FIntRect ReadRect(0, 0, ReadW, ReadH); + + // ReadSurfaceData: copies pixels from the GPU texture to a CPU array. + // Each pixel is an FColor (BGRA, 4 bytes). + // This is a BLOCKING call — the CPU waits for the GPU to finish. + TArray Pixels; + Pixels.SetNumUninitialized(ReadW * ReadH); + + RHICmdList.ReadSurfaceData( + BackBuffer.GetReference(), + ReadRect, + Pixels, + FReadSurfaceDataFlags(RCM_UNorm) + ); + + // Expected frame size for FFmpeg + const int32 ExpectedPixels = Width * Height; + const int32 FrameBytes = ExpectedPixels * sizeof(FColor); + + if (ReadW == Width && ReadH == Height) + { + // Fast path — dimensions match exactly + VideoPipe->WriteFrame(Pixels.GetData(), FrameBytes); + } + else + { + // Slow path — back-buffer is smaller than target. + // Pad with black so FFmpeg always gets a full-size frame. + TArray Padded; + Padded.SetNumZeroed(ExpectedPixels); + + for (int32 Row = 0; Row < ReadH; ++Row) + { + FMemory::Memcpy( + &Padded[Row * Width], + &Pixels[Row * ReadW], + ReadW * sizeof(FColor)); + } + + VideoPipe->WriteFrame(Padded.GetData(), FrameBytes); + } +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.h new file mode 100644 index 0000000..a25acc7 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderManager.h @@ -0,0 +1,161 @@ +// RecorderManager.h +// ───────────────────────────────────────────────────────────────────── +// Top-level orchestrator for gameplay recording WITH voice support. +// +// This is a UObject-based class so Unreal's garbage collector manages +// its lifetime and Blueprint can call its functions. +// +// JS analogy: think of this as the "RecordingService" singleton that +// owns a VideoEncoder (FFmpegPipe), a VoiceSessionManager (voice chat +// + per-participant audio), and an AudioRecorder (game-world audio). +// It exposes start/stop/mute methods to Blueprint. +// +// Outputs (in /Saved/Recordings/ by default): +// video_only.mp4 — HW-encoded H.264 +// audio_only.wav — mixed voice + game audio +// final_recording.mp4 — muxed video + audio +// _audio.wav (optional) — individual voice tracks +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "VoiceTypes.h" +#include "RecorderManager.generated.h" + +// Forward declarations (avoid including heavy headers in the .h) +class FFFmpegPipe; +class FAudioRecorder; +class USoundSubmix; +class UVoiceSessionManager; + +// ───────────────────────────────────────────────────────────────────── +// URecorderManager +// ───────────────────────────────────────────────────────────────────── +UCLASS(BlueprintType) +class GAMEPLAYRECORDER_API URecorderManager : public UObject +{ + GENERATED_BODY() + +public: + URecorderManager(); + + // ═════════════════════════════════════════════════════════════════ + // RECORDING API + // ═════════════════════════════════════════════════════════════════ + + /** Begins capturing video frames, game audio, AND voice. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder") + void StartRecording(); + + /** Stops everything, saves WAVs, muxes final_recording.mp4. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder") + void StopRecording(); + + /** True while a recording session is active. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GameplayRecorder") + bool IsRecording() const { return bRecording; } + + // ═════════════════════════════════════════════════════════════════ + // VOICE / PARTICIPANT API (delegates to VoiceSessionManager) + // ═════════════════════════════════════════════════════════════════ + + /** Add a participant to the voice session (call BEFORE StartRecording). */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + bool AddParticipant(const FString& UserID, const FString& DisplayName, EVoiceRole Role); + + /** Remove a participant. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + bool RemoveParticipant(const FString& UserID); + + /** Get the full roster. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + TArray GetAllParticipants() const; + + /** + * Mute TargetUserID on behalf of RequestorUserID. + * Enforces role permissions (Pilot can only self-mute, Instructor can mute anyone). + */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + EVoicePermissionResult MuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + /** Unmute TargetUserID on behalf of RequestorUserID. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + EVoicePermissionResult UnmuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + /** Toggle mute state. */ + UFUNCTION(BlueprintCallable, Category = "GameplayRecorder|Voice") + EVoicePermissionResult ToggleMuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + // ═════════════════════════════════════════════════════════════════ + // SETTINGS (set BEFORE calling StartRecording) + // ═════════════════════════════════════════════════════════════════ + + /** Capture width in pixels. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + int32 Width = 1920; + + /** Capture height in pixels. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + int32 Height = 1080; + + /** Target frames per second. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + int32 Framerate = 60; + + /** Video bitrate (FFmpeg string, e.g. "8M", "12M", "20M"). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + FString Bitrate = TEXT("8M"); + + /** Output directory. Empty = /Saved/Recordings. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + FString OutputDirectory; + + /** Full path to ffmpeg.exe. Empty = use the system PATH. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + FString FFmpegPath; + + /** Optional: specific submix to record. Null = master (everything). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + USoundSubmix* AudioSubmix = nullptr; + + /** If true, save separate WAV files per participant (for post-analysis). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayRecorder|Settings") + bool bSaveIndividualVoiceTracks = true; + +protected: + /** Safety net: if the object is garbage-collected while recording, stop. */ + virtual void BeginDestroy() override; + +private: + // ── Internal helpers ──────────────────────────────────────────── + void ResolveOutputPaths(); + + /** Render-thread callback from Unreal's Slate renderer. */ + void OnBackBufferReady(SWindow& Window, const FTextureRHIRef& BackBuffer); + + // ── State ─────────────────────────────────────────────────────── + bool bRecording = false; + FDelegateHandle BackBufferHandle; + + // Sub-systems + FFFmpegPipe* VideoPipe = nullptr; // raw ptr — we manage lifetime + FAudioRecorder* AudioCapture = nullptr; // game-world submix fallback + + /** The voice session. UObject — GC-managed (created as sub-object). */ + UPROPERTY() + UVoiceSessionManager* VoiceSession = nullptr; + + // Resolved file paths for this session + FString OutputDir; + FString VideoPath; + FString AudioPath; + FString FinalPath; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.cpp new file mode 100644 index 0000000..61e2751 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.cpp @@ -0,0 +1,20 @@ +// RecorderModule.cpp + +#include "RecorderModule.h" + +// Define the log so every .cpp in this plugin can use UE_LOG(LogGameplayRecorder, ...) +DEFINE_LOG_CATEGORY(LogGameplayRecorder); + +void FGameplayRecorderModule::StartupModule() +{ + UE_LOG(LogGameplayRecorder, Log, TEXT("GameplayRecorder plugin loaded.")); +} + +void FGameplayRecorderModule::ShutdownModule() +{ + UE_LOG(LogGameplayRecorder, Log, TEXT("GameplayRecorder plugin unloaded.")); +} + +// Tell Unreal "this module exists and here is the class that manages it" +// This macro connects the module name string to the C++ class. +IMPLEMENT_MODULE(FGameplayRecorderModule, GameplayRecorder) diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.h new file mode 100644 index 0000000..c19821e --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/RecorderModule.h @@ -0,0 +1,22 @@ +// RecorderModule.h +// IModuleInterface implementation for the GameplayRecorder plugin. +// This is the "entry point" Unreal uses to load the plugin — think of it +// like the main() of a Node.js package, but for an Unreal module. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +// Log category shared by every file in this plugin +DECLARE_LOG_CATEGORY_EXTERN(LogGameplayRecorder, Log, All); + +class FGameplayRecorderModule : public IModuleInterface +{ +public: + /** Called when the engine loads this plugin. */ + virtual void StartupModule() override; + + /** Called when the engine unloads this plugin. */ + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.cpp new file mode 100644 index 0000000..fa598ac --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.cpp @@ -0,0 +1,106 @@ +// VoicePermissionSystem.cpp +// ───────────────────────────────────────────────────────────────────── +// Implementation of role-based voice permission checks. +// ───────────────────────────────────────────────────────────────────── + +#include "VoicePermissionSystem.h" +#include "RecorderModule.h" + +// ===================================================================== +// CanMute +// ===================================================================== + +EVoicePermissionResult UVoicePermissionSystem::CanMute( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target) +{ + // ── Rule 1: Instructors can mute anyone ───────────────────────── + if (Requestor.Role == EVoiceRole::Instructor) + { + return EVoicePermissionResult::Allowed; + } + + // ── Rule 2: Pilots can only mute themselves ───────────────────── + if (Requestor.Role == EVoiceRole::Pilot) + { + if (Requestor.UserID == Target.UserID) + { + return EVoicePermissionResult::Allowed; + } + + UE_LOG(LogGameplayRecorder, Warning, + TEXT("Permission DENIED: Pilot '%s' tried to mute '%s' — pilots can only mute themselves."), + *Requestor.DisplayName, *Target.DisplayName); + + return EVoicePermissionResult::Denied; + } + + // Fallback: deny unknown roles + return EVoicePermissionResult::Denied; +} + +// ===================================================================== +// CanUnmute — same rules as CanMute (symmetric) +// ===================================================================== + +EVoicePermissionResult UVoicePermissionSystem::CanUnmute( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target) +{ + // Instructors can unmute anyone + if (Requestor.Role == EVoiceRole::Instructor) + { + return EVoicePermissionResult::Allowed; + } + + // Pilots can only unmute themselves + if (Requestor.Role == EVoiceRole::Pilot) + { + if (Requestor.UserID == Target.UserID) + { + return EVoicePermissionResult::Allowed; + } + + UE_LOG(LogGameplayRecorder, Warning, + TEXT("Permission DENIED: Pilot '%s' tried to unmute '%s' — pilots can only unmute themselves."), + *Requestor.DisplayName, *Target.DisplayName); + + return EVoicePermissionResult::Denied; + } + + return EVoicePermissionResult::Denied; +} + +// ===================================================================== +// GetDenialReason — human-readable explanation for UI / logs +// ===================================================================== + +FString UVoicePermissionSystem::GetDenialReason( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target, + bool bIsMuteAction) +{ + const FString Action = bIsMuteAction ? TEXT("mute") : TEXT("unmute"); + + // Check the appropriate permission + EVoicePermissionResult Result = bIsMuteAction + ? CanMute(Requestor, Target) + : CanUnmute(Requestor, Target); + + if (Result == EVoicePermissionResult::Allowed) + { + return FString(); // empty = allowed, no denial reason + } + + // Build a human-readable reason + if (Requestor.Role == EVoiceRole::Pilot) + { + return FString::Printf( + TEXT("Pilots can only %s themselves. You cannot %s %s."), + *Action, *Action, *Target.DisplayName); + } + + return FString::Printf( + TEXT("You do not have permission to %s %s."), + *Action, *Target.DisplayName); +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.h new file mode 100644 index 0000000..eccbd35 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoicePermissionSystem.h @@ -0,0 +1,60 @@ +// VoicePermissionSystem.h +// ───────────────────────────────────────────────────────────────────── +// Stateless permission checker for voice operations. +// +// This class encapsulates the business rules: +// • Pilots can only mute/unmute THEMSELVES +// • Instructors can mute/unmute ANYONE +// +// JS analogy: think of this like a pure-function middleware: +// function canMute(requestor, target) { ... return true/false; } +// We put it in a UObject so Blueprint can call it too. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "VoiceTypes.h" +#include "VoicePermissionSystem.generated.h" + +UCLASS(BlueprintType) +class GAMEPLAYRECORDER_API UVoicePermissionSystem : public UObject +{ + GENERATED_BODY() + +public: + // ── Permission checks (static — no instance state needed) ─────── + + /** + * Can the requestor mute the target participant? + * + * Rules: + * Pilot → only if TargetID == RequestorID (muting yourself) + * Instructor → always allowed + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Permissions") + static EVoicePermissionResult CanMute( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target); + + /** + * Can the requestor unmute the target participant? + * + * Same rules as CanMute — symmetric for simplicity. + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Permissions") + static EVoicePermissionResult CanUnmute( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target); + + /** + * Human-readable reason string (useful for UI toasts / logs). + * Returns "" if allowed, or an explanation like "Pilots can only mute themselves." + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Permissions") + static FString GetDenialReason( + const FVoiceParticipant& Requestor, + const FVoiceParticipant& Target, + bool bIsMuteAction); +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.cpp b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.cpp new file mode 100644 index 0000000..9ae72ff --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.cpp @@ -0,0 +1,355 @@ +// VoiceSessionManager.cpp +// ───────────────────────────────────────────────────────────────────── +// Orchestrates voice sessions: participants, permissions, capture, +// and feeding audio to the mixer recorder. +// ───────────────────────────────────────────────────────────────────── + +#include "VoiceSessionManager.h" +#include "RecorderModule.h" +#include "ParticipantManager.h" +#include "VoicePermissionSystem.h" +#include "AudioMixerRecorder.h" + +#include "AudioDevice.h" +#include "AudioMixerDevice.h" +#include "Engine/Engine.h" + +// ===================================================================== +// FSubmixBridge — wraps callback in TSharedRef for UE 5.6 API +// ===================================================================== +class FVoiceSessionSubmixBridge + : public ISubmixBufferListener +{ +public: + TWeakObjectPtr Owner; + + FVoiceSessionSubmixBridge(UVoiceSessionManager* InOwner) : Owner(InOwner) {} + + virtual void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock) override + { + if (UVoiceSessionManager* Mgr = Owner.Get()) + { + Mgr->OnNewSubmixBuffer(OwningSubmix, AudioData, NumSamples, NumChannels, SampleRate, AudioClock); + } + } +}; + +// ===================================================================== +// Constructor +// ===================================================================== + +UVoiceSessionManager::UVoiceSessionManager() +{ +} + +// ===================================================================== +// BeginDestroy — safety net +// ===================================================================== + +void UVoiceSessionManager::BeginDestroy() +{ + ShutdownSession(); + Super::BeginDestroy(); +} + +// ===================================================================== +// InitializeSession / ShutdownSession +// ===================================================================== + +void UVoiceSessionManager::InitializeSession() +{ + if (bSessionActive) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("VoiceSessionManager: Session already active.")); + return; + } + + // Create the participant manager as a sub-object of this UObject. + // NewObject keeps it under GC control. + ParticipantMgr = NewObject(this); + + // Create the plain-C++ audio mixer recorder + MixerRecorder = new FAudioMixerRecorder(); + + bSessionActive = true; + UE_LOG(LogGameplayRecorder, Log, TEXT("VoiceSessionManager: Session initialized.")); +} + +void UVoiceSessionManager::ShutdownSession() +{ + if (!bSessionActive) + { + return; + } + + StopVoiceCapture(); + + delete MixerRecorder; + MixerRecorder = nullptr; + + if (ParticipantMgr) + { + ParticipantMgr->RemoveAll(); + } + ParticipantMgr = nullptr; + + bSessionActive = false; + UE_LOG(LogGameplayRecorder, Log, TEXT("VoiceSessionManager: Session shut down.")); +} + +// ===================================================================== +// Participant management — thin wrappers around ParticipantManager +// ===================================================================== + +bool UVoiceSessionManager::AddParticipant( + const FString& UserID, + const FString& DisplayName, + EVoiceRole Role) +{ + if (!ParticipantMgr) + { + UE_LOG(LogGameplayRecorder, Error, + TEXT("VoiceSessionManager: Not initialized. Call InitializeSession() first.")); + return false; + } + return ParticipantMgr->AddParticipant(UserID, DisplayName, Role); +} + +bool UVoiceSessionManager::RemoveParticipant(const FString& UserID) +{ + if (!ParticipantMgr) return false; + return ParticipantMgr->RemoveParticipant(UserID); +} + +TArray UVoiceSessionManager::GetAllParticipants() const +{ + if (!ParticipantMgr) return {}; + return ParticipantMgr->GetAllParticipants(); +} + +// ===================================================================== +// Mute / Unmute / Toggle — with permission checks +// ===================================================================== + +EVoicePermissionResult UVoiceSessionManager::MuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID) +{ + if (!ParticipantMgr) + { + return EVoicePermissionResult::Denied; + } + + FVoiceParticipant* Requestor = ParticipantMgr->FindParticipant(RequestorUserID); + FVoiceParticipant* Target = ParticipantMgr->FindParticipant(TargetUserID); + + if (!Requestor || !Target) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("VoiceSessionManager: MuteParticipant — user not found. Requestor='%s' Target='%s'"), + *RequestorUserID, *TargetUserID); + return EVoicePermissionResult::Denied; + } + + // ── Permission check ──────────────────────────────────────────── + EVoicePermissionResult Result = UVoicePermissionSystem::CanMute(*Requestor, *Target); + if (Result == EVoicePermissionResult::Denied) + { + FString Reason = UVoicePermissionSystem::GetDenialReason(*Requestor, *Target, true); + UE_LOG(LogGameplayRecorder, Warning, TEXT(" MUTE DENIED: %s"), *Reason); + return EVoicePermissionResult::Denied; + } + + // ── Apply the mute ────────────────────────────────────────────── + Target->bMuted = true; + + UE_LOG(LogGameplayRecorder, Log, + TEXT("VoiceSessionManager: '%s' muted '%s'."), + *Requestor->DisplayName, *Target->DisplayName); + + // Fire the event so UI can update + OnMuteStateChanged.Broadcast(TargetUserID, true, RequestorUserID); + + return EVoicePermissionResult::Allowed; +} + +EVoicePermissionResult UVoiceSessionManager::UnmuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID) +{ + if (!ParticipantMgr) + { + return EVoicePermissionResult::Denied; + } + + FVoiceParticipant* Requestor = ParticipantMgr->FindParticipant(RequestorUserID); + FVoiceParticipant* Target = ParticipantMgr->FindParticipant(TargetUserID); + + if (!Requestor || !Target) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("VoiceSessionManager: UnmuteParticipant — user not found.")); + return EVoicePermissionResult::Denied; + } + + EVoicePermissionResult Result = UVoicePermissionSystem::CanUnmute(*Requestor, *Target); + if (Result == EVoicePermissionResult::Denied) + { + FString Reason = UVoicePermissionSystem::GetDenialReason(*Requestor, *Target, false); + UE_LOG(LogGameplayRecorder, Warning, TEXT(" UNMUTE DENIED: %s"), *Reason); + return EVoicePermissionResult::Denied; + } + + Target->bMuted = false; + + UE_LOG(LogGameplayRecorder, Log, + TEXT("VoiceSessionManager: '%s' unmuted '%s'."), + *Requestor->DisplayName, *Target->DisplayName); + + OnMuteStateChanged.Broadcast(TargetUserID, false, RequestorUserID); + + return EVoicePermissionResult::Allowed; +} + +EVoicePermissionResult UVoiceSessionManager::ToggleMuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID) +{ + if (!ParticipantMgr) + { + return EVoicePermissionResult::Denied; + } + + const FVoiceParticipant* Target = ParticipantMgr->FindParticipant(TargetUserID); + if (!Target) + { + return EVoicePermissionResult::Denied; + } + + if (Target->bMuted) + { + return UnmuteParticipant(RequestorUserID, TargetUserID); + } + else + { + return MuteParticipant(RequestorUserID, TargetUserID); + } +} + +// ===================================================================== +// Voice Capture — submix listener +// ===================================================================== + +void UVoiceSessionManager::StartVoiceCapture(USoundSubmix* OptionalSubmix) +{ + if (bCapturingVoice) + { + UE_LOG(LogGameplayRecorder, Warning, + TEXT("VoiceSessionManager: Voice capture already active.")); + return; + } + + if (!MixerRecorder) + { + UE_LOG(LogGameplayRecorder, Error, + TEXT("VoiceSessionManager: Not initialized.")); + return; + } + + MixerRecorder->Start(); + + // Register as a submix listener to capture the combined audio output + if (GEngine && GEngine->GetMainAudioDevice()) + { + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (AudioDevice) + { + USoundSubmix* Submix = OptionalSubmix; + if (!Submix) + { + Submix = &AudioDevice->GetMainSubmixObject(); + } + if (Submix) + { + RegisteredSubmix = Submix; + SubmixBridge = MakeShared(this); + AudioDevice->RegisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *Submix); + bCapturingVoice = true; + + UE_LOG(LogGameplayRecorder, Log, + TEXT("VoiceSessionManager: Voice capture started (submix: %s)."), + *Submix->GetName()); + } + } + } +} + +void UVoiceSessionManager::StopVoiceCapture() +{ + if (!bCapturingVoice) + { + return; + } + + bCapturingVoice = false; + + // Unregister from the audio device + if (GEngine && GEngine->GetMainAudioDevice()) + { + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (AudioDevice && RegisteredSubmix.IsValid() && SubmixBridge.IsValid()) + { + AudioDevice->UnregisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *RegisteredSubmix.Get()); + SubmixBridge.Reset(); + } + } + RegisteredSubmix = nullptr; + + if (MixerRecorder) + { + MixerRecorder->Stop(); + } + + UE_LOG(LogGameplayRecorder, Log, TEXT("VoiceSessionManager: Voice capture stopped.")); +} + +// ===================================================================== +// ISubmixBufferListener — audio callback (audio render thread) +// ===================================================================== +// +// This captures the MIXED game audio output (all sounds + voice chat +// that has been routed through Unreal's audio engine). +// +// For per-participant voice capture in a networked scenario, each +// client would also feed their local mic data to +// MixerRecorder->FeedParticipantAudio() separately. In a local/LAN +// training setup you can use Unreal's VoiceCapture API or VOIP +// integration to route individual mic streams here. + +void UVoiceSessionManager::OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock) +{ + if (!bCapturingVoice || !MixerRecorder) + { + return; + } + + // Update recorder format + MixerRecorder->SampleRate = SampleRate; + MixerRecorder->NumChannels = NumChannels; + + // Feed the combined game audio into the mixed buffer + MixerRecorder->FeedGameAudio(AudioData, NumSamples); +} diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.h new file mode 100644 index 0000000..6cd75f7 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceSessionManager.h @@ -0,0 +1,151 @@ +// VoiceSessionManager.h +// ───────────────────────────────────────────────────────────────────── +// Top-level voice-session orchestrator. +// +// Responsibilities: +// • Owns the ParticipantManager (roster) +// • Enforces permissions via VoicePermissionSystem +// • Manages per-participant voice capture +// • Feeds captured audio into an AudioMixerRecorder +// +// JS analogy: this is like a "VoiceChatService" class that wraps +// WebRTC connections, user lists, and mute logic into one API — +// but running locally inside the Unreal process. +// +// Blueprint-callable so instructors can wire up UI buttons to +// mute/unmute participants. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "VoiceTypes.h" +#include "ISubmixBufferListener.h" +#include "Sound/SoundSubmix.h" +#include "VoiceSessionManager.generated.h" + +// Forward declarations +class UParticipantManager; +class UVoicePermissionSystem; +class FAudioMixerRecorder; + +// ── Delegate fired when a participant's mute state changes ────────── +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( + FOnMuteStateChanged, + const FString&, UserID, + bool, bNewMutedState, + const FString&, ChangedByUserID); + +// ───────────────────────────────────────────────────────────────────── +// UVoiceSessionManager +// ───────────────────────────────────────────────────────────────────── +UCLASS(BlueprintType) +class GAMEPLAYRECORDER_API UVoiceSessionManager : public UObject +{ + GENERATED_BODY() + +public: + UVoiceSessionManager(); + + // ── Session lifecycle ─────────────────────────────────────────── + + /** Call once to set up the session. Creates the participant manager. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + void InitializeSession(); + + /** Tear down the session. Stops voice capture if active. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + void ShutdownSession(); + + // ── Participant management (delegate to ParticipantManager) ───── + + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + bool AddParticipant(const FString& UserID, const FString& DisplayName, EVoiceRole Role); + + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + bool RemoveParticipant(const FString& UserID); + + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + TArray GetAllParticipants() const; + + // ── Mute / Unmute with permission checks ──────────────────────── + + /** + * Attempt to mute TargetUserID on behalf of RequestorUserID. + * Checks permissions first. Returns the permission result. + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + EVoicePermissionResult MuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + /** + * Attempt to unmute TargetUserID on behalf of RequestorUserID. + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + EVoicePermissionResult UnmuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + /** Convenience: toggle mute. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + EVoicePermissionResult ToggleMuteParticipant( + const FString& RequestorUserID, + const FString& TargetUserID); + + // ── Voice capture control ─────────────────────────────────────── + + /** + * Start capturing voice from the submix (microphones + game audio). + * Feed data to the AudioMixerRecorder for recording. + */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + void StartVoiceCapture(USoundSubmix* OptionalSubmix = nullptr); + + /** Stop voice capture. */ + UFUNCTION(BlueprintCallable, Category = "Voice|Session") + void StopVoiceCapture(); + + // ── Access to the internal audio mixer recorder ───────────────── + // (C++ only — RecorderManager uses this to save WAVs) + + FAudioMixerRecorder* GetAudioMixerRecorder() const { return MixerRecorder; } + + // ── Events ────────────────────────────────────────────────────── + + /** Broadcast when any participant's mute state changes. */ + UPROPERTY(BlueprintAssignable, Category = "Voice|Session") + FOnMuteStateChanged OnMuteStateChanged; + + // ── Audio capture callback (forwarded from bridge) ─────────────── + void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock); + + // ── Direct access to managers (C++) ───────────────────────────── + UParticipantManager* GetParticipantManager() const { return ParticipantMgr; } + +protected: + virtual void BeginDestroy() override; + +private: + // ── Owned sub-objects ─────────────────────────────────────────── + UPROPERTY() + UParticipantManager* ParticipantMgr = nullptr; + + // Plain C++ (not UObject — we manage lifetime manually) + FAudioMixerRecorder* MixerRecorder = nullptr; + + bool bSessionActive = false; + bool bCapturingVoice = false; + + TWeakObjectPtr RegisteredSubmix; + + // Submix bridge (UE 5.6 requires TSharedRef) + TSharedPtr SubmixBridge; +}; diff --git a/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceTypes.h b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceTypes.h new file mode 100644 index 0000000..013e8b5 --- /dev/null +++ b/Plugins/GameplayRecorder/Source/GameplayRecorder/VoiceTypes.h @@ -0,0 +1,73 @@ +// VoiceTypes.h +// ───────────────────────────────────────────────────────────────────── +// Shared enums and structs used across all voice-system classes. +// +// JS analogy: This is like a shared "types.ts" file that defines your +// TypeScript interfaces and enums used across the whole feature. +// ───────────────────────────────────────────────────────────────────── + +#pragma once + +#include "CoreMinimal.h" +#include "VoiceTypes.generated.h" + +// ───────────────────────────────────────────────────────────────────── +// EVoiceRole — the role a participant plays in a training session +// ───────────────────────────────────────────────────────────────────── +UENUM(BlueprintType) +enum class EVoiceRole : uint8 +{ + /** The player flying / driving / playing the game. One per session. */ + Pilot UMETA(DisplayName = "Pilot"), + + /** An observer who can give instructions and moderate voice. */ + Instructor UMETA(DisplayName = "Instructor") +}; + +// ───────────────────────────────────────────────────────────────────── +// FVoiceParticipant — one person in the voice session +// +// JS analogy: like a plain object { userId, displayName, role, muted } +// but as a C++ struct with Unreal reflection so Blueprint can see it. +// ───────────────────────────────────────────────────────────────────── +USTRUCT(BlueprintType) +struct GAMEPLAYRECORDER_API FVoiceParticipant +{ + GENERATED_BODY() + + /** Unique identifier (e.g. "Player_0", "Instructor_1"). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voice") + FString UserID; + + /** Human-readable name shown in UI. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voice") + FString DisplayName; + + /** Pilot or Instructor. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voice") + EVoiceRole Role = EVoiceRole::Pilot; + + /** True if this participant is currently muted. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voice") + bool bMuted = false; + + // ── Constructors ──────────────────────────────────────────────── + FVoiceParticipant() = default; + + FVoiceParticipant(const FString& InID, const FString& InName, EVoiceRole InRole) + : UserID(InID) + , DisplayName(InName) + , Role(InRole) + , bMuted(false) + {} +}; + +// ───────────────────────────────────────────────────────────────────── +// EVoicePermissionResult — returned from permission checks +// ───────────────────────────────────────────────────────────────────── +UENUM(BlueprintType) +enum class EVoicePermissionResult : uint8 +{ + Allowed UMETA(DisplayName = "Allowed"), + Denied UMETA(DisplayName = "Denied") +}; diff --git a/Source/AudioVideoRecord.Target.cs b/Source/AudioVideoRecord.Target.cs new file mode 100644 index 0000000..edc66d5 --- /dev/null +++ b/Source/AudioVideoRecord.Target.cs @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AudioVideoRecordTarget : TargetRules +{ + public AudioVideoRecordTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V5; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; + ExtraModuleNames.Add("AudioVideoRecord"); + } +} diff --git a/Source/AudioVideoRecord/AudioVideoRecord.Build.cs b/Source/AudioVideoRecord/AudioVideoRecord.Build.cs new file mode 100644 index 0000000..1b8bfab --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecord.Build.cs @@ -0,0 +1,48 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class AudioVideoRecord : ModuleRules +{ + public AudioVideoRecord(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "EnhancedInput", + "AIModule", + "StateTreeModule", + "GameplayStateTreeModule", + "UMG", + "Slate", + "AudioMixer", // Submix recording (audio capture) + "RHI", // RHI commands for ReadSurfaceData + "RenderCore", // Render thread utilities + "SlateCore" // Slate renderer back-buffer delegate + }); + + PrivateDependencyModuleNames.AddRange(new string[] { }); + + PublicIncludePaths.AddRange(new string[] { + "AudioVideoRecord", + "AudioVideoRecord/Variant_Horror", + "AudioVideoRecord/Variant_Horror/UI", + "AudioVideoRecord/Variant_Shooter", + "AudioVideoRecord/Variant_Shooter/AI", + "AudioVideoRecord/Variant_Shooter/UI", + "AudioVideoRecord/Variant_Shooter/Weapons" + }); + + // Uncomment if you are using Slate UI + // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); + + // Uncomment if you are using online features + // PrivateDependencyModuleNames.Add("OnlineSubsystem"); + + // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true + } +} diff --git a/Source/AudioVideoRecord/AudioVideoRecord.cpp b/Source/AudioVideoRecord/AudioVideoRecord.cpp new file mode 100644 index 0000000..4b1919b --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecord.cpp @@ -0,0 +1,8 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AudioVideoRecord.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, AudioVideoRecord, "AudioVideoRecord" ); + +DEFINE_LOG_CATEGORY(LogAudioVideoRecord) \ No newline at end of file diff --git a/Source/AudioVideoRecord/AudioVideoRecord.h b/Source/AudioVideoRecord/AudioVideoRecord.h new file mode 100644 index 0000000..6acdc06 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecord.h @@ -0,0 +1,8 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +/** Main log category used across the project */ +DECLARE_LOG_CATEGORY_EXTERN(LogAudioVideoRecord, Log, All); \ No newline at end of file diff --git a/Source/AudioVideoRecord/AudioVideoRecordCameraManager.cpp b/Source/AudioVideoRecord/AudioVideoRecordCameraManager.cpp new file mode 100644 index 0000000..e711c53 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordCameraManager.cpp @@ -0,0 +1,11 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "AudioVideoRecordCameraManager.h" + +AAudioVideoRecordCameraManager::AAudioVideoRecordCameraManager() +{ + // set the min/max pitch + ViewPitchMin = -70.0f; + ViewPitchMax = 80.0f; +} diff --git a/Source/AudioVideoRecord/AudioVideoRecordCameraManager.h b/Source/AudioVideoRecord/AudioVideoRecordCameraManager.h new file mode 100644 index 0000000..99d864d --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordCameraManager.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Camera/PlayerCameraManager.h" +#include "AudioVideoRecordCameraManager.generated.h" + +/** + * Basic First Person camera manager. + * Limits min/max look pitch. + */ +UCLASS() +class AAudioVideoRecordCameraManager : public APlayerCameraManager +{ + GENERATED_BODY() + +public: + + /** Constructor */ + AAudioVideoRecordCameraManager(); +}; diff --git a/Source/AudioVideoRecord/AudioVideoRecordCharacter.cpp b/Source/AudioVideoRecord/AudioVideoRecordCharacter.cpp new file mode 100644 index 0000000..3de9a98 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordCharacter.cpp @@ -0,0 +1,120 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AudioVideoRecordCharacter.h" +#include "Animation/AnimInstance.h" +#include "Camera/CameraComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "EnhancedInputComponent.h" +#include "InputActionValue.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "AudioVideoRecord.h" + +AAudioVideoRecordCharacter::AAudioVideoRecordCharacter() +{ + // Set size for collision capsule + GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f); + + // Create the first person mesh that will be viewed only by this character's owner + FirstPersonMesh = CreateDefaultSubobject(TEXT("First Person Mesh")); + + FirstPersonMesh->SetupAttachment(GetMesh()); + FirstPersonMesh->SetOnlyOwnerSee(true); + FirstPersonMesh->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::FirstPerson; + FirstPersonMesh->SetCollisionProfileName(FName("NoCollision")); + + // Create the Camera Component + FirstPersonCameraComponent = CreateDefaultSubobject(TEXT("First Person Camera")); + FirstPersonCameraComponent->SetupAttachment(FirstPersonMesh, FName("head")); + FirstPersonCameraComponent->SetRelativeLocationAndRotation(FVector(-2.8f, 5.89f, 0.0f), FRotator(0.0f, 90.0f, -90.0f)); + FirstPersonCameraComponent->bUsePawnControlRotation = true; + FirstPersonCameraComponent->bEnableFirstPersonFieldOfView = true; + FirstPersonCameraComponent->bEnableFirstPersonScale = true; + FirstPersonCameraComponent->FirstPersonFieldOfView = 70.0f; + FirstPersonCameraComponent->FirstPersonScale = 0.6f; + + // configure the character comps + GetMesh()->SetOwnerNoSee(true); + GetMesh()->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::WorldSpaceRepresentation; + + GetCapsuleComponent()->SetCapsuleSize(34.0f, 96.0f); + + // Configure character movement + GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f; + GetCharacterMovement()->AirControl = 0.5f; +} + +void AAudioVideoRecordCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + // Set up action bindings + if (UEnhancedInputComponent* EnhancedInputComponent = Cast(PlayerInputComponent)) + { + // Jumping + EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AAudioVideoRecordCharacter::DoJumpStart); + EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AAudioVideoRecordCharacter::DoJumpEnd); + + // Moving + EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAudioVideoRecordCharacter::MoveInput); + + // Looking/Aiming + EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AAudioVideoRecordCharacter::LookInput); + EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AAudioVideoRecordCharacter::LookInput); + } + else + { + UE_LOG(LogAudioVideoRecord, Error, TEXT("'%s' Failed to find an Enhanced Input Component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this)); + } +} + + +void AAudioVideoRecordCharacter::MoveInput(const FInputActionValue& Value) +{ + // get the Vector2D move axis + FVector2D MovementVector = Value.Get(); + + // pass the axis values to the move input + DoMove(MovementVector.X, MovementVector.Y); + +} + +void AAudioVideoRecordCharacter::LookInput(const FInputActionValue& Value) +{ + // get the Vector2D look axis + FVector2D LookAxisVector = Value.Get(); + + // pass the axis values to the aim input + DoAim(LookAxisVector.X, LookAxisVector.Y); + +} + +void AAudioVideoRecordCharacter::DoAim(float Yaw, float Pitch) +{ + if (GetController()) + { + // pass the rotation inputs + AddControllerYawInput(Yaw); + AddControllerPitchInput(Pitch); + } +} + +void AAudioVideoRecordCharacter::DoMove(float Right, float Forward) +{ + if (GetController()) + { + // pass the move inputs + AddMovementInput(GetActorRightVector(), Right); + AddMovementInput(GetActorForwardVector(), Forward); + } +} + +void AAudioVideoRecordCharacter::DoJumpStart() +{ + // pass Jump to the character + Jump(); +} + +void AAudioVideoRecordCharacter::DoJumpEnd() +{ + // pass StopJumping to the character + StopJumping(); +} diff --git a/Source/AudioVideoRecord/AudioVideoRecordCharacter.h b/Source/AudioVideoRecord/AudioVideoRecordCharacter.h new file mode 100644 index 0000000..d540fdc --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordCharacter.h @@ -0,0 +1,94 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Character.h" +#include "Logging/LogMacros.h" +#include "AudioVideoRecordCharacter.generated.h" + +class UInputComponent; +class USkeletalMeshComponent; +class UCameraComponent; +class UInputAction; +struct FInputActionValue; + +DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All); + +/** + * A basic first person character + */ +UCLASS(abstract) +class AAudioVideoRecordCharacter : public ACharacter +{ + GENERATED_BODY() + + /** Pawn mesh: first person view (arms; seen only by self) */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USkeletalMeshComponent* FirstPersonMesh; + + /** First person camera */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UCameraComponent* FirstPersonCameraComponent; + +protected: + + /** Jump Input Action */ + UPROPERTY(EditAnywhere, Category ="Input") + UInputAction* JumpAction; + + /** Move Input Action */ + UPROPERTY(EditAnywhere, Category ="Input") + UInputAction* MoveAction; + + /** Look Input Action */ + UPROPERTY(EditAnywhere, Category ="Input") + class UInputAction* LookAction; + + /** Mouse Look Input Action */ + UPROPERTY(EditAnywhere, Category ="Input") + class UInputAction* MouseLookAction; + +public: + AAudioVideoRecordCharacter(); + +protected: + + /** Called from Input Actions for movement input */ + void MoveInput(const FInputActionValue& Value); + + /** Called from Input Actions for looking input */ + void LookInput(const FInputActionValue& Value); + + /** Handles aim inputs from either controls or UI interfaces */ + UFUNCTION(BlueprintCallable, Category="Input") + virtual void DoAim(float Yaw, float Pitch); + + /** Handles move inputs from either controls or UI interfaces */ + UFUNCTION(BlueprintCallable, Category="Input") + virtual void DoMove(float Right, float Forward); + + /** Handles jump start inputs from either controls or UI interfaces */ + UFUNCTION(BlueprintCallable, Category="Input") + virtual void DoJumpStart(); + + /** Handles jump end inputs from either controls or UI interfaces */ + UFUNCTION(BlueprintCallable, Category="Input") + virtual void DoJumpEnd(); + +protected: + + /** Set up input action bindings */ + virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override; + + +public: + + /** Returns the first person mesh **/ + USkeletalMeshComponent* GetFirstPersonMesh() const { return FirstPersonMesh; } + + /** Returns first person camera component **/ + UCameraComponent* GetFirstPersonCameraComponent() const { return FirstPersonCameraComponent; } + +}; + diff --git a/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp b/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp new file mode 100644 index 0000000..fb772d5 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp @@ -0,0 +1,8 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AudioVideoRecordGameMode.h" + +AAudioVideoRecordGameMode::AAudioVideoRecordGameMode() +{ + // stub +} diff --git a/Source/AudioVideoRecord/AudioVideoRecordGameMode.h b/Source/AudioVideoRecord/AudioVideoRecordGameMode.h new file mode 100644 index 0000000..9749052 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordGameMode.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "AudioVideoRecordGameMode.generated.h" + +/** + * Simple GameMode for a first person game + */ +UCLASS(abstract) +class AAudioVideoRecordGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + AAudioVideoRecordGameMode(); +}; + + + diff --git a/Source/AudioVideoRecord/AudioVideoRecordPlayerController.cpp b/Source/AudioVideoRecord/AudioVideoRecordPlayerController.cpp new file mode 100644 index 0000000..72267c3 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordPlayerController.cpp @@ -0,0 +1,70 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "AudioVideoRecordPlayerController.h" +#include "EnhancedInputSubsystems.h" +#include "Engine/LocalPlayer.h" +#include "InputMappingContext.h" +#include "AudioVideoRecordCameraManager.h" +#include "Blueprint/UserWidget.h" +#include "AudioVideoRecord.h" +#include "Widgets/Input/SVirtualJoystick.h" + +AAudioVideoRecordPlayerController::AAudioVideoRecordPlayerController() +{ + // set the player camera manager class + PlayerCameraManagerClass = AAudioVideoRecordCameraManager::StaticClass(); +} + +void AAudioVideoRecordPlayerController::BeginPlay() +{ + Super::BeginPlay(); + + + // only spawn touch controls on local player controllers + if (SVirtualJoystick::ShouldDisplayTouchInterface() && IsLocalPlayerController()) + { + // spawn the mobile controls widget + MobileControlsWidget = CreateWidget(this, MobileControlsWidgetClass); + + if (MobileControlsWidget) + { + // add the controls to the player screen + MobileControlsWidget->AddToPlayerScreen(0); + + } else { + + UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn mobile controls widget.")); + + } + + } +} + +void AAudioVideoRecordPlayerController::SetupInputComponent() +{ + Super::SetupInputComponent(); + + // only add IMCs for local player controllers + if (IsLocalPlayerController()) + { + // Add Input Mapping Context + if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(GetLocalPlayer())) + { + for (UInputMappingContext* CurrentContext : DefaultMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + + // only add these IMCs if we're not using mobile touch input + if (!SVirtualJoystick::ShouldDisplayTouchInterface()) + { + for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + } + } + } + +} diff --git a/Source/AudioVideoRecord/AudioVideoRecordPlayerController.h b/Source/AudioVideoRecord/AudioVideoRecordPlayerController.h new file mode 100644 index 0000000..60d0ee3 --- /dev/null +++ b/Source/AudioVideoRecord/AudioVideoRecordPlayerController.h @@ -0,0 +1,50 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "AudioVideoRecordPlayerController.generated.h" + +class UInputMappingContext; +class UUserWidget; + +/** + * Simple first person Player Controller + * Manages the input mapping context. + * Overrides the Player Camera Manager class. + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AAudioVideoRecordPlayerController : public APlayerController +{ + GENERATED_BODY() + +public: + + /** Constructor */ + AAudioVideoRecordPlayerController(); + +protected: + + /** Input Mapping Contexts */ + UPROPERTY(EditAnywhere, Category="Input|Input Mappings") + TArray DefaultMappingContexts; + + /** Input Mapping Contexts */ + UPROPERTY(EditAnywhere, Category="Input|Input Mappings") + TArray MobileExcludedMappingContexts; + + /** Mobile controls widget to spawn */ + UPROPERTY(EditAnywhere, Category="Input|Touch Controls") + TSubclassOf MobileControlsWidgetClass; + + /** Pointer to the mobile controls widget */ + TObjectPtr MobileControlsWidget; + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Input mapping context setup */ + virtual void SetupInputComponent() override; + +}; diff --git a/Source/AudioVideoRecord/SimpleRecorder.cpp b/Source/AudioVideoRecord/SimpleRecorder.cpp new file mode 100644 index 0000000..d029f2d --- /dev/null +++ b/Source/AudioVideoRecord/SimpleRecorder.cpp @@ -0,0 +1,518 @@ +// SimpleRecorder.cpp — Full implementation +// Video: back-buffer → raw BGRA → FFmpeg pipe → NVENC → video_only.mp4 +// Audio: Submix listener → float PCM → WAV file → audio_only.wav +// Mux: FFmpeg CLI → final_video_with_audio.mp4 + +#include "SimpleRecorder.h" + +#include "AudioVideoRecord.h" // For LogAudioVideoRecord +#include "AudioDevice.h" +#include "AudioMixerDevice.h" +#include "Engine/GameViewportClient.h" +#include "Framework/Application/SlateApplication.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFileManager.h" +#include "HAL/PlatformProcess.h" +#include "RenderingThread.h" +#include "RHI.h" +#include "RHICommandList.h" +#include "RHIResources.h" +#include "Serialization/Archive.h" + +// ===================================================================== +// FSubmixBridge — wraps a callback in a TSharedRef for UE 5.6 API +// UObjects can't use AsShared(), so we need this bridge. +// ===================================================================== +class FSimpleRecorderSubmixBridge + : public ISubmixBufferListener +{ +public: + TWeakObjectPtr Owner; + + FSimpleRecorderSubmixBridge(USimpleRecorder* InOwner) : Owner(InOwner) {} + + virtual void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock) override + { + if (USimpleRecorder* Rec = Owner.Get()) + { + Rec->OnNewSubmixBuffer(OwningSubmix, AudioData, NumSamples, NumChannels, SampleRate, AudioClock); + } + } +}; + +// ===================================================================== +// Constructor +// ===================================================================== +USimpleRecorder::USimpleRecorder() +{ + // Default output directory: /Saved/Recordings + OutputDirectory = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Recordings")); +} + +// ===================================================================== +// BeginDestroy — safety net: stop recording if object is garbage-collected +// ===================================================================== +void USimpleRecorder::BeginDestroy() +{ + if (bIsRecording) + { + StopRecording(); + } + Super::BeginDestroy(); +} + +// ===================================================================== +// Helpers +// ===================================================================== + +/** Resolve the ffmpeg executable path. */ +FString USimpleRecorder::GetFFmpegExecutable() const +{ + if (!FFmpegPath.IsEmpty()) + { + return FFmpegPath; + } + // Assume ffmpeg is on the system PATH + return TEXT("ffmpeg"); +} + +/** Set up output file paths and make sure the directory exists. */ +void USimpleRecorder::InitOutputPaths() +{ + // Ensure absolute path + FString Dir = OutputDirectory; + if (FPaths::IsRelative(Dir)) + { + Dir = FPaths::ConvertRelativePathToFull(Dir); + } + + // Create directory if it doesn't exist + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*Dir)) + { + PlatformFile.CreateDirectoryTree(*Dir); + } + + VideoFilePath = FPaths::Combine(Dir, TEXT("video_only.mp4")); + AudioFilePath = FPaths::Combine(Dir, TEXT("audio_only.wav")); + FinalFilePath = FPaths::Combine(Dir, TEXT("final_video_with_audio.mp4")); +} + +// ===================================================================== +// StartRecording +// ===================================================================== +void USimpleRecorder::StartRecording() +{ + if (bIsRecording) + { + UE_LOG(LogAudioVideoRecord, Warning, TEXT("SimpleRecorder: Already recording!")); + return; + } + + InitOutputPaths(); + bIsRecording = true; + + // ─── 0. Auto-detect viewport size if not explicitly set ───────── + if (bAutoDetectResolution) + { + if (GEngine && GEngine->GameViewport) + { + FVector2D ViewportSize; + GEngine->GameViewport->GetViewportSize(ViewportSize); + if (ViewportSize.X > 0 && ViewportSize.Y > 0) + { + CaptureWidth = FMath::RoundToInt32(ViewportSize.X); + CaptureHeight = FMath::RoundToInt32(ViewportSize.Y); + // Ensure even dimensions (required by most video encoders) + CaptureWidth = CaptureWidth & ~1; + CaptureHeight = CaptureHeight & ~1; + } + } + } + + UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Starting recording...")); + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Resolution: %dx%d @ %d FPS"), CaptureWidth, CaptureHeight, CaptureFPS); + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Video → %s"), *VideoFilePath); + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Audio → %s"), *AudioFilePath); + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Final → %s"), *FinalFilePath); + + // ─── 1. Open FFmpeg video pipe ────────────────────────────────── + { + FString FFmpeg = GetFFmpegExecutable(); + + // Build command: + // ffmpeg -y -f rawvideo -pix_fmt bgra + // -video_size WxH -framerate FPS + // -i - (stdin) + // -c:v ... -b:v BITRATE + // output.mp4 + // + // Encoder selection per platform: + // Mac: h264_videotoolbox (Apple HW encoder) + // Windows: h264_nvenc / h264_amf / h264_qsv (detected at runtime) + // Linux: libx264 (software fallback) +#if PLATFORM_MAC + FString EncoderFlags = FString::Printf(TEXT("-c:v h264_videotoolbox -b:v %s"), *VideoBitrate); +#elif PLATFORM_WINDOWS + FString EncoderFlags = FString::Printf(TEXT("-c:v h264_nvenc -preset p5 -b:v %s"), *VideoBitrate); +#else + FString EncoderFlags = FString::Printf(TEXT("-c:v libx264 -preset medium -b:v %s"), *VideoBitrate); +#endif + + FString Cmd = FString::Printf( + TEXT("\"%s\" -y -f rawvideo -pix_fmt bgra -video_size %dx%d -framerate %d ") + TEXT("-i - %s \"%s\""), + *FFmpeg, + CaptureWidth, CaptureHeight, CaptureFPS, + *EncoderFlags, + *VideoFilePath + ); + + UE_LOG(LogAudioVideoRecord, Log, TEXT(" FFmpeg cmd: %s"), *Cmd); + + // popen opens a pipe — we write raw BGRA bytes into FFmpeg's stdin +#if PLATFORM_WINDOWS + FFmpegVideoPipe = _popen(TCHAR_TO_ANSI(*Cmd), "wb"); +#else + FFmpegVideoPipe = popen(TCHAR_TO_ANSI(*Cmd), "w"); +#endif + if (!FFmpegVideoPipe) + { + UE_LOG(LogAudioVideoRecord, Error, + TEXT("SimpleRecorder: Failed to open FFmpeg pipe! Make sure ffmpeg.exe is on your PATH.")); + bIsRecording = false; + return; + } + } + + // ─── 2. Register back-buffer delegate (video frames) ──────────── + if (FSlateApplication::IsInitialized()) + { + BackBufferDelegateHandle = + FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent() + .AddUObject(this, &USimpleRecorder::OnBackBufferReady); + } + + // ─── 3. Start audio submix recording ──────────────────────────── + { + // Clear any leftover audio data + FScopeLock Lock(&AudioBufferCritSection); + AudioBuffer.Empty(); + } + + if (GEngine && GEngine->GetMainAudioDevice()) + { + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (AudioDevice) + { + // If user didn't set a submix, use the master submix + USoundSubmix* Submix = TargetSubmix; + if (!Submix) + { + // Get the main submix from the audio device + Submix = &AudioDevice->GetMainSubmixObject(); + } + + if (Submix) + { + SubmixBridge = MakeShared(this); + AudioDevice->RegisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *Submix); + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Audio submix listener registered.")); + } + else + { + UE_LOG(LogAudioVideoRecord, Warning, + TEXT("SimpleRecorder: Could not find a submix to record audio from.")); + } + } + } + + UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Recording started.")); +} + +// ===================================================================== +// StopRecording +// ===================================================================== +void USimpleRecorder::StopRecording() +{ + if (!bIsRecording) + { + UE_LOG(LogAudioVideoRecord, Warning, TEXT("SimpleRecorder: Not recording.")); + return; + } + + bIsRecording = false; + UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Stopping recording...")); + + // ─── 1. Unregister back-buffer delegate ───────────────────────── + if (FSlateApplication::IsInitialized() && BackBufferDelegateHandle.IsValid()) + { + FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent() + .Remove(BackBufferDelegateHandle); + BackBufferDelegateHandle.Reset(); + } + + // ─── 2. Close the FFmpeg video pipe ───────────────────────────── + if (FFmpegVideoPipe) + { +#if PLATFORM_WINDOWS + _pclose(FFmpegVideoPipe); +#else + pclose(FFmpegVideoPipe); +#endif + FFmpegVideoPipe = nullptr; + UE_LOG(LogAudioVideoRecord, Log, TEXT(" FFmpeg video pipe closed → video_only.mp4 written.")); + } + + // ─── 3. Unregister audio submix listener ──────────────────────── + if (GEngine && GEngine->GetMainAudioDevice()) + { + FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice().GetAudioDevice(); + if (AudioDevice) + { + USoundSubmix* Submix = TargetSubmix; + if (!Submix) + { + Submix = &AudioDevice->GetMainSubmixObject(); + } + if (Submix) + { + if (SubmixBridge.IsValid()) + { + AudioDevice->UnregisterSubmixBufferListener(SubmixBridge.ToSharedRef(), *Submix); + SubmixBridge.Reset(); + } + } + } + } + + // ─── 4. Save audio to .wav ────────────────────────────────────── + SaveAudioToWav(); + + // ─── 5. Mux video + audio → final mp4 ────────────────────────── + MuxAudioVideo(); + + UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Recording stopped. All files saved.")); +} + +// ===================================================================== +// OnBackBufferReady — called every presented frame on the RENDER thread +// ===================================================================== +void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer) +{ + // Safety: if we already stopped, do nothing + if (!bIsRecording || !FFmpegVideoPipe) + { + return; + } + + // We're on the render thread here. + // Read the back-buffer pixels into a CPU-side array. + + FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); + + // Determine actual back-buffer size (may differ from our target) + const FIntPoint BBSize = BackBuffer->GetSizeXY(); + + // We need a rect matching our capture resolution, clamped to back-buffer + const int32 ReadW = FMath::Min(CaptureWidth, (int32)BBSize.X); + const int32 ReadH = FMath::Min(CaptureHeight, (int32)BBSize.Y); + + FIntRect ReadRect(0, 0, ReadW, ReadH); + + // ReadSurfaceData puts pixels into a TArray (BGRA, 8-bit per channel) + TArray Pixels; + Pixels.SetNumUninitialized(ReadW * ReadH); + + RHICmdList.ReadSurfaceData( + BackBuffer.GetReference(), + ReadRect, + Pixels, + FReadSurfaceDataFlags(RCM_UNorm) + ); + + // If the back-buffer is smaller than our target, we still write the full + // frame size so FFmpeg doesn't choke. Pad with black if needed. + if (ReadW == CaptureWidth && ReadH == CaptureHeight) + { + // Fast path — dimensions match exactly + fwrite(Pixels.GetData(), sizeof(FColor), Pixels.Num(), FFmpegVideoPipe); + } + else + { + // Slow path — need to pad to CaptureWidth x CaptureHeight + // Allocate a black frame + TArray PaddedFrame; + PaddedFrame.SetNumZeroed(CaptureWidth * CaptureHeight); + + for (int32 Row = 0; Row < ReadH; ++Row) + { + FMemory::Memcpy( + &PaddedFrame[Row * CaptureWidth], + &Pixels[Row * ReadW], + ReadW * sizeof(FColor)); + } + + fwrite(PaddedFrame.GetData(), sizeof(FColor), PaddedFrame.Num(), FFmpegVideoPipe); + } +} + +// ===================================================================== +// ISubmixBufferListener — audio callback (called from the audio thread) +// ===================================================================== +void USimpleRecorder::OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock) +{ + if (!bIsRecording) + { + return; + } + + FScopeLock Lock(&AudioBufferCritSection); + + // Store the format (may already be set, but harmless to update) + AudioSampleRate = SampleRate; + AudioNumChannels = NumChannels; + + // Append the incoming samples + AudioBuffer.Append(AudioData, NumSamples); +} + +// ===================================================================== +// SaveAudioToWav — writes accumulated PCM float data as a 16-bit WAV +// ===================================================================== +void USimpleRecorder::SaveAudioToWav() +{ + FScopeLock Lock(&AudioBufferCritSection); + + if (AudioBuffer.Num() == 0) + { + UE_LOG(LogAudioVideoRecord, Warning, TEXT("SimpleRecorder: No audio data captured.")); + return; + } + + UE_LOG(LogAudioVideoRecord, Log, + TEXT(" Saving audio: %d samples, %d channels, %d Hz"), + AudioBuffer.Num(), AudioNumChannels, AudioSampleRate); + + // Convert float [-1,1] → int16 + TArray PCM16; + PCM16.SetNumUninitialized(AudioBuffer.Num()); + for (int32 i = 0; i < AudioBuffer.Num(); ++i) + { + float Clamped = FMath::Clamp(AudioBuffer[i], -1.0f, 1.0f); + PCM16[i] = static_cast(Clamped * 32767.0f); + } + + // ── WAV header ────────────────────────────────────────────────── + const int32 BitsPerSample = 16; + const int32 BytesPerSample = BitsPerSample / 8; + const int32 DataSize = PCM16.Num() * BytesPerSample; + const int32 ByteRate = AudioSampleRate * AudioNumChannels * BytesPerSample; + const int16 BlockAlign = static_cast(AudioNumChannels * BytesPerSample); + + TArray WavFile; + // Reserve enough space: 44-byte header + data + WavFile.Reserve(44 + DataSize); + + // Helper lambdas to append little-endian integers + auto Write4CC = [&](const char* FourCC) + { + WavFile.Append(reinterpret_cast(FourCC), 4); + }; + auto WriteInt32 = [&](int32 Val) + { + WavFile.Append(reinterpret_cast(&Val), 4); + }; + auto WriteInt16 = [&](int16 Val) + { + WavFile.Append(reinterpret_cast(&Val), 2); + }; + + // RIFF header + Write4CC("RIFF"); + WriteInt32(36 + DataSize); // File size - 8 + Write4CC("WAVE"); + + // fmt sub-chunk + Write4CC("fmt "); + WriteInt32(16); // Sub-chunk size (PCM) + WriteInt16(1); // Audio format = PCM + WriteInt16(static_cast(AudioNumChannels)); + WriteInt32(AudioSampleRate); + WriteInt32(ByteRate); + WriteInt16(BlockAlign); + WriteInt16(static_cast(BitsPerSample)); + + // data sub-chunk + Write4CC("data"); + WriteInt32(DataSize); + WavFile.Append(reinterpret_cast(PCM16.GetData()), DataSize); + + // Write to disk + if (FFileHelper::SaveArrayToFile(WavFile, *AudioFilePath)) + { + UE_LOG(LogAudioVideoRecord, Log, TEXT(" audio_only.wav saved (%d bytes)."), WavFile.Num()); + } + else + { + UE_LOG(LogAudioVideoRecord, Error, TEXT(" Failed to save audio_only.wav!")); + } + + // Free memory + AudioBuffer.Empty(); +} + +// ===================================================================== +// MuxAudioVideo — runs FFmpeg to combine video + audio into final mp4 +// ===================================================================== +void USimpleRecorder::MuxAudioVideo() +{ + FString FFmpeg = GetFFmpegExecutable(); + + // ffmpeg -y -i video_only.mp4 -i audio_only.wav -c:v copy -c:a aac -shortest final.mp4 + FString Cmd = FString::Printf( + TEXT("\"%s\" -y -i \"%s\" -i \"%s\" -c:v copy -c:a aac -shortest \"%s\""), + *FFmpeg, + *VideoFilePath, + *AudioFilePath, + *FinalFilePath + ); + + UE_LOG(LogAudioVideoRecord, Log, TEXT(" Mux cmd: %s"), *Cmd); + + // Run synchronously (blocks until done) + int32 ReturnCode = -1; + FString StdOut; + FString StdErr; + FPlatformProcess::ExecProcess( + *FFmpeg, + *FString::Printf( + TEXT("-y -i \"%s\" -i \"%s\" -c:v copy -c:a aac -shortest \"%s\""), + *VideoFilePath, *AudioFilePath, *FinalFilePath), + &ReturnCode, &StdOut, &StdErr + ); + + if (ReturnCode == 0) + { + UE_LOG(LogAudioVideoRecord, Log, + TEXT(" Mux complete → final_video_with_audio.mp4 saved.")); + } + else + { + UE_LOG(LogAudioVideoRecord, Error, + TEXT(" Mux failed (code %d). stderr: %s"), ReturnCode, *StdErr); + } +} \ No newline at end of file diff --git a/Source/AudioVideoRecord/SimpleRecorder.h b/Source/AudioVideoRecord/SimpleRecorder.h new file mode 100644 index 0000000..13f89b2 --- /dev/null +++ b/Source/AudioVideoRecord/SimpleRecorder.h @@ -0,0 +1,125 @@ +// SimpleRecorder.h — In-game gameplay recorder (Video + Audio + Mux) +// Captures the Unreal back buffer & audio submix, pipes to FFmpeg/NVENC. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Sound/SoundSubmix.h" +#include "ISubmixBufferListener.h" +#include "SimpleRecorder.generated.h" + + +// ───────────────────────────────────────────────────────────────────── +// USimpleRecorder +// +// A UObject-based recorder you can create from Blueprint or C++. +// • StartRecording() — begins video + audio capture +// • StopRecording() — stops capture, writes .wav, muxes final .mp4 +// +// Outputs (inside /Saved/Recordings/): +// video_only.mp4 — NVENC-encoded H.264 +// audio_only.wav — PCM 16-bit +// final_video_with_audio.mp4 — muxed result +// ───────────────────────────────────────────────────────────────────── +UCLASS(BlueprintType) +class AUDIOVIDEORECORD_API USimpleRecorder : public UObject +{ + GENERATED_BODY() + +public: + USimpleRecorder(); + + // ── Blueprint-callable API ─────────────────────────────────────── + /** Starts capturing video frames and audio. */ + UFUNCTION(BlueprintCallable, Category = "SimpleRecorder") + void StartRecording(); + + /** Stops capturing. Saves audio_only.wav, then muxes the final file. */ + UFUNCTION(BlueprintCallable, Category = "SimpleRecorder") + void StopRecording(); + + /** Returns true while recording is active. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "SimpleRecorder") + bool IsRecording() const { return bIsRecording; } + + // ── Configurable settings (edit before calling StartRecording) ─── + /** If true, capture resolution is auto-detected from the viewport at recording start. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + bool bAutoDetectResolution = true; + + /** Capture width in pixels (ignored if bAutoDetectResolution is true). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + int32 CaptureWidth = 1920; + + /** Capture height in pixels (ignored if bAutoDetectResolution is true). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + int32 CaptureHeight = 1080; + + /** Target frames per second. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + int32 CaptureFPS = 60; + + /** Video bitrate string for FFmpeg (e.g. "8M", "12M"). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + FString VideoBitrate = TEXT("8M"); + + /** Directory where output files are saved (absolute or project-relative). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + FString OutputDirectory; + + /** Full path to ffmpeg.exe. If empty we look on the system PATH. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + FString FFmpegPath; + + /** The audio submix to record. If null, the engine master submix is used. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SimpleRecorder|Settings") + USoundSubmix* TargetSubmix = nullptr; + + // ── Audio submix callback (forwarded from bridge) ────────────── + void OnNewSubmixBuffer( + const USoundSubmix* OwningSubmix, + float* AudioData, + int32 NumSamples, + int32 NumChannels, + const int32 SampleRate, + double AudioClock); + +protected: + virtual void BeginDestroy() override; + +private: + // ── Internal helpers ───────────────────────────────────────────── + void InitOutputPaths(); + FString GetFFmpegExecutable() const; + + /** Called every frame on the render thread when the back buffer is ready. */ + void OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer); + + /** Writes the captured audio buffer to a .wav file. */ + void SaveAudioToWav(); + + /** Runs FFmpeg to mux video_only.mp4 + audio_only.wav → final .mp4. */ + void MuxAudioVideo(); + + // ── State ──────────────────────────────────────────────────────── + bool bIsRecording = false; + + // Video + FDelegateHandle BackBufferDelegateHandle; + FILE* FFmpegVideoPipe = nullptr; + + // Audio — accumulated raw PCM data + TArray AudioBuffer; // interleaved float samples + int32 AudioSampleRate = 48000; + int32 AudioNumChannels = 2; + FCriticalSection AudioBufferCritSection; + + // Output file paths (computed once per recording session) + FString VideoFilePath; + FString AudioFilePath; + FString FinalFilePath; + + // Submix bridge (UObjects can't be TSharedRef, so we use a bridge) + TSharedPtr SubmixBridge; +}; diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.cpp b/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.cpp new file mode 100644 index 0000000..d3e76ed --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.cpp @@ -0,0 +1,143 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Horror/HorrorCharacter.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Camera/CameraComponent.h" +#include "Components/SpotLightComponent.h" +#include "EnhancedInputComponent.h" +#include "InputAction.h" + +AHorrorCharacter::AHorrorCharacter() +{ + // create the spotlight + SpotLight = CreateDefaultSubobject(TEXT("SpotLight")); + SpotLight->SetupAttachment(GetFirstPersonCameraComponent()); + + SpotLight->SetRelativeLocationAndRotation(FVector(30.0f, 17.5f, -5.0f), FRotator(-18.6f, -1.3f, 5.26f)); + SpotLight->Intensity = 0.5; + SpotLight->SetIntensityUnits(ELightUnits::Lumens); + SpotLight->AttenuationRadius = 1050.0f; + SpotLight->InnerConeAngle = 18.7f; + SpotLight->OuterConeAngle = 45.24f; +} + +void AHorrorCharacter::BeginPlay() +{ + Super::BeginPlay(); + + // initialize sprint meter to max + SprintMeter = SprintTime; + + // Initialize the walk speed + GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; + + // start the sprint tick timer + GetWorld()->GetTimerManager().SetTimer(SprintTimer, this, &AHorrorCharacter::SprintFixedTick, SprintFixedTickTime, true); +} + +void AHorrorCharacter::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the sprint timer + GetWorld()->GetTimerManager().ClearTimer(SprintTimer); +} + +void AHorrorCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + Super::SetupPlayerInputComponent(PlayerInputComponent); + + { + // Set up action bindings + if (UEnhancedInputComponent* EnhancedInputComponent = Cast(PlayerInputComponent)) + { + // Sprinting + EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AHorrorCharacter::DoStartSprint); + EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AHorrorCharacter::DoEndSprint); + + } + } +} + +void AHorrorCharacter::DoStartSprint() +{ + // set the sprinting flag + bSprinting = true; + + // are we out of recovery mode? + if (!bRecovering) + { + // set the sprint walk speed + GetCharacterMovement()->MaxWalkSpeed = SprintSpeed; + + // call the sprint state changed delegate + OnSprintStateChanged.Broadcast(true); + } + +} + +void AHorrorCharacter::DoEndSprint() +{ + // set the sprinting flag + bSprinting = false; + + // are we out of recovery mode? + if (!bRecovering) + { + // set the default walk speed + GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; + + // call the sprint state changed delegate + OnSprintStateChanged.Broadcast(false); + } +} + +void AHorrorCharacter::SprintFixedTick() +{ + // are we out of recovery, still have stamina and are moving faster than our walk speed? + if (bSprinting && !bRecovering && GetVelocity().Length() > WalkSpeed) + { + + // do we still have meter to burn? + if (SprintMeter > 0.0f) + { + // update the sprint meter + SprintMeter = FMath::Max(SprintMeter - SprintFixedTickTime, 0.0f); + + // have we run out of stamina? + if (SprintMeter <= 0.0f) + { + // raise the recovering flag + bRecovering = true; + + // set the recovering walk speed + GetCharacterMovement()->MaxWalkSpeed = RecoveringWalkSpeed; + } + } + + } else { + + // recover stamina + SprintMeter = FMath::Min(SprintMeter + SprintFixedTickTime, SprintTime); + + if (SprintMeter >= SprintTime) + { + // lower the recovering flag + bRecovering = false; + + // set the walk or sprint speed depending on whether the sprint button is down + GetCharacterMovement()->MaxWalkSpeed = bSprinting ? SprintSpeed : WalkSpeed; + + // update the sprint state depending on whether the button is down or not + OnSprintStateChanged.Broadcast(bSprinting); + } + + } + + // broadcast the sprint meter updated delegate + OnSprintMeterUpdated.Broadcast(SprintMeter / SprintTime); + +} diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.h b/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.h new file mode 100644 index 0000000..d15d1d0 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorCharacter.h @@ -0,0 +1,104 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AudioVideoRecordCharacter.h" +#include "HorrorCharacter.generated.h" + +class USpotLightComponent; +class UInputAction; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUpdateSprintMeterDelegate, float, Percentage); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSprintStateChangedDelegate, bool, bSprinting); + +/** + * Simple first person horror character + * Provides stamina-based sprinting + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AHorrorCharacter : public AAudioVideoRecordCharacter +{ + GENERATED_BODY() + + /** Player light source */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USpotLightComponent* SpotLight; + +protected: + + /** Fire weapon input action */ + UPROPERTY(EditAnywhere, Category ="Input") + UInputAction* SprintAction; + + /** If true, we're sprinting */ + bool bSprinting = false; + + /** If true, we're recovering stamina */ + bool bRecovering = false; + + /** Default walk speed when not sprinting or recovering */ + UPROPERTY(EditAnywhere, Category="Walk") + float WalkSpeed = 250.0f; + + /** Time interval for sprinting stamina ticks */ + UPROPERTY(EditAnywhere, Category="Sprint", meta = (ClampMin = 0, ClampMax = 1, Units = "s")) + float SprintFixedTickTime = 0.03333f; + + /** Sprint stamina amount. Maxes at SprintTime */ + float SprintMeter = 0.0f; + + /** How long we can sprint for, in seconds */ + UPROPERTY(EditAnywhere, Category="Sprint", meta = (ClampMin = 0, ClampMax = 10, Units = "s")) + float SprintTime = 3.0f; + + /** Walk speed while sprinting */ + UPROPERTY(EditAnywhere, Category="Sprint", meta = (ClampMin = 0, ClampMax = 10, Units = "cm/s")) + float SprintSpeed = 600.0f; + + /** Walk speed while recovering stamina */ + UPROPERTY(EditAnywhere, Category="Recovery", meta = (ClampMin = 0, ClampMax = 10, Units = "cm/s")) + float RecoveringWalkSpeed = 150.0f; + + /** Time it takes for the sprint meter to recover */ + UPROPERTY(EditAnywhere, Category="Recovery", meta = (ClampMin = 0, ClampMax = 10, Units = "s")) + float RecoveryTime = 0.0f; + + /** Sprint tick timer */ + FTimerHandle SprintTimer; + +public: + + /** Delegate called when the sprint meter should be updated */ + FUpdateSprintMeterDelegate OnSprintMeterUpdated; + + /** Delegate called when we start and stop sprinting */ + FSprintStateChangedDelegate OnSprintStateChanged; + +protected: + + /** Constructor */ + AHorrorCharacter(); + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Gameplay cleanup */ + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + + /** Set up input action bindings */ + virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override; + +protected: + + /** Starts sprinting behavior */ + UFUNCTION(BlueprintCallable, Category = "Input") + void DoStartSprint(); + + /** Stops sprinting behavior */ + UFUNCTION(BlueprintCallable, Category="Input") + void DoEndSprint(); + + /** Called while sprinting at a fixed time interval */ + void SprintFixedTick(); +}; diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.cpp b/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.cpp new file mode 100644 index 0000000..b0621d3 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.cpp @@ -0,0 +1,9 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Horror/HorrorGameMode.h" + +AHorrorGameMode::AHorrorGameMode() +{ + // stub +} diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.h b/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.h new file mode 100644 index 0000000..7e2b24c --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorGameMode.h @@ -0,0 +1,21 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "HorrorGameMode.generated.h" + +/** + * Simple GameMode for a first person horror game + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AHorrorGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + + /** Constructor */ + AHorrorGameMode(); +}; diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.cpp b/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.cpp new file mode 100644 index 0000000..361e125 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.cpp @@ -0,0 +1,92 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Horror/HorrorPlayerController.h" +#include "EnhancedInputSubsystems.h" +#include "Engine/LocalPlayer.h" +#include "InputMappingContext.h" +#include "AudioVideoRecordCameraManager.h" +#include "HorrorCharacter.h" +#include "HorrorUI.h" +#include "AudioVideoRecord.h" +#include "Widgets/Input/SVirtualJoystick.h" + +AHorrorPlayerController::AHorrorPlayerController() +{ + // set the player camera manager class + PlayerCameraManagerClass = AAudioVideoRecordCameraManager::StaticClass(); +} + +void AHorrorPlayerController::BeginPlay() +{ + Super::BeginPlay(); + + // only spawn touch controls on local player controllers + if (SVirtualJoystick::ShouldDisplayTouchInterface() && IsLocalPlayerController()) + { + // spawn the mobile controls widget + MobileControlsWidget = CreateWidget(this, MobileControlsWidgetClass); + + if (MobileControlsWidget) + { + // add the controls to the player screen + MobileControlsWidget->AddToPlayerScreen(0); + + } else { + + UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn mobile controls widget.")); + + } + + } +} + +void AHorrorPlayerController::OnPossess(APawn* aPawn) +{ + Super::OnPossess(aPawn); + + // only spawn UI on local player controllers + if (IsLocalPlayerController()) + { + // set up the UI for the character + if (AHorrorCharacter* HorrorCharacter = Cast(aPawn)) + { + // create the UI + if (!HorrorUI) + { + HorrorUI = CreateWidget(this, HorrorUIClass); + HorrorUI->AddToViewport(0); + } + + HorrorUI->SetupCharacter(HorrorCharacter); + } + } + +} + +void AHorrorPlayerController::SetupInputComponent() +{ + Super::SetupInputComponent(); + + // only add IMCs for local player controllers + if (IsLocalPlayerController()) + { + // Add Input Mapping Contexts + if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(GetLocalPlayer())) + { + for (UInputMappingContext* CurrentContext : DefaultMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + + // only add these IMCs if we're not using mobile touch input + if (!SVirtualJoystick::ShouldDisplayTouchInterface()) + { + for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + } + } + } +} diff --git a/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.h b/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.h new file mode 100644 index 0000000..ae29f4b --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/HorrorPlayerController.h @@ -0,0 +1,62 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "HorrorPlayerController.generated.h" + +class UInputMappingContext; +class UHorrorUI; + +/** + * Player Controller for a first person horror game + * Manages input mappings + * Manages UI + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AHorrorPlayerController : public APlayerController +{ + GENERATED_BODY() + +protected: + + /** Type of UI widget to spawn */ + UPROPERTY(EditAnywhere, Category="Horror|UI") + TSubclassOf HorrorUIClass; + + /** Pointer to the UI widget */ + TObjectPtr HorrorUI; + +public: + + /** Constructor */ + AHorrorPlayerController(); + +protected: + + /** Input Mapping Contexts */ + UPROPERTY(EditAnywhere, Category ="Input|Input Mappings") + TArray DefaultMappingContexts; + + /** Input Mapping Contexts */ + UPROPERTY(EditAnywhere, Category="Input|Input Mappings") + TArray MobileExcludedMappingContexts; + + /** Mobile controls widget to spawn */ + UPROPERTY(EditAnywhere, Category="Input|Touch Controls") + TSubclassOf MobileControlsWidgetClass; + + /** Pointer to the mobile controls widget */ + TObjectPtr MobileControlsWidget; + + /** Gameplay Initialization */ + virtual void BeginPlay() override; + + /** Possessed pawn initialization */ + virtual void OnPossess(APawn* aPawn) override; + + /** Input mapping context setup */ + virtual void SetupInputComponent() override; + +}; diff --git a/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.cpp b/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.cpp new file mode 100644 index 0000000..78d17bd --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.cpp @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "HorrorUI.h" +#include "HorrorCharacter.h" + +void UHorrorUI::SetupCharacter(AHorrorCharacter* HorrorCharacter) +{ + HorrorCharacter->OnSprintMeterUpdated.AddDynamic(this, &UHorrorUI::OnSprintMeterUpdated); + HorrorCharacter->OnSprintStateChanged.AddDynamic(this, &UHorrorUI::OnSprintStateChanged); +} + +void UHorrorUI::OnSprintMeterUpdated(float Percent) +{ + // call the BP handler + BP_SprintMeterUpdated(Percent); +} + +void UHorrorUI::OnSprintStateChanged(bool bSprinting) +{ + // call the BP handler + BP_SprintStateChanged(bSprinting); +} diff --git a/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.h b/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.h new file mode 100644 index 0000000..b770a89 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Horror/UI/HorrorUI.h @@ -0,0 +1,42 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "HorrorUI.generated.h" + +class AHorrorCharacter; + +/** + * Simple UI for a first person horror game + * Manages character sprint meter display + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API UHorrorUI : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** Sets up delegate listeners for the passed character */ + void SetupCharacter(AHorrorCharacter* HorrorCharacter); + + /** Called when the character's sprint meter is updated */ + UFUNCTION() + void OnSprintMeterUpdated(float Percent); + + /** Called when the character's sprint state changes */ + UFUNCTION() + void OnSprintStateChanged(bool bSprinting); + +protected: + + /** Passes control to Blueprint to update the sprint meter widgets */ + UFUNCTION(BlueprintImplementableEvent, Category="Horror", meta = (DisplayName = "Sprint Meter Updated")) + void BP_SprintMeterUpdated(float Percent); + + /** Passes control to Blueprint to update the sprint meter status */ + UFUNCTION(BlueprintImplementableEvent, Category="Horror", meta = (DisplayName = "Sprint State Changed")) + void BP_SprintStateChanged(bool bSprinting); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.cpp b/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.cpp new file mode 100644 index 0000000..7a45abe --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.cpp @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/AI/EnvQueryContext_Target.h" +#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h" +#include "EnvironmentQuery/EnvQueryTypes.h" +#include "ShooterAIController.h" + +void UEnvQueryContext_Target::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const +{ + // get the controller from the query instance + if (AShooterAIController* Controller = Cast(QueryInstance.Owner)) + { + // ensure the target is valid + if (IsValid(Controller->GetCurrentTarget())) + { + // add the controller's target actor to the context + UEnvQueryItemType_Actor::SetContextHelper(ContextData, Controller->GetCurrentTarget()); + + } else { + + // if for any reason there's no target, default to the controller + UEnvQueryItemType_Actor::SetContextHelper(ContextData, Controller); + } + } + +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.h b/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.h new file mode 100644 index 0000000..df15a9a --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/EnvQueryContext_Target.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "EnvironmentQuery/EnvQueryContext.h" +#include "EnvQueryContext_Target.generated.h" + +/** + * Custom EnvQuery Context that returns the actor currently targeted by an NPC + */ +UCLASS() +class AUDIOVIDEORECORD_API UEnvQueryContext_Target : public UEnvQueryContext +{ + GENERATED_BODY() + +public: + + /** Provides the context locations or actors for this EnvQuery */ + virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override; + +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.cpp b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.cpp new file mode 100644 index 0000000..f1697c3 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.cpp @@ -0,0 +1,74 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/AI/ShooterAIController.h" +#include "ShooterNPC.h" +#include "Components/StateTreeAIComponent.h" +#include "Perception/AIPerceptionComponent.h" +#include "Navigation/PathFollowingComponent.h" +#include "AI/Navigation/PathFollowingAgentInterface.h" + +AShooterAIController::AShooterAIController() +{ + // create the StateTree component + StateTreeAI = CreateDefaultSubobject(TEXT("StateTreeAI")); + + // create the AI perception component. It will be configured in BP + AIPerception = CreateDefaultSubobject(TEXT("AIPerception")); + + // subscribe to the AI perception delegates + AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AShooterAIController::OnPerceptionUpdated); + AIPerception->OnTargetPerceptionForgotten.AddDynamic(this, &AShooterAIController::OnPerceptionForgotten); +} + +void AShooterAIController::OnPossess(APawn* InPawn) +{ + Super::OnPossess(InPawn); + + // ensure we're possessing an NPC + if (AShooterNPC* NPC = Cast(InPawn)) + { + // add the team tag to the pawn + NPC->Tags.Add(TeamTag); + + // subscribe to the pawn's OnDeath delegate + NPC->OnPawnDeath.AddDynamic(this, &AShooterAIController::OnPawnDeath); + } +} + +void AShooterAIController::OnPawnDeath() +{ + // stop movement + GetPathFollowingComponent()->AbortMove(*this, FPathFollowingResultFlags::UserAbort); + + // stop StateTree logic + StateTreeAI->StopLogic(FString("")); + + // unpossess the pawn + UnPossess(); + + // destroy this controller + Destroy(); +} + +void AShooterAIController::SetCurrentTarget(AActor* Target) +{ + TargetEnemy = Target; +} + +void AShooterAIController::ClearCurrentTarget() +{ + TargetEnemy = nullptr; +} + +void AShooterAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus) +{ + // pass the data to the StateTree delegate hook + OnShooterPerceptionUpdated.ExecuteIfBound(Actor, Stimulus); +} + +void AShooterAIController::OnPerceptionForgotten(AActor* Actor) +{ + // pass the data to the StateTree delegate hook + OnShooterPerceptionForgotten.ExecuteIfBound(Actor); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.h b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.h new file mode 100644 index 0000000..2ecc632 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterAIController.h @@ -0,0 +1,85 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AIController.h" +#include "ShooterAIController.generated.h" + +class UStateTreeAIComponent; +class UAIPerceptionComponent; +struct FAIStimulus; + +DECLARE_DELEGATE_TwoParams(FShooterPerceptionUpdatedDelegate, AActor*, const FAIStimulus&); +DECLARE_DELEGATE_OneParam(FShooterPerceptionForgottenDelegate, AActor*); + +/** + * Simple AI Controller for a first person shooter enemy + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterAIController : public AAIController +{ + GENERATED_BODY() + + /** Runs the behavior StateTree for this NPC */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UStateTreeAIComponent* StateTreeAI; + + /** Detects other actors through sight, hearing and other senses */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UAIPerceptionComponent* AIPerception; + +protected: + + /** Team tag for pawn friend or foe identification */ + UPROPERTY(EditAnywhere, Category="Shooter") + FName TeamTag = FName("Enemy"); + + /** Enemy currently being targeted */ + TObjectPtr TargetEnemy; + +public: + + /** Called when an AI perception has been updated. StateTree task delegate hook */ + FShooterPerceptionUpdatedDelegate OnShooterPerceptionUpdated; + + /** Called when an AI perception has been forgotten. StateTree task delegate hook */ + FShooterPerceptionForgottenDelegate OnShooterPerceptionForgotten; + +public: + + /** Constructor */ + AShooterAIController(); + +protected: + + /** Pawn initialization */ + virtual void OnPossess(APawn* InPawn) override; + +protected: + + /** Called when the possessed pawn dies */ + UFUNCTION() + void OnPawnDeath(); + +public: + + /** Sets the targeted enemy */ + void SetCurrentTarget(AActor* Target); + + /** Clears the targeted enemy */ + void ClearCurrentTarget(); + + /** Returns the targeted enemy */ + AActor* GetCurrentTarget() const { return TargetEnemy; }; + +protected: + + /** Called when the AI perception component updates a perception on a given actor */ + UFUNCTION() + void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus); + + /** Called when the AI perception component forgets a given actor */ + UFUNCTION() + void OnPerceptionForgotten(AActor* Actor); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.cpp b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.cpp new file mode 100644 index 0000000..6d59f15 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.cpp @@ -0,0 +1,208 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/AI/ShooterNPC.h" +#include "ShooterWeapon.h" +#include "Components/SkeletalMeshComponent.h" +#include "Camera/CameraComponent.h" +#include "Kismet/KismetMathLibrary.h" +#include "Engine/World.h" +#include "ShooterGameMode.h" +#include "Components/CapsuleComponent.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "TimerManager.h" + +void AShooterNPC::BeginPlay() +{ + Super::BeginPlay(); + + // spawn the weapon + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = this; + SpawnParams.Instigator = this; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + Weapon = GetWorld()->SpawnActor(WeaponClass, GetActorTransform(), SpawnParams); +} + +void AShooterNPC::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the death timer + GetWorld()->GetTimerManager().ClearTimer(DeathTimer); +} + +float AShooterNPC::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) +{ + // ignore if already dead + if (bIsDead) + { + return 0.0f; + } + + // Reduce HP + CurrentHP -= Damage; + + // Have we depleted HP? + if (CurrentHP <= 0.0f) + { + Die(); + } + + return Damage; +} + +void AShooterNPC::AttachWeaponMeshes(AShooterWeapon* WeaponToAttach) +{ + const FAttachmentTransformRules AttachmentRule(EAttachmentRule::SnapToTarget, false); + + // attach the weapon actor + WeaponToAttach->AttachToActor(this, AttachmentRule); + + // attach the weapon meshes + WeaponToAttach->GetFirstPersonMesh()->AttachToComponent(GetFirstPersonMesh(), AttachmentRule, FirstPersonWeaponSocket); + WeaponToAttach->GetThirdPersonMesh()->AttachToComponent(GetMesh(), AttachmentRule, FirstPersonWeaponSocket); +} + +void AShooterNPC::PlayFiringMontage(UAnimMontage* Montage) +{ + // unused +} + +void AShooterNPC::AddWeaponRecoil(float Recoil) +{ + // unused +} + +void AShooterNPC::UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) +{ + // unused +} + +FVector AShooterNPC::GetWeaponTargetLocation() +{ + // start aiming from the camera location + const FVector AimSource = GetFirstPersonCameraComponent()->GetComponentLocation(); + + FVector AimDir, AimTarget = FVector::ZeroVector; + + // do we have an aim target? + if (CurrentAimTarget) + { + // target the actor location + AimTarget = CurrentAimTarget->GetActorLocation(); + + // apply a vertical offset to target head/feet + AimTarget.Z += FMath::RandRange(MinAimOffsetZ, MaxAimOffsetZ); + + // get the aim direction and apply randomness in a cone + AimDir = (AimTarget - AimSource).GetSafeNormal(); + AimDir = UKismetMathLibrary::RandomUnitVectorInConeInDegrees(AimDir, AimVarianceHalfAngle); + + + } else { + + // no aim target, so just use the camera facing + AimDir = UKismetMathLibrary::RandomUnitVectorInConeInDegrees(GetFirstPersonCameraComponent()->GetForwardVector(), AimVarianceHalfAngle); + + } + + // calculate the unobstructed aim target location + AimTarget = AimSource + (AimDir * AimRange); + + // run a visibility trace to see if there's obstructions + FHitResult OutHit; + + FCollisionQueryParams QueryParams; + QueryParams.AddIgnoredActor(this); + + GetWorld()->LineTraceSingleByChannel(OutHit, AimSource, AimTarget, ECC_Visibility, QueryParams); + + // return either the impact point or the trace end + return OutHit.bBlockingHit ? OutHit.ImpactPoint : OutHit.TraceEnd; +} + +void AShooterNPC::AddWeaponClass(const TSubclassOf& InWeaponClass) +{ + // unused +} + +void AShooterNPC::OnWeaponActivated(AShooterWeapon* InWeapon) +{ + // unused +} + +void AShooterNPC::OnWeaponDeactivated(AShooterWeapon* InWeapon) +{ + // unused +} + +void AShooterNPC::OnSemiWeaponRefire() +{ + // are we still shooting? + if (bIsShooting) + { + // fire the weapon + Weapon->StartFiring(); + } +} + +void AShooterNPC::Die() +{ + // ignore if already dead + if (bIsDead) + { + return; + } + + // raise the dead flag + bIsDead = true; + + // increment the team score + if (AShooterGameMode* GM = Cast(GetWorld()->GetAuthGameMode())) + { + GM->IncrementTeamScore(TeamByte); + } + + // disable capsule collision + GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + // stop movement + GetCharacterMovement()->StopMovementImmediately(); + GetCharacterMovement()->StopActiveMovement(); + + // enable ragdoll physics on the third person mesh + GetMesh()->SetCollisionProfileName(RagdollCollisionProfile); + GetMesh()->SetSimulatePhysics(true); + GetMesh()->SetPhysicsBlendWeight(1.0f); + + // schedule actor destruction + GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &AShooterNPC::DeferredDestruction, DeferredDestructionTime, false); +} + +void AShooterNPC::DeferredDestruction() +{ + Destroy(); +} + +void AShooterNPC::StartShooting(AActor* ActorToShoot) +{ + // save the aim target + CurrentAimTarget = ActorToShoot; + + // raise the flag + bIsShooting = true; + + // signal the weapon + Weapon->StartFiring(); +} + +void AShooterNPC::StopShooting() +{ + // lower the flag + bIsShooting = false; + + // signal the weapon + Weapon->StopFiring(); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.h b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.h new file mode 100644 index 0000000..38681ac --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterNPC.h @@ -0,0 +1,153 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AudioVideoRecordCharacter.h" +#include "ShooterWeaponHolder.h" +#include "ShooterNPC.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPawnDeathDelegate); + +class AShooterWeapon; + +/** + * A simple AI-controlled shooter game NPC + * Executes its behavior through a StateTree managed by its AI Controller + * Holds and manages a weapon + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterNPC : public AAudioVideoRecordCharacter, public IShooterWeaponHolder +{ + GENERATED_BODY() + +public: + + /** Current HP for this character. It dies if it reaches zero through damage */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage") + float CurrentHP = 100.0f; + +protected: + + /** Name of the collision profile to use during ragdoll death */ + UPROPERTY(EditAnywhere, Category="Damage") + FName RagdollCollisionProfile = FName("Ragdoll"); + + /** Time to wait after death before destroying this actor */ + UPROPERTY(EditAnywhere, Category="Damage") + float DeferredDestructionTime = 5.0f; + + /** Team byte for this character */ + UPROPERTY(EditAnywhere, Category="Team") + uint8 TeamByte = 1; + + /** Pointer to the equipped weapon */ + TObjectPtr Weapon; + + /** Type of weapon to spawn for this character */ + UPROPERTY(EditAnywhere, Category="Weapon") + TSubclassOf WeaponClass; + + /** Name of the first person mesh weapon socket */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category ="Weapons") + FName FirstPersonWeaponSocket = FName("HandGrip_R"); + + /** Name of the third person mesh weapon socket */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category ="Weapons") + FName ThirdPersonWeaponSocket = FName("HandGrip_R"); + + /** Max range for aiming calculations */ + UPROPERTY(EditAnywhere, Category="Aim") + float AimRange = 10000.0f; + + /** Cone variance to apply while aiming */ + UPROPERTY(EditAnywhere, Category="Aim") + float AimVarianceHalfAngle = 10.0f; + + /** Minimum vertical offset from the target center to apply when aiming */ + UPROPERTY(EditAnywhere, Category="Aim") + float MinAimOffsetZ = -35.0f; + + /** Maximum vertical offset from the target center to apply when aiming */ + UPROPERTY(EditAnywhere, Category="Aim") + float MaxAimOffsetZ = -60.0f; + + /** Actor currently being targeted */ + TObjectPtr CurrentAimTarget; + + /** If true, this character is currently shooting its weapon */ + bool bIsShooting = false; + + /** If true, this character has already died */ + bool bIsDead = false; + + /** Deferred destruction on death timer */ + FTimerHandle DeathTimer; + +public: + + /** Delegate called when this NPC dies */ + FPawnDeathDelegate OnPawnDeath; + +protected: + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Gameplay cleanup */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +public: + + /** Handle incoming damage */ + virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; + +public: + + //~Begin IShooterWeaponHolder interface + + /** Attaches a weapon's meshes to the owner */ + virtual void AttachWeaponMeshes(AShooterWeapon* Weapon) override; + + /** Plays the firing montage for the weapon */ + virtual void PlayFiringMontage(UAnimMontage* Montage) override; + + /** Applies weapon recoil to the owner */ + virtual void AddWeaponRecoil(float Recoil) override; + + /** Updates the weapon's HUD with the current ammo count */ + virtual void UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) override; + + /** Calculates and returns the aim location for the weapon */ + virtual FVector GetWeaponTargetLocation() override; + + /** Gives a weapon of this class to the owner */ + virtual void AddWeaponClass(const TSubclassOf& WeaponClass) override; + + /** Activates the passed weapon */ + virtual void OnWeaponActivated(AShooterWeapon* Weapon) override; + + /** Deactivates the passed weapon */ + virtual void OnWeaponDeactivated(AShooterWeapon* Weapon) override; + + /** Notifies the owner that the weapon cooldown has expired and it's ready to shoot again */ + virtual void OnSemiWeaponRefire() override; + + //~End IShooterWeaponHolder interface + +protected: + + /** Called when HP is depleted and the character should die */ + void Die(); + + /** Called after death to destroy the actor */ + void DeferredDestruction(); + +public: + + /** Signals this character to start shooting at the passed actor */ + void StartShooting(AActor* ActorToShoot); + + /** Signals this character to stop shooting */ + void StopShooting(); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.cpp b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.cpp new file mode 100644 index 0000000..6943378 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.cpp @@ -0,0 +1,366 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/AI/ShooterStateTreeUtility.h" +#include "StateTreeExecutionContext.h" +#include "ShooterNPC.h" +#include "Camera/CameraComponent.h" +#include "AIController.h" +#include "Perception/AIPerceptionComponent.h" +#include "ShooterAIController.h" +#include "StateTreeAsyncExecutionContext.h" + +bool FStateTreeLineOfSightToTargetCondition::TestCondition(FStateTreeExecutionContext& Context) const +{ + const FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // ensure the target is valid + if (!IsValid(InstanceData.Target)) + { + return !InstanceData.bMustHaveLineOfSight; + } + + // check if the character is facing towards the target + const FVector TargetDir = (InstanceData.Target->GetActorLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal(); + + const float FacingDot = FVector::DotProduct(TargetDir, InstanceData.Character->GetActorForwardVector()); + const float MaxDot = FMath::Cos(FMath::DegreesToRadians(InstanceData.LineOfSightConeAngle)); + + // is the facing outside of our cone half angle? + if (FacingDot <= MaxDot) + { + return !InstanceData.bMustHaveLineOfSight; + } + + // get the target's bounding box + FVector CenterOfMass, Extent; + InstanceData.Target->GetActorBounds(true, CenterOfMass, Extent, false); + + // divide the vertical extent by the number of line of sight checks we'll do + const float ExtentZOffset = Extent.Z * 2.0f / InstanceData.NumberOfVerticalLineOfSightChecks; + + // get the character's camera location as the source for the line checks + const FVector Start = InstanceData.Character->GetFirstPersonCameraComponent()->GetComponentLocation(); + + // ignore the character and target. We want to ensure there's an unobstructed trace not counting them + FCollisionQueryParams QueryParams; + QueryParams.AddIgnoredActor(InstanceData.Character); + QueryParams.AddIgnoredActor(InstanceData.Target); + + FHitResult OutHit; + + // run a number of vertically offset line traces to the target location + for (int32 i = 0; i < InstanceData.NumberOfVerticalLineOfSightChecks - 1; ++i) + { + // calculate the endpoint for the trace + const FVector End = CenterOfMass + FVector(0.0f, 0.0f, Extent.Z - ExtentZOffset * i); + + InstanceData.Character->GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams); + + // is the trace unobstructed? + if (!OutHit.bBlockingHit) + { + // we only need one unobstructed trace, so terminate early + return InstanceData.bMustHaveLineOfSight; + } + } + + // no line of sight found + return !InstanceData.bMustHaveLineOfSight; +} + +#if WITH_EDITOR +FText FStateTreeLineOfSightToTargetCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Has Line of Sight"); +} +#endif + +//////////////////////////////////////////////////////////////////// + +EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned from another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // set the AI Controller's focus + InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards); + } + + return EStateTreeRunStatus::Running; +} + +void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned to another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // clear the AI Controller's focus + InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay); + } +} + +#if WITH_EDITOR +FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Face Towards Actor"); +} +#endif // WITH_EDITOR + +//////////////////////////////////////////////////////////////////// + +EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned from another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // set the AI Controller's focus + InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation); + } + + return EStateTreeRunStatus::Running; +} + +void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned to another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // clear the AI Controller's focus + InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay); + } +} + +#if WITH_EDITOR +FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Face Towards Location"); +} +#endif // WITH_EDITOR + +//////////////////////////////////////////////////////////////////// + +EStateTreeRunStatus FStateTreeSetRandomFloatTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned to another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // calculate the output value + InstanceData.OutValue = FMath::RandRange(InstanceData.MinValue, InstanceData.MaxValue); + } + + return EStateTreeRunStatus::Running; +} + +#if WITH_EDITOR +FText FStateTreeSetRandomFloatTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Set Random Float"); +} +#endif // WITH_EDITOR + +//////////////////////////////////////////////////////////////////// + +EStateTreeRunStatus FStateTreeShootAtTargetTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned from another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // tell the character to shoot the target + InstanceData.Character->StartShooting(InstanceData.Target); + } + + return EStateTreeRunStatus::Running; +} + +void FStateTreeShootAtTargetTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned to another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // tell the character to stop shooting + InstanceData.Character->StopShooting(); + } +} + +#if WITH_EDITOR +FText FStateTreeShootAtTargetTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Shoot at Target"); +} +#endif // WITH_EDITOR + +EStateTreeRunStatus FStateTreeSenseEnemiesTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned from another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // bind the perception updated delegate on the controller + InstanceData.Controller->OnShooterPerceptionUpdated.BindLambda( + [WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor, const FAIStimulus& Stimulus) + { + // get the instance data inside the lambda + const FStateTreeStrongExecutionContext StrongContext = WeakContext.MakeStrongExecutionContext(); + + if (FInstanceDataType* LambdaInstanceData = StrongContext.GetInstanceDataPtr()) + { + if (SensedActor->ActorHasTag(LambdaInstanceData->SenseTag)) + { + bool bDirectLOS = false; + + // calculate the direction of the stimulus + const FVector StimulusDir = (Stimulus.StimulusLocation - LambdaInstanceData->Character->GetActorLocation()).GetSafeNormal(); + + // infer the angle from the dot product between the character facing and the stimulus direction + const float DirDot = FVector::DotProduct(StimulusDir, LambdaInstanceData->Character->GetActorForwardVector()); + const float MaxDot = FMath::Cos(FMath::DegreesToRadians(LambdaInstanceData->DirectLineOfSightCone)); + + // is the direction within our perception cone? + if (DirDot >= MaxDot) + { + // run a line trace between the character and the sensed actor + FCollisionQueryParams QueryParams; + QueryParams.AddIgnoredActor(LambdaInstanceData->Character); + QueryParams.AddIgnoredActor(SensedActor); + + FHitResult OutHit; + + // we have direct line of sight if this trace is unobstructed + bDirectLOS = !LambdaInstanceData->Character->GetWorld()->LineTraceSingleByChannel(OutHit, LambdaInstanceData->Character->GetActorLocation(), SensedActor->GetActorLocation(), ECC_Visibility, QueryParams); + + } + + // check if we have a direct line of sight to the stimulus + if (bDirectLOS) + { + // set the controller's target + LambdaInstanceData->Controller->SetCurrentTarget(SensedActor); + + // set the task output + LambdaInstanceData->TargetActor = SensedActor; + + // set the flags + LambdaInstanceData->bHasTarget = true; + LambdaInstanceData->bHasInvestigateLocation = false; + + // no direct line of sight to target + } else { + + // if we already have a target, ignore the partial sense and keep on them + if (!IsValid(LambdaInstanceData->TargetActor)) + { + // is this stimulus stronger than the last one we had? + if (Stimulus.Strength > LambdaInstanceData->LastStimulusStrength) + { + // update the stimulus strength + LambdaInstanceData->LastStimulusStrength = Stimulus.Strength; + + // set the investigate location + LambdaInstanceData->InvestigateLocation = Stimulus.StimulusLocation; + + // set the investigate flag + LambdaInstanceData->bHasInvestigateLocation = true; + } + } + } + } + } + } + ); + + // bind the perception forgotten delegate on the controller + InstanceData.Controller->OnShooterPerceptionForgotten.BindLambda( + [WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor) + { + // get the instance data inside the lambda + FInstanceDataType* LambdaInstanceData = WeakContext.MakeStrongExecutionContext().GetInstanceDataPtr(); + + if (!LambdaInstanceData) + { + return; + } + + bool bForget = false; + + // are we forgetting the current target? + if (SensedActor == LambdaInstanceData->TargetActor) + { + bForget = true; + + } else { + + // are we forgetting about a partial sense? + if (!IsValid(LambdaInstanceData->TargetActor)) + { + bForget = true; + } + } + + if (bForget) + { + // clear the target + LambdaInstanceData->TargetActor = nullptr; + + // clear the flags + LambdaInstanceData->bHasInvestigateLocation = false; + LambdaInstanceData->bHasTarget = false; + + // reset the stimulus strength + LambdaInstanceData->LastStimulusStrength = 0.0f; + + // clear the target on the controller + LambdaInstanceData->Controller->ClearCurrentTarget(); + LambdaInstanceData->Controller->ClearFocus(EAIFocusPriority::Gameplay); + } + + } + ); + } + + return EStateTreeRunStatus::Running; +} + +void FStateTreeSenseEnemiesTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const +{ + // have we transitioned to another state? + if (Transition.ChangeType == EStateTreeStateChangeType::Changed) + { + // get the instance data + FInstanceDataType& InstanceData = Context.GetInstanceData(*this); + + // unbind the perception delegates + InstanceData.Controller->OnShooterPerceptionUpdated.Unbind(); + InstanceData.Controller->OnShooterPerceptionForgotten.Unbind(); + } +} + +#if WITH_EDITOR +FText FStateTreeSenseEnemiesTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const +{ + return FText::FromString("Sense Enemies"); +} +#endif // WITH_EDITOR \ No newline at end of file diff --git a/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.h b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.h new file mode 100644 index 0000000..a9e24c8 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/AI/ShooterStateTreeUtility.h @@ -0,0 +1,309 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "StateTreeTaskBase.h" +#include "StateTreeConditionBase.h" + +#include "ShooterStateTreeUtility.generated.h" + +class AShooterNPC; +class AAIController; +class AShooterAIController; + +/** + * Instance data struct for the FStateTreeLineOfSightToTargetCondition condition + */ +USTRUCT() +struct FStateTreeLineOfSightToTargetConditionInstanceData +{ + GENERATED_BODY() + + /** Targeting character */ + UPROPERTY(EditAnywhere, Category = "Context") + AShooterNPC* Character; + + /** Target to check line of sight for */ + UPROPERTY(EditAnywhere, Category = "Condition") + AActor* Target; + + /** Max allowed line of sight cone angle, in degrees */ + UPROPERTY(EditAnywhere, Category = "Condition") + float LineOfSightConeAngle = 35.0f; + + /** Number of vertical line of sight checks to run to try and get around low obstacles */ + UPROPERTY(EditAnywhere, Category = "Condition") + int32 NumberOfVerticalLineOfSightChecks = 5; + + /** If true, the condition passes if the character has line of sight */ + UPROPERTY(EditAnywhere, Category = "Condition") + bool bMustHaveLineOfSight = true; +}; +STATETREE_POD_INSTANCEDATA(FStateTreeLineOfSightToTargetConditionInstanceData); + +/** + * StateTree condition to check if the character is grounded + */ +USTRUCT(DisplayName = "Has Line of Sight to Target", Category="Shooter") +struct FStateTreeLineOfSightToTargetCondition : public FStateTreeConditionCommonBase +{ + GENERATED_BODY() + + /** Set the instance data type */ + using FInstanceDataType = FStateTreeLineOfSightToTargetConditionInstanceData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Default constructor */ + FStateTreeLineOfSightToTargetCondition() = default; + + /** Tests the StateTree condition */ + virtual bool TestCondition(FStateTreeExecutionContext& Context) const override; + +#if WITH_EDITOR + /** Provides the description string */ + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif + +}; + +//////////////////////////////////////////////////////////////////// + +/** + * Instance data struct for the Face Towards Actor StateTree task + */ +USTRUCT() +struct FStateTreeFaceActorInstanceData +{ + GENERATED_BODY() + + /** AI Controller that will determine the focused actor */ + UPROPERTY(EditAnywhere, Category = Context) + TObjectPtr Controller; + + /** Actor that will be faced towards */ + UPROPERTY(EditAnywhere, Category = Input) + TObjectPtr ActorToFaceTowards; +}; + +/** + * StateTree task to face an AI-Controlled Pawn towards an Actor + */ +USTRUCT(meta=(DisplayName="Face Towards Actor", Category="Shooter")) +struct FStateTreeFaceActorTask : public FStateTreeTaskCommonBase +{ + GENERATED_BODY() + + /* Ensure we're using the correct instance data struct */ + using FInstanceDataType = FStateTreeFaceActorInstanceData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Runs when the owning state is entered */ + virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + + /** Runs when the owning state is ended */ + virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + +#if WITH_EDITOR + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif // WITH_EDITOR +}; + +//////////////////////////////////////////////////////////////////// + +/** + * Instance data struct for the Face Towards Location StateTree task + */ +USTRUCT() +struct FStateTreeFaceLocationInstanceData +{ + GENERATED_BODY() + + /** AI Controller that will determine the focused location */ + UPROPERTY(EditAnywhere, Category = Context) + TObjectPtr Controller; + + /** Location that will be faced towards */ + UPROPERTY(EditAnywhere, Category = Parameter) + FVector FaceLocation = FVector::ZeroVector; +}; + +/** + * StateTree task to face an AI-Controlled Pawn towards a world location + */ +USTRUCT(meta=(DisplayName="Face Towards Location", Category="Shooter")) +struct FStateTreeFaceLocationTask : public FStateTreeTaskCommonBase +{ + GENERATED_BODY() + + /* Ensure we're using the correct instance data struct */ + using FInstanceDataType = FStateTreeFaceLocationInstanceData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Runs when the owning state is entered */ + virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + + /** Runs when the owning state is ended */ + virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + +#if WITH_EDITOR + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif // WITH_EDITOR +}; + +//////////////////////////////////////////////////////////////////// + +/** + * Instance data struct for the Set Random Float StateTree task + */ +USTRUCT() +struct FStateTreeSetRandomFloatData +{ + GENERATED_BODY() + + /** Minimum random value */ + UPROPERTY(EditAnywhere, Category = Parameter) + float MinValue = 0.0f; + + /** Maximum random value */ + UPROPERTY(EditAnywhere, Category = Parameter) + float MaxValue = 0.0f; + + /** Output calculated value */ + UPROPERTY(EditAnywhere, Category = Output) + float OutValue = 0.0f; +}; + +/** + * StateTree task to calculate a random float value within the specified range + */ +USTRUCT(meta=(DisplayName="Set Random Float", Category="Shooter")) +struct FStateTreeSetRandomFloatTask : public FStateTreeTaskCommonBase +{ + GENERATED_BODY() + + /* Ensure we're using the correct instance data struct */ + using FInstanceDataType = FStateTreeSetRandomFloatData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Runs when the owning state is entered */ + virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + +#if WITH_EDITOR + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif // WITH_EDITOR +}; + +//////////////////////////////////////////////////////////////////// + +/** + * Instance data struct for the Shoot At Target StateTree task + */ +USTRUCT() +struct FStateTreeShootAtTargetInstanceData +{ + GENERATED_BODY() + + /** NPC that will do the shooting */ + UPROPERTY(EditAnywhere, Category = Context) + TObjectPtr Character; + + /** Target to shoot at */ + UPROPERTY(EditAnywhere, Category = Input) + TObjectPtr Target; +}; + +/** + * StateTree task to have an NPC shoot at an actor + */ +USTRUCT(meta=(DisplayName="Shoot at Target", Category="Shooter")) +struct FStateTreeShootAtTargetTask : public FStateTreeTaskCommonBase +{ + GENERATED_BODY() + + /* Ensure we're using the correct instance data struct */ + using FInstanceDataType = FStateTreeShootAtTargetInstanceData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Runs when the owning state is entered */ + virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + + /** Runs when the owning state is ended */ + virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + +#if WITH_EDITOR + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif // WITH_EDITOR +}; + +//////////////////////////////////////////////////////////////////// + +/** + * Instance data struct for the Sense Enemies StateTree task + */ +USTRUCT() +struct FStateTreeSenseEnemiesInstanceData +{ + GENERATED_BODY() + + /** Sensing AI Controller */ + UPROPERTY(EditAnywhere, Category = Context) + TObjectPtr Controller; + + /** Sensing NPC */ + UPROPERTY(EditAnywhere, Category = Context) + TObjectPtr Character; + + /** Sensed actor to target */ + UPROPERTY(EditAnywhere, Category = Output) + TObjectPtr TargetActor; + + /** Sensed location to investigate */ + UPROPERTY(EditAnywhere, Category = Output) + FVector InvestigateLocation = FVector::ZeroVector; + + /** True if a target was successfully sensed */ + UPROPERTY(EditAnywhere, Category = Output) + bool bHasTarget = false; + + /** True if an investigate location was successfully sensed */ + UPROPERTY(EditAnywhere, Category = Output) + bool bHasInvestigateLocation = false; + + /** Tag required on sensed actors */ + UPROPERTY(EditAnywhere, Category = Parameter) + FName SenseTag = FName("Player"); + + /** Line of sight cone half angle to consider a full sense */ + UPROPERTY(EditAnywhere, Category = Parameter) + float DirectLineOfSightCone = 85.0f; + + /** Strength of the last processed stimulus */ + UPROPERTY(EditAnywhere) + float LastStimulusStrength = 0.0f; +}; + +/** + * StateTree task to have an NPC process AI Perceptions and sense nearby enemies + */ +USTRUCT(meta=(DisplayName="Sense Enemies", Category="Shooter")) +struct FStateTreeSenseEnemiesTask : public FStateTreeTaskCommonBase +{ + GENERATED_BODY() + + /* Ensure we're using the correct instance data struct */ + using FInstanceDataType = FStateTreeSenseEnemiesInstanceData; + virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); } + + /** Runs when the owning state is entered */ + virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + + /** Runs when the owning state is ended */ + virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; + +#if WITH_EDITOR + virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override; +#endif // WITH_EDITOR +}; + +//////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.cpp b/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.cpp new file mode 100644 index 0000000..2b522c9 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.cpp @@ -0,0 +1,283 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterCharacter.h" +#include "ShooterWeapon.h" +#include "EnhancedInputComponent.h" +#include "Components/InputComponent.h" +#include "Components/PawnNoiseEmitterComponent.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/World.h" +#include "Camera/CameraComponent.h" +#include "TimerManager.h" +#include "ShooterGameMode.h" + +AShooterCharacter::AShooterCharacter() +{ + // create the noise emitter component + PawnNoiseEmitter = CreateDefaultSubobject(TEXT("Pawn Noise Emitter")); + + // configure movement + GetCharacterMovement()->RotationRate = FRotator(0.0f, 600.0f, 0.0f); +} + +void AShooterCharacter::BeginPlay() +{ + Super::BeginPlay(); + + // reset HP to max + CurrentHP = MaxHP; + + // update the HUD + OnDamaged.Broadcast(1.0f); +} + +void AShooterCharacter::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the respawn timer + GetWorld()->GetTimerManager().ClearTimer(RespawnTimer); +} + +void AShooterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + // base class handles move, aim and jump inputs + Super::SetupPlayerInputComponent(PlayerInputComponent); + + // Set up action bindings + if (UEnhancedInputComponent* EnhancedInputComponent = Cast(PlayerInputComponent)) + { + // Firing + EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &AShooterCharacter::DoStartFiring); + EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Completed, this, &AShooterCharacter::DoStopFiring); + + // Switch weapon + EnhancedInputComponent->BindAction(SwitchWeaponAction, ETriggerEvent::Triggered, this, &AShooterCharacter::DoSwitchWeapon); + } + +} + +float AShooterCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) +{ + // ignore if already dead + if (CurrentHP <= 0.0f) + { + return 0.0f; + } + + // Reduce HP + CurrentHP -= Damage; + + // Have we depleted HP? + if (CurrentHP <= 0.0f) + { + Die(); + } + + // update the HUD + OnDamaged.Broadcast(FMath::Max(0.0f, CurrentHP / MaxHP)); + + return Damage; +} + +void AShooterCharacter::DoStartFiring() +{ + // fire the current weapon + if (CurrentWeapon) + { + CurrentWeapon->StartFiring(); + } +} + +void AShooterCharacter::DoStopFiring() +{ + // stop firing the current weapon + if (CurrentWeapon) + { + CurrentWeapon->StopFiring(); + } +} + +void AShooterCharacter::DoSwitchWeapon() +{ + // ensure we have at least two weapons two switch between + if (OwnedWeapons.Num() > 1) + { + // deactivate the old weapon + CurrentWeapon->DeactivateWeapon(); + + // find the index of the current weapon in the owned list + int32 WeaponIndex = OwnedWeapons.Find(CurrentWeapon); + + // is this the last weapon? + if (WeaponIndex == OwnedWeapons.Num() - 1) + { + // loop back to the beginning of the array + WeaponIndex = 0; + } + else { + // select the next weapon index + ++WeaponIndex; + } + + // set the new weapon as current + CurrentWeapon = OwnedWeapons[WeaponIndex]; + + // activate the new weapon + CurrentWeapon->ActivateWeapon(); + } +} + +void AShooterCharacter::AttachWeaponMeshes(AShooterWeapon* Weapon) +{ + const FAttachmentTransformRules AttachmentRule(EAttachmentRule::SnapToTarget, false); + + // attach the weapon actor + Weapon->AttachToActor(this, AttachmentRule); + + // attach the weapon meshes + Weapon->GetFirstPersonMesh()->AttachToComponent(GetFirstPersonMesh(), AttachmentRule, FirstPersonWeaponSocket); + Weapon->GetThirdPersonMesh()->AttachToComponent(GetMesh(), AttachmentRule, FirstPersonWeaponSocket); + +} + +void AShooterCharacter::PlayFiringMontage(UAnimMontage* Montage) +{ + +} + +void AShooterCharacter::AddWeaponRecoil(float Recoil) +{ + // apply the recoil as pitch input + AddControllerPitchInput(Recoil); +} + +void AShooterCharacter::UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) +{ + OnBulletCountUpdated.Broadcast(MagazineSize, CurrentAmmo); +} + +FVector AShooterCharacter::GetWeaponTargetLocation() +{ + // trace ahead from the camera viewpoint + FHitResult OutHit; + + const FVector Start = GetFirstPersonCameraComponent()->GetComponentLocation(); + const FVector End = Start + (GetFirstPersonCameraComponent()->GetForwardVector() * MaxAimDistance); + + FCollisionQueryParams QueryParams; + QueryParams.AddIgnoredActor(this); + + GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams); + + // return either the impact point or the trace end + return OutHit.bBlockingHit ? OutHit.ImpactPoint : OutHit.TraceEnd; +} + +void AShooterCharacter::AddWeaponClass(const TSubclassOf& WeaponClass) +{ + // do we already own this weapon? + AShooterWeapon* OwnedWeapon = FindWeaponOfType(WeaponClass); + + if (!OwnedWeapon) + { + // spawn the new weapon + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = this; + SpawnParams.Instigator = this; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnParams.TransformScaleMethod = ESpawnActorScaleMethod::MultiplyWithRoot; + + AShooterWeapon* AddedWeapon = GetWorld()->SpawnActor(WeaponClass, GetActorTransform(), SpawnParams); + + if (AddedWeapon) + { + // add the weapon to the owned list + OwnedWeapons.Add(AddedWeapon); + + // if we have an existing weapon, deactivate it + if (CurrentWeapon) + { + CurrentWeapon->DeactivateWeapon(); + } + + // switch to the new weapon + CurrentWeapon = AddedWeapon; + CurrentWeapon->ActivateWeapon(); + } + } +} + +void AShooterCharacter::OnWeaponActivated(AShooterWeapon* Weapon) +{ + // update the bullet counter + OnBulletCountUpdated.Broadcast(Weapon->GetMagazineSize(), Weapon->GetBulletCount()); + + // set the character mesh AnimInstances + GetFirstPersonMesh()->SetAnimInstanceClass(Weapon->GetFirstPersonAnimInstanceClass()); + GetMesh()->SetAnimInstanceClass(Weapon->GetThirdPersonAnimInstanceClass()); +} + +void AShooterCharacter::OnWeaponDeactivated(AShooterWeapon* Weapon) +{ + // unused +} + +void AShooterCharacter::OnSemiWeaponRefire() +{ + // unused +} + +AShooterWeapon* AShooterCharacter::FindWeaponOfType(TSubclassOf WeaponClass) const +{ + // check each owned weapon + for (AShooterWeapon* Weapon : OwnedWeapons) + { + if (Weapon->IsA(WeaponClass)) + { + return Weapon; + } + } + + // weapon not found + return nullptr; + +} + +void AShooterCharacter::Die() +{ + // deactivate the weapon + if (IsValid(CurrentWeapon)) + { + CurrentWeapon->DeactivateWeapon(); + } + + // increment the team score + if (AShooterGameMode* GM = Cast(GetWorld()->GetAuthGameMode())) + { + GM->IncrementTeamScore(TeamByte); + } + + // stop character movement + GetCharacterMovement()->StopMovementImmediately(); + + // disable controls + DisableInput(nullptr); + + // reset the bullet counter UI + OnBulletCountUpdated.Broadcast(0, 0); + + // call the BP handler + BP_OnDeath(); + + // schedule character respawn + GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &AShooterCharacter::OnRespawn, RespawnTime, false); +} + +void AShooterCharacter::OnRespawn() +{ + // destroy the character to force the PC to respawn + Destroy(); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.h b/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.h new file mode 100644 index 0000000..aaadeb1 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterCharacter.h @@ -0,0 +1,166 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AudioVideoRecordCharacter.h" +#include "ShooterWeaponHolder.h" +#include "ShooterCharacter.generated.h" + +class AShooterWeapon; +class UInputAction; +class UInputComponent; +class UPawnNoiseEmitterComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FBulletCountUpdatedDelegate, int32, MagazineSize, int32, Bullets); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDamagedDelegate, float, LifePercent); + +/** + * A player controllable first person shooter character + * Manages a weapon inventory through the IShooterWeaponHolder interface + * Manages health and death + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterCharacter : public AAudioVideoRecordCharacter, public IShooterWeaponHolder +{ + GENERATED_BODY() + + /** AI Noise emitter component */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UPawnNoiseEmitterComponent* PawnNoiseEmitter; + +protected: + + /** Fire weapon input action */ + UPROPERTY(EditAnywhere, Category ="Input") + UInputAction* FireAction; + + /** Switch weapon input action */ + UPROPERTY(EditAnywhere, Category ="Input") + UInputAction* SwitchWeaponAction; + + /** Name of the first person mesh weapon socket */ + UPROPERTY(EditAnywhere, Category ="Weapons") + FName FirstPersonWeaponSocket = FName("HandGrip_R"); + + /** Name of the third person mesh weapon socket */ + UPROPERTY(EditAnywhere, Category ="Weapons") + FName ThirdPersonWeaponSocket = FName("HandGrip_R"); + + /** Max distance to use for aim traces */ + UPROPERTY(EditAnywhere, Category ="Aim", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm")) + float MaxAimDistance = 10000.0f; + + /** Max HP this character can have */ + UPROPERTY(EditAnywhere, Category="Health") + float MaxHP = 500.0f; + + /** Current HP remaining to this character */ + float CurrentHP = 0.0f; + + /** Team ID for this character*/ + UPROPERTY(EditAnywhere, Category="Team") + uint8 TeamByte = 0; + + /** List of weapons picked up by the character */ + TArray OwnedWeapons; + + /** Weapon currently equipped and ready to shoot with */ + TObjectPtr CurrentWeapon; + + UPROPERTY(EditAnywhere, Category ="Destruction", meta = (ClampMin = 0, ClampMax = 10, Units = "s")) + float RespawnTime = 5.0f; + + FTimerHandle RespawnTimer; + +public: + + /** Bullet count updated delegate */ + FBulletCountUpdatedDelegate OnBulletCountUpdated; + + /** Damaged delegate */ + FDamagedDelegate OnDamaged; + +public: + + /** Constructor */ + AShooterCharacter(); + +protected: + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Gameplay cleanup */ + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + + /** Set up input action bindings */ + virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override; + +public: + + /** Handle incoming damage */ + virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; + +public: + + /** Handles start firing input */ + UFUNCTION(BlueprintCallable, Category="Input") + void DoStartFiring(); + + /** Handles stop firing input */ + UFUNCTION(BlueprintCallable, Category="Input") + void DoStopFiring(); + + /** Handles switch weapon input */ + UFUNCTION(BlueprintCallable, Category="Input") + void DoSwitchWeapon(); + +public: + + //~Begin IShooterWeaponHolder interface + + /** Attaches a weapon's meshes to the owner */ + virtual void AttachWeaponMeshes(AShooterWeapon* Weapon) override; + + /** Plays the firing montage for the weapon */ + virtual void PlayFiringMontage(UAnimMontage* Montage) override; + + /** Applies weapon recoil to the owner */ + virtual void AddWeaponRecoil(float Recoil) override; + + /** Updates the weapon's HUD with the current ammo count */ + virtual void UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) override; + + /** Calculates and returns the aim location for the weapon */ + virtual FVector GetWeaponTargetLocation() override; + + /** Gives a weapon of this class to the owner */ + virtual void AddWeaponClass(const TSubclassOf& WeaponClass) override; + + /** Activates the passed weapon */ + virtual void OnWeaponActivated(AShooterWeapon* Weapon) override; + + /** Deactivates the passed weapon */ + virtual void OnWeaponDeactivated(AShooterWeapon* Weapon) override; + + /** Notifies the owner that the weapon cooldown has expired and it's ready to shoot again */ + virtual void OnSemiWeaponRefire() override; + + //~End IShooterWeaponHolder interface + +protected: + + /** Returns true if the character already owns a weapon of the given class */ + AShooterWeapon* FindWeaponOfType(TSubclassOf WeaponClass) const; + + /** Called when this character's HP is depleted */ + void Die(); + + /** Called to allow Blueprint code to react to this character's death */ + UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta = (DisplayName = "On Death")) + void BP_OnDeath(); + + /** Called from the respawn timer to destroy this character and force the PC to respawn */ + void OnRespawn(); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.cpp b/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.cpp new file mode 100644 index 0000000..2a56327 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.cpp @@ -0,0 +1,33 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/ShooterGameMode.h" +#include "ShooterUI.h" +#include "Kismet/GameplayStatics.h" +#include "Engine/World.h" + +void AShooterGameMode::BeginPlay() +{ + Super::BeginPlay(); + + // create the UI + ShooterUI = CreateWidget(UGameplayStatics::GetPlayerController(GetWorld(), 0), ShooterUIClass); + ShooterUI->AddToViewport(0); +} + +void AShooterGameMode::IncrementTeamScore(uint8 TeamByte) +{ + // retrieve the team score if any + int32 Score = 0; + if (int32* FoundScore = TeamScores.Find(TeamByte)) + { + Score = *FoundScore; + } + + // increment the score for the given team + ++Score; + TeamScores.Add(TeamByte, Score); + + // update the UI + ShooterUI->BP_UpdateScore(TeamByte, Score); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.h b/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.h new file mode 100644 index 0000000..ef4621b --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterGameMode.h @@ -0,0 +1,42 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "ShooterGameMode.generated.h" + +class UShooterUI; + +/** + * Simple GameMode for a first person shooter game + * Manages game UI + * Keeps track of team scores + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterGameMode : public AGameModeBase +{ + GENERATED_BODY() + +protected: + + /** Type of UI widget to spawn */ + UPROPERTY(EditAnywhere, Category="Shooter") + TSubclassOf ShooterUIClass; + + /** Pointer to the UI widget */ + TObjectPtr ShooterUI; + + /** Map of scores by team ID */ + TMap TeamScores; + +protected: + + /** Gameplay initialization */ + virtual void BeginPlay() override; + +public: + + /** Increases the score for the given team */ + void IncrementTeamScore(uint8 TeamByte); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.cpp b/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.cpp new file mode 100644 index 0000000..14d0d84 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.cpp @@ -0,0 +1,142 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "Variant_Shooter/ShooterPlayerController.h" +#include "EnhancedInputSubsystems.h" +#include "Engine/LocalPlayer.h" +#include "InputMappingContext.h" +#include "Kismet/GameplayStatics.h" +#include "GameFramework/PlayerStart.h" +#include "ShooterCharacter.h" +#include "ShooterBulletCounterUI.h" +#include "AudioVideoRecord.h" +#include "Widgets/Input/SVirtualJoystick.h" + +void AShooterPlayerController::BeginPlay() +{ + Super::BeginPlay(); + + // only spawn touch controls on local player controllers + if (IsLocalPlayerController()) + { + if (SVirtualJoystick::ShouldDisplayTouchInterface()) + { + // spawn the mobile controls widget + MobileControlsWidget = CreateWidget(this, MobileControlsWidgetClass); + + if (MobileControlsWidget) + { + // add the controls to the player screen + MobileControlsWidget->AddToPlayerScreen(0); + + } else { + + UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn mobile controls widget.")); + + } + } + + // create the bullet counter widget and add it to the screen + BulletCounterUI = CreateWidget(this, BulletCounterUIClass); + + if (BulletCounterUI) + { + BulletCounterUI->AddToPlayerScreen(0); + + } else { + + UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn bullet counter widget.")); + + } + + } +} + +void AShooterPlayerController::SetupInputComponent() +{ + // only add IMCs for local player controllers + if (IsLocalPlayerController()) + { + // add the input mapping contexts + if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(GetLocalPlayer())) + { + for (UInputMappingContext* CurrentContext : DefaultMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + + // only add these IMCs if we're not using mobile touch input + if (!SVirtualJoystick::ShouldDisplayTouchInterface()) + { + for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts) + { + Subsystem->AddMappingContext(CurrentContext, 0); + } + } + } + } +} + +void AShooterPlayerController::OnPossess(APawn* InPawn) +{ + Super::OnPossess(InPawn); + + // subscribe to the pawn's OnDestroyed delegate + InPawn->OnDestroyed.AddDynamic(this, &AShooterPlayerController::OnPawnDestroyed); + + // is this a shooter character? + if (AShooterCharacter* ShooterCharacter = Cast(InPawn)) + { + // add the player tag + ShooterCharacter->Tags.Add(PlayerPawnTag); + + // subscribe to the pawn's delegates + ShooterCharacter->OnBulletCountUpdated.AddDynamic(this, &AShooterPlayerController::OnBulletCountUpdated); + ShooterCharacter->OnDamaged.AddDynamic(this, &AShooterPlayerController::OnPawnDamaged); + + // force update the life bar + ShooterCharacter->OnDamaged.Broadcast(1.0f); + } +} + +void AShooterPlayerController::OnPawnDestroyed(AActor* DestroyedActor) +{ + // reset the bullet counter HUD + BulletCounterUI->BP_UpdateBulletCounter(0, 0); + + // find the player start + TArray ActorList; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList); + + if (ActorList.Num() > 0) + { + // select a random player start + AActor* RandomPlayerStart = ActorList[FMath::RandRange(0, ActorList.Num() - 1)]; + + // spawn a character at the player start + const FTransform SpawnTransform = RandomPlayerStart->GetActorTransform(); + + if (AShooterCharacter* RespawnedCharacter = GetWorld()->SpawnActor(CharacterClass, SpawnTransform)) + { + // possess the character + Possess(RespawnedCharacter); + } + } +} + +void AShooterPlayerController::OnBulletCountUpdated(int32 MagazineSize, int32 Bullets) +{ + // update the UI + if (BulletCounterUI) + { + BulletCounterUI->BP_UpdateBulletCounter(MagazineSize, Bullets); + } +} + +void AShooterPlayerController::OnPawnDamaged(float LifePercent) +{ + if (IsValid(BulletCounterUI)) + { + BulletCounterUI->BP_Damaged(LifePercent); + } +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.h b/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.h new file mode 100644 index 0000000..0ed70e5 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/ShooterPlayerController.h @@ -0,0 +1,77 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "ShooterPlayerController.generated.h" + +class UInputMappingContext; +class AShooterCharacter; +class UShooterBulletCounterUI; + +/** + * Simple PlayerController for a first person shooter game + * Manages input mappings + * Respawns the player pawn when it's destroyed + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterPlayerController : public APlayerController +{ + GENERATED_BODY() + +protected: + + /** Input mapping contexts for this player */ + UPROPERTY(EditAnywhere, Category="Input|Input Mappings") + TArray DefaultMappingContexts; + + /** Input Mapping Contexts */ + UPROPERTY(EditAnywhere, Category="Input|Input Mappings") + TArray MobileExcludedMappingContexts; + + /** Mobile controls widget to spawn */ + UPROPERTY(EditAnywhere, Category="Input|Touch Controls") + TSubclassOf MobileControlsWidgetClass; + + /** Pointer to the mobile controls widget */ + TObjectPtr MobileControlsWidget; + + /** Character class to respawn when the possessed pawn is destroyed */ + UPROPERTY(EditAnywhere, Category="Shooter|Respawn") + TSubclassOf CharacterClass; + + /** Type of bullet counter UI widget to spawn */ + UPROPERTY(EditAnywhere, Category="Shooter|UI") + TSubclassOf BulletCounterUIClass; + + /** Tag to grant the possessed pawn to flag it as the player */ + UPROPERTY(EditAnywhere, Category="Shooter|Player") + FName PlayerPawnTag = FName("Player"); + + /** Pointer to the bullet counter UI widget */ + TObjectPtr BulletCounterUI; + +protected: + + /** Gameplay Initialization */ + virtual void BeginPlay() override; + + /** Initialize input bindings */ + virtual void SetupInputComponent() override; + + /** Pawn initialization */ + virtual void OnPossess(APawn* InPawn) override; + + /** Called if the possessed pawn is destroyed */ + UFUNCTION() + void OnPawnDestroyed(AActor* DestroyedActor); + + /** Called when the bullet count on the possessed pawn is updated */ + UFUNCTION() + void OnBulletCountUpdated(int32 MagazineSize, int32 Bullets); + + /** Called when the possessed pawn is damaged */ + UFUNCTION() + void OnPawnDamaged(float LifePercent); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.cpp b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.cpp new file mode 100644 index 0000000..c6ccd39 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterBulletCounterUI.h" + diff --git a/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.h b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.h new file mode 100644 index 0000000..4611bc4 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterBulletCounterUI.h @@ -0,0 +1,26 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "ShooterBulletCounterUI.generated.h" + +/** + * Simple bullet counter UI widget for a first person shooter game + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API UShooterBulletCounterUI : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** Allows Blueprint to update sub-widgets with the new bullet count */ + UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta=(DisplayName = "UpdateBulletCounter")) + void BP_UpdateBulletCounter(int32 MagazineSize, int32 BulletCount); + + /** Allows Blueprint to update sub-widgets with the new life total and play a damage effect on the HUD */ + UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta=(DisplayName = "Damaged")) + void BP_Damaged(float LifePercent); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.cpp b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.cpp new file mode 100644 index 0000000..c656497 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterUI.h" + diff --git a/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.h b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.h new file mode 100644 index 0000000..3502d90 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/UI/ShooterUI.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "ShooterUI.generated.h" + +/** + * Simple scoreboard UI for a first person shooter game + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API UShooterUI : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** Allows Blueprint to update score sub-widgets */ + UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta = (DisplayName = "Update Score")) + void BP_UpdateScore(uint8 TeamByte, int32 Score); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.cpp b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.cpp new file mode 100644 index 0000000..e0205d3 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.cpp @@ -0,0 +1,108 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterPickup.h" +#include "Components/SceneComponent.h" +#include "Components/SphereComponent.h" +#include "Components/StaticMeshComponent.h" +#include "ShooterWeaponHolder.h" +#include "ShooterWeapon.h" +#include "Engine/World.h" +#include "TimerManager.h" + +AShooterPickup::AShooterPickup() +{ + PrimaryActorTick.bCanEverTick = true; + + // create the root + RootComponent = CreateDefaultSubobject(TEXT("Root")); + + // create the collision sphere + SphereCollision = CreateDefaultSubobject(TEXT("Sphere Collision")); + SphereCollision->SetupAttachment(RootComponent); + + SphereCollision->SetRelativeLocation(FVector(0.0f, 0.0f, 84.0f)); + SphereCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + SphereCollision->SetCollisionObjectType(ECC_WorldStatic); + SphereCollision->SetCollisionResponseToAllChannels(ECR_Ignore); + SphereCollision->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); + SphereCollision->bFillCollisionUnderneathForNavmesh = true; + + // subscribe to the collision overlap on the sphere + SphereCollision->OnComponentBeginOverlap.AddDynamic(this, &AShooterPickup::OnOverlap); + + // create the mesh + Mesh = CreateDefaultSubobject(TEXT("Mesh")); + Mesh->SetupAttachment(SphereCollision); + + Mesh->SetCollisionProfileName(FName("NoCollision")); +} + +void AShooterPickup::OnConstruction(const FTransform& Transform) +{ + Super::OnConstruction(Transform); + + if (FWeaponTableRow* WeaponData = WeaponType.GetRow(FString())) + { + // set the mesh + Mesh->SetStaticMesh(WeaponData->StaticMesh.LoadSynchronous()); + } +} + +void AShooterPickup::BeginPlay() +{ + Super::BeginPlay(); + + if (FWeaponTableRow* WeaponData = WeaponType.GetRow(FString())) + { + // copy the weapon class + WeaponClass = WeaponData->WeaponToSpawn; + } +} + +void AShooterPickup::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the respawn timer + GetWorld()->GetTimerManager().ClearTimer(RespawnTimer); +} + +void AShooterPickup::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) +{ + // have we collided against a weapon holder? + if (IShooterWeaponHolder* WeaponHolder = Cast(OtherActor)) + { + WeaponHolder->AddWeaponClass(WeaponClass); + + // hide this mesh + SetActorHiddenInGame(true); + + // disable collision + SetActorEnableCollision(false); + + // disable ticking + SetActorTickEnabled(false); + + // schedule the respawn + GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &AShooterPickup::RespawnPickup, RespawnTime, false); + } +} + +void AShooterPickup::RespawnPickup() +{ + // unhide this pickup + SetActorHiddenInGame(false); + + // call the BP handler + BP_OnRespawn(); +} + +void AShooterPickup::FinishRespawn() +{ + // enable collision + SetActorEnableCollision(true); + + // enable tick + SetActorTickEnabled(true); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.h b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.h new file mode 100644 index 0000000..9a2c92f --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterPickup.h @@ -0,0 +1,96 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "Engine/DataTable.h" +#include "Engine/StaticMesh.h" +#include "ShooterPickup.generated.h" + +class USphereComponent; +class UPrimitiveComponent; +class AShooterWeapon; + +/** + * Holds information about a type of weapon pickup + */ +USTRUCT(BlueprintType) +struct FWeaponTableRow : public FTableRowBase +{ + GENERATED_BODY() + + /** Mesh to display on the pickup */ + UPROPERTY(EditAnywhere) + TSoftObjectPtr StaticMesh; + + /** Weapon class to grant on pickup */ + UPROPERTY(EditAnywhere) + TSubclassOf WeaponToSpawn; +}; + +/** + * Simple shooter game weapon pickup + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterPickup : public AActor +{ + GENERATED_BODY() + + /** Collision sphere */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USphereComponent* SphereCollision; + + /** Weapon pickup mesh. Its mesh asset is set from the weapon data table */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UStaticMeshComponent* Mesh; + +protected: + + /** Data on the type of picked weapon and visuals of this pickup */ + UPROPERTY(EditAnywhere, Category="Pickup") + FDataTableRowHandle WeaponType; + + /** Type to weapon to grant on pickup. Set from the weapon data table. */ + TSubclassOf WeaponClass; + + /** Time to wait before respawning this pickup */ + UPROPERTY(EditAnywhere, Category="Pickup", meta = (ClampMin = 0, ClampMax = 120, Units = "s")) + float RespawnTime = 4.0f; + + /** Timer to respawn the pickup */ + FTimerHandle RespawnTimer; + +public: + + /** Constructor */ + AShooterPickup(); + +protected: + + /** Native construction script */ + virtual void OnConstruction(const FTransform& Transform) override; + + /** Gameplay Initialization*/ + virtual void BeginPlay() override; + + /** Gameplay cleanup */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** Handles collision overlap */ + UFUNCTION() + virtual void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); + +protected: + + /** Called when it's time to respawn this pickup */ + void RespawnPickup(); + + /** Passes control to Blueprint to animate the pickup respawn. Should end by calling FinishRespawn */ + UFUNCTION(BlueprintImplementableEvent, Category="Pickup", meta = (DisplayName = "OnRespawn")) + void BP_OnRespawn(); + + /** Enables this pickup after respawning */ + UFUNCTION(BlueprintCallable, Category="Pickup") + void FinishRespawn(); +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.cpp b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.cpp new file mode 100644 index 0000000..cd075c0 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.cpp @@ -0,0 +1,167 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterProjectile.h" +#include "Components/SphereComponent.h" +#include "GameFramework/ProjectileMovementComponent.h" +#include "GameFramework/Character.h" +#include "Kismet/GameplayStatics.h" +#include "GameFramework/DamageType.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/Controller.h" +#include "Engine/OverlapResult.h" +#include "Engine/World.h" +#include "TimerManager.h" + +AShooterProjectile::AShooterProjectile() +{ + PrimaryActorTick.bCanEverTick = true; + + // create the collision component and assign it as the root + RootComponent = CollisionComponent = CreateDefaultSubobject(TEXT("Collision Component")); + + CollisionComponent->SetSphereRadius(16.0f); + CollisionComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + CollisionComponent->SetCollisionResponseToAllChannels(ECR_Block); + CollisionComponent->CanCharacterStepUpOn = ECanBeCharacterBase::ECB_No; + + // create the projectile movement component. No need to attach it because it's not a Scene Component + ProjectileMovement = CreateDefaultSubobject(TEXT("Projectile Movement")); + + ProjectileMovement->InitialSpeed = 3000.0f; + ProjectileMovement->MaxSpeed = 3000.0f; + ProjectileMovement->bShouldBounce = true; + + // set the default damage type + HitDamageType = UDamageType::StaticClass(); +} + +void AShooterProjectile::BeginPlay() +{ + Super::BeginPlay(); + + // ignore the pawn that shot this projectile + CollisionComponent->IgnoreActorWhenMoving(GetInstigator(), true); +} + +void AShooterProjectile::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the destruction timer + GetWorld()->GetTimerManager().ClearTimer(DestructionTimer); +} + +void AShooterProjectile::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) +{ + // ignore if we've already hit something else + if (bHit) + { + return; + } + + bHit = true; + + // disable collision on the projectile + CollisionComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + // make AI perception noise + MakeNoise(NoiseLoudness, GetInstigator(), GetActorLocation(), NoiseRange, NoiseTag); + + if (bExplodeOnHit) + { + + // apply explosion damage centered on the projectile + ExplosionCheck(GetActorLocation()); + + } else { + + // single hit projectile. Process the collided actor + ProcessHit(Other, OtherComp, Hit.ImpactPoint, -Hit.ImpactNormal); + + } + + // pass control to BP for any extra effects + BP_OnProjectileHit(Hit); + + // check if we should schedule deferred destruction of the projectile + if (DeferredDestructionTime > 0.0f) + { + GetWorld()->GetTimerManager().SetTimer(DestructionTimer, this, &AShooterProjectile::OnDeferredDestruction, DeferredDestructionTime, false); + + } else { + + // destroy the projectile right away + Destroy(); + } +} + +void AShooterProjectile::ExplosionCheck(const FVector& ExplosionCenter) +{ + // do a sphere overlap check look for nearby actors to damage + TArray Overlaps; + + FCollisionShape OverlapShape; + OverlapShape.SetSphere(ExplosionRadius); + + FCollisionObjectQueryParams ObjectParams; + ObjectParams.AddObjectTypesToQuery(ECC_Pawn); + ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic); + ObjectParams.AddObjectTypesToQuery(ECC_PhysicsBody); + + FCollisionQueryParams QueryParams; + QueryParams.AddIgnoredActor(this); + if (!bDamageOwner) + { + QueryParams.AddIgnoredActor(GetInstigator()); + } + + GetWorld()->OverlapMultiByObjectType(Overlaps, ExplosionCenter, FQuat::Identity, ObjectParams, OverlapShape, QueryParams); + + TArray DamagedActors; + + // process the overlap results + for (const FOverlapResult& CurrentOverlap : Overlaps) + { + // overlaps may return the same actor multiple times per each component overlapped + // ensure we only damage each actor once by adding it to a damaged list + if (DamagedActors.Find(CurrentOverlap.GetActor()) == INDEX_NONE) + { + DamagedActors.Add(CurrentOverlap.GetActor()); + + // apply physics force away from the explosion + const FVector& ExplosionDir = CurrentOverlap.GetActor()->GetActorLocation() - GetActorLocation(); + + // push and/or damage the overlapped actor + ProcessHit(CurrentOverlap.GetActor(), CurrentOverlap.GetComponent(), GetActorLocation(), ExplosionDir.GetSafeNormal()); + } + + } +} + +void AShooterProjectile::ProcessHit(AActor* HitActor, UPrimitiveComponent* HitComp, const FVector& HitLocation, const FVector& HitDirection) +{ + // have we hit a character? + if (ACharacter* HitCharacter = Cast(HitActor)) + { + // ignore the owner of this projectile + if (HitCharacter != GetOwner() || bDamageOwner) + { + // apply damage to the character + UGameplayStatics::ApplyDamage(HitCharacter, HitDamage, GetInstigator()->GetController(), this, HitDamageType); + } + } + + // have we hit a physics object? + if (HitComp->IsSimulatingPhysics()) + { + // give some physics impulse to the object + HitComp->AddImpulseAtLocation(HitDirection * PhysicsForce, HitLocation); + } +} + +void AShooterProjectile::OnDeferredDestruction() +{ + // destroy this actor + Destroy(); +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.h b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.h new file mode 100644 index 0000000..8ef9048 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterProjectile.h @@ -0,0 +1,109 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ShooterProjectile.generated.h" + +class USphereComponent; +class UProjectileMovementComponent; +class ACharacter; +class UPrimitiveComponent; + +/** + * Simple projectile class for a first person shooter game + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterProjectile : public AActor +{ + GENERATED_BODY() + + /** Provides collision detection for the projectile */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USphereComponent* CollisionComponent; + + /** Handles movement for the projectile */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UProjectileMovementComponent* ProjectileMovement; + +protected: + + /** Loudness of the AI perception noise done by this projectile on hit */ + UPROPERTY(EditAnywhere, Category="Projectile|Noise", meta = (ClampMin = 0, ClampMax = 100)) + float NoiseLoudness = 3.0f; + + /** Range of the AI perception noise done by this projectile on hit */ + UPROPERTY(EditAnywhere, Category="Projectile|Noise", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm")) + float NoiseRange = 3000.0f; + + /** Tag of the AI perception noise done by this projectile on hit */ + UPROPERTY(EditAnywhere, Category="Noise") + FName NoiseTag = FName("Projectile"); + + /** Physics force to apply on hit */ + UPROPERTY(EditAnywhere, Category="Projectile|Hit", meta = (ClampMin = 0, ClampMax = 50000)) + float PhysicsForce = 100.0f; + + /** Damage to apply on hit */ + UPROPERTY(EditAnywhere, Category="Projectile|Hit", meta = (ClampMin = 0, ClampMax = 100)) + float HitDamage = 25.0f; + + /** Type of damage to apply. Can be used to represent specific types of damage such as fire, explosion, etc. */ + UPROPERTY(EditAnywhere, Category="Projectile|Hit") + TSubclassOf HitDamageType; + + /** If true, the projectile can damage the character that shot it */ + UPROPERTY(EditAnywhere, Category="Projectile|Hit") + bool bDamageOwner = false; + + /** If true, the projectile will explode and apply radial damage to all actors in range */ + UPROPERTY(EditAnywhere, Category="Projectile|Explosion") + bool bExplodeOnHit = false; + + /** Max distance for actors to be affected by explosion damage */ + UPROPERTY(EditAnywhere, Category="Projectile|Explosion", meta = (ClampMin = 0, ClampMax = 5000, Units = "cm")) + float ExplosionRadius = 500.0f; + + /** If true, this projectile has already hit another surface */ + bool bHit = false; + + /** How long to wait after a hit before destroying this projectile */ + UPROPERTY(EditAnywhere, Category="Projectile|Destruction", meta = (ClampMin = 0, ClampMax = 10, Units = "s")) + float DeferredDestructionTime = 5.0f; + + /** Timer to handle deferred destruction of this projectile */ + FTimerHandle DestructionTimer; + +public: + + /** Constructor */ + AShooterProjectile(); + +protected: + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Gameplay cleanup */ + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + + /** Handles collision */ + virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override; + +protected: + + /** Looks up actors within the explosion radius and damages them */ + void ExplosionCheck(const FVector& ExplosionCenter); + + /** Processes a projectile hit for the given actor */ + void ProcessHit(AActor* HitActor, UPrimitiveComponent* HitComp, const FVector& HitLocation, const FVector& HitDirection); + + /** Passes control to Blueprint to implement any effects on hit. */ + UFUNCTION(BlueprintImplementableEvent, Category="Projectile", meta = (DisplayName = "On Projectile Hit")) + void BP_OnProjectileHit(const FHitResult& Hit); + + /** Called from the destruction timer to destroy this projectile */ + void OnDeferredDestruction(); + +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.cpp b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.cpp new file mode 100644 index 0000000..d53d0b4 --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.cpp @@ -0,0 +1,218 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterWeapon.h" +#include "Kismet/KismetMathLibrary.h" +#include "Engine/World.h" +#include "ShooterProjectile.h" +#include "ShooterWeaponHolder.h" +#include "Components/SceneComponent.h" +#include "TimerManager.h" +#include "Animation/AnimInstance.h" +#include "Components/SkeletalMeshComponent.h" +#include "GameFramework/Pawn.h" + +AShooterWeapon::AShooterWeapon() +{ + PrimaryActorTick.bCanEverTick = true; + + // create the root + RootComponent = CreateDefaultSubobject(TEXT("Root")); + + // create the first person mesh + FirstPersonMesh = CreateDefaultSubobject(TEXT("First Person Mesh")); + FirstPersonMesh->SetupAttachment(RootComponent); + + FirstPersonMesh->SetCollisionProfileName(FName("NoCollision")); + FirstPersonMesh->SetFirstPersonPrimitiveType(EFirstPersonPrimitiveType::FirstPerson); + FirstPersonMesh->bOnlyOwnerSee = true; + + // create the third person mesh + ThirdPersonMesh = CreateDefaultSubobject(TEXT("Third Person Mesh")); + ThirdPersonMesh->SetupAttachment(RootComponent); + + ThirdPersonMesh->SetCollisionProfileName(FName("NoCollision")); + ThirdPersonMesh->SetFirstPersonPrimitiveType(EFirstPersonPrimitiveType::WorldSpaceRepresentation); + ThirdPersonMesh->bOwnerNoSee = true; +} + +void AShooterWeapon::BeginPlay() +{ + Super::BeginPlay(); + + // subscribe to the owner's destroyed delegate + GetOwner()->OnDestroyed.AddDynamic(this, &AShooterWeapon::OnOwnerDestroyed); + + // cast the weapon owner + WeaponOwner = Cast(GetOwner()); + PawnOwner = Cast(GetOwner()); + + // fill the first ammo clip + CurrentBullets = MagazineSize; + + // attach the meshes to the owner + WeaponOwner->AttachWeaponMeshes(this); +} + +void AShooterWeapon::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + // clear the refire timer + GetWorld()->GetTimerManager().ClearTimer(RefireTimer); +} + +void AShooterWeapon::OnOwnerDestroyed(AActor* DestroyedActor) +{ + // ensure this weapon is destroyed when the owner is destroyed + Destroy(); +} + +void AShooterWeapon::ActivateWeapon() +{ + // unhide this weapon + SetActorHiddenInGame(false); + + // notify the owner + WeaponOwner->OnWeaponActivated(this); +} + +void AShooterWeapon::DeactivateWeapon() +{ + // ensure we're no longer firing this weapon while deactivated + StopFiring(); + + // hide the weapon + SetActorHiddenInGame(true); + + // notify the owner + WeaponOwner->OnWeaponDeactivated(this); +} + +void AShooterWeapon::StartFiring() +{ + // raise the firing flag + bIsFiring = true; + + // check how much time has passed since we last shot + // this may be under the refire rate if the weapon shoots slow enough and the player is spamming the trigger + const float TimeSinceLastShot = GetWorld()->GetTimeSeconds() - TimeOfLastShot; + + if (TimeSinceLastShot > RefireRate) + { + // fire the weapon right away + Fire(); + + } else { + + // if we're full auto, schedule the next shot + if (bFullAuto) + { + GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::Fire, TimeSinceLastShot, false); + } + + } +} + +void AShooterWeapon::StopFiring() +{ + // lower the firing flag + bIsFiring = false; + + // clear the refire timer + GetWorld()->GetTimerManager().ClearTimer(RefireTimer); +} + +void AShooterWeapon::Fire() +{ + // ensure the player still wants to fire. They may have let go of the trigger + if (!bIsFiring) + { + return; + } + + // fire a projectile at the target + FireProjectile(WeaponOwner->GetWeaponTargetLocation()); + + // update the time of our last shot + TimeOfLastShot = GetWorld()->GetTimeSeconds(); + + // make noise so the AI perception system can hear us + MakeNoise(ShotLoudness, PawnOwner, PawnOwner->GetActorLocation(), ShotNoiseRange, ShotNoiseTag); + + // are we full auto? + if (bFullAuto) + { + // schedule the next shot + GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::Fire, RefireRate, false); + } else { + + // for semi-auto weapons, schedule the cooldown notification + GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::FireCooldownExpired, RefireRate, false); + + } +} + +void AShooterWeapon::FireCooldownExpired() +{ + // notify the owner + WeaponOwner->OnSemiWeaponRefire(); +} + +void AShooterWeapon::FireProjectile(const FVector& TargetLocation) +{ + // get the projectile transform + FTransform ProjectileTransform = CalculateProjectileSpawnTransform(TargetLocation); + + // spawn the projectile + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnParams.TransformScaleMethod = ESpawnActorScaleMethod::OverrideRootScale; + SpawnParams.Owner = GetOwner(); + SpawnParams.Instigator = PawnOwner; + + AShooterProjectile* Projectile = GetWorld()->SpawnActor(ProjectileClass, ProjectileTransform, SpawnParams); + + // play the firing montage + WeaponOwner->PlayFiringMontage(FiringMontage); + + // add recoil + WeaponOwner->AddWeaponRecoil(FiringRecoil); + + // consume bullets + --CurrentBullets; + + // if the clip is depleted, reload it + if (CurrentBullets <= 0) + { + CurrentBullets = MagazineSize; + } + + // update the weapon HUD + WeaponOwner->UpdateWeaponHUD(CurrentBullets, MagazineSize); +} + +FTransform AShooterWeapon::CalculateProjectileSpawnTransform(const FVector& TargetLocation) const +{ + // find the muzzle location + const FVector MuzzleLoc = FirstPersonMesh->GetSocketLocation(MuzzleSocketName); + + // calculate the spawn location ahead of the muzzle + const FVector SpawnLoc = MuzzleLoc + ((TargetLocation - MuzzleLoc).GetSafeNormal() * MuzzleOffset); + + // find the aim rotation vector while applying some variance to the target + const FRotator AimRot = UKismetMathLibrary::FindLookAtRotation(SpawnLoc, TargetLocation + (UKismetMathLibrary::RandomUnitVector() * AimVariance)); + + // return the built transform + return FTransform(AimRot, SpawnLoc, FVector::OneVector); +} + +const TSubclassOf& AShooterWeapon::GetFirstPersonAnimInstanceClass() const +{ + return FirstPersonAnimInstanceClass; +} + +const TSubclassOf& AShooterWeapon::GetThirdPersonAnimInstanceClass() const +{ + return ThirdPersonAnimInstanceClass; +} diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.h b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.h new file mode 100644 index 0000000..e25727a --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeapon.h @@ -0,0 +1,180 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ShooterWeaponHolder.h" +#include "Animation/AnimInstance.h" +#include "ShooterWeapon.generated.h" + +class IShooterWeaponHolder; +class AShooterProjectile; +class USkeletalMeshComponent; +class UAnimMontage; +class UAnimInstance; + +/** + * Base class for a simple first person shooter weapon + * Provides both first person and third person perspective meshes + * Handles ammo and firing logic + * Interacts with the weapon owner through the ShooterWeaponHolder interface + */ +UCLASS(abstract) +class AUDIOVIDEORECORD_API AShooterWeapon : public AActor +{ + GENERATED_BODY() + + /** First person perspective mesh */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USkeletalMeshComponent* FirstPersonMesh; + + /** Third person perspective mesh */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + USkeletalMeshComponent* ThirdPersonMesh; + +protected: + + /** Cast pointer to the weapon owner */ + IShooterWeaponHolder* WeaponOwner; + + /** Type of projectiles this weapon will shoot */ + UPROPERTY(EditAnywhere, Category="Ammo") + TSubclassOf ProjectileClass; + + /** Number of bullets in a magazine */ + UPROPERTY(EditAnywhere, Category="Ammo", meta = (ClampMin = 0, ClampMax = 100)) + int32 MagazineSize = 10; + + /** Number of bullets in the current magazine */ + int32 CurrentBullets = 0; + + /** Animation montage to play when firing this weapon */ + UPROPERTY(EditAnywhere, Category="Animation") + UAnimMontage* FiringMontage; + + /** AnimInstance class to set for the first person character mesh when this weapon is active */ + UPROPERTY(EditAnywhere, Category="Animation") + TSubclassOf FirstPersonAnimInstanceClass; + + /** AnimInstance class to set for the third person character mesh when this weapon is active */ + UPROPERTY(EditAnywhere, Category="Animation") + TSubclassOf ThirdPersonAnimInstanceClass; + + /** Cone half-angle for variance while aiming */ + UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 90, Units = "Degrees")) + float AimVariance = 0.0f; + + /** Amount of firing recoil to apply to the owner */ + UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 100)) + float FiringRecoil = 0.0f; + + /** Name of the first person muzzle socket where projectiles will spawn */ + UPROPERTY(EditAnywhere, Category="Aim") + FName MuzzleSocketName; + + /** Distance ahead of the muzzle that bullets will spawn at */ + UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm")) + float MuzzleOffset = 10.0f; + + /** If true, this weapon will automatically fire at the refire rate */ + UPROPERTY(EditAnywhere, Category="Refire") + bool bFullAuto = false; + + /** Time between shots for this weapon. Affects both full auto and semi auto modes */ + UPROPERTY(EditAnywhere, Category="Refire", meta = (ClampMin = 0, ClampMax = 5, Units = "s")) + float RefireRate = 0.5f; + + /** Game time of last shot fired, used to enforce refire rate on semi auto */ + float TimeOfLastShot = 0.0f; + + /** If true, the weapon is currently firing */ + bool bIsFiring = false; + + /** Timer to handle full auto refiring */ + FTimerHandle RefireTimer; + + /** Cast pawn pointer to the owner for AI perception system interactions */ + TObjectPtr PawnOwner; + + /** Loudness of the shot for AI perception system interactions */ + UPROPERTY(EditAnywhere, Category="Perception", meta = (ClampMin = 0, ClampMax = 100)) + float ShotLoudness = 1.0f; + + /** Max range of shot AI perception noise */ + UPROPERTY(EditAnywhere, Category="Perception", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm")) + float ShotNoiseRange = 3000.0f; + + /** Tag to apply to noise generated by shooting this weapon */ + UPROPERTY(EditAnywhere, Category="Perception") + FName ShotNoiseTag = FName("Shot"); + +public: + + /** Constructor */ + AShooterWeapon(); + +protected: + + /** Gameplay initialization */ + virtual void BeginPlay() override; + + /** Gameplay Cleanup */ + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + +protected: + + /** Called when the weapon's owner is destroyed */ + UFUNCTION() + void OnOwnerDestroyed(AActor* DestroyedActor); + +public: + + /** Activates this weapon and gets it ready to fire */ + void ActivateWeapon(); + + /** Deactivates this weapon */ + void DeactivateWeapon(); + + /** Start firing this weapon */ + void StartFiring(); + + /** Stop firing this weapon */ + void StopFiring(); + +protected: + + /** Fire the weapon */ + virtual void Fire(); + + /** Called when the refire rate time has passed while shooting semi auto weapons */ + void FireCooldownExpired(); + + /** Fire a projectile towards the target location */ + virtual void FireProjectile(const FVector& TargetLocation); + + /** Calculates the spawn transform for projectiles shot by this weapon */ + FTransform CalculateProjectileSpawnTransform(const FVector& TargetLocation) const; + +public: + + /** Returns the first person mesh */ + UFUNCTION(BlueprintPure, Category="Weapon") + USkeletalMeshComponent* GetFirstPersonMesh() const { return FirstPersonMesh; }; + + /** Returns the third person mesh */ + UFUNCTION(BlueprintPure, Category="Weapon") + USkeletalMeshComponent* GetThirdPersonMesh() const { return ThirdPersonMesh; }; + + /** Returns the first person anim instance class */ + const TSubclassOf& GetFirstPersonAnimInstanceClass() const; + + /** Returns the third person anim instance class */ + const TSubclassOf& GetThirdPersonAnimInstanceClass() const; + + /** Returns the magazine size */ + int32 GetMagazineSize() const { return MagazineSize; }; + + /** Returns the current bullet count */ + int32 GetBulletCount() const { return CurrentBullets; } +}; diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.cpp b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.cpp new file mode 100644 index 0000000..5b4675e --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.cpp @@ -0,0 +1,6 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterWeaponHolder.h" + +// Add default functionality here for any IShooterWeaponHolder functions that are not pure virtual. diff --git a/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.h b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.h new file mode 100644 index 0000000..df576dd --- /dev/null +++ b/Source/AudioVideoRecord/Variant_Shooter/Weapons/ShooterWeaponHolder.h @@ -0,0 +1,55 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "ShooterWeaponHolder.generated.h" + +class AShooterWeapon; +class UAnimMontage; + + +// This class does not need to be modified. +UINTERFACE(MinimalAPI) +class UShooterWeaponHolder : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Common interface for Shooter Game weapon holder classes + */ +class AUDIOVIDEORECORD_API IShooterWeaponHolder +{ + GENERATED_BODY() + +public: + + /** Attaches a weapon's meshes to the owner */ + virtual void AttachWeaponMeshes(AShooterWeapon* Weapon) = 0; + + /** Plays the firing montage for the weapon */ + virtual void PlayFiringMontage(UAnimMontage* Montage) = 0; + + /** Applies weapon recoil to the owner */ + virtual void AddWeaponRecoil(float Recoil) = 0; + + /** Updates the weapon's HUD with the current ammo count */ + virtual void UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) = 0; + + /** Calculates and returns the aim location for the weapon */ + virtual FVector GetWeaponTargetLocation() = 0; + + /** Gives a weapon of this class to the owner */ + virtual void AddWeaponClass(const TSubclassOf& WeaponClass) = 0; + + /** Activates the passed weapon */ + virtual void OnWeaponActivated(AShooterWeapon* Weapon) = 0; + + /** Deactivates the passed weapon */ + virtual void OnWeaponDeactivated(AShooterWeapon* Weapon) = 0; + + /** Notifies the owner that the weapon cooldown has expired and it's ready to shoot again */ + virtual void OnSemiWeaponRefire() = 0; +}; diff --git a/Source/AudioVideoRecordEditor.Target.cs b/Source/AudioVideoRecordEditor.Target.cs new file mode 100644 index 0000000..da074f0 --- /dev/null +++ b/Source/AudioVideoRecordEditor.Target.cs @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AudioVideoRecordEditorTarget : TargetRules +{ + public AudioVideoRecordEditorTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V5; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; + ExtraModuleNames.Add("AudioVideoRecord"); + } +}