commit 3e434877e8adcd98a15e19e26cfee6ad8840fa3b Author: 不明不惑 Date: Tue Mar 3 01:23:02 2026 +0800 第一次提交 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fbec821 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,91 @@ +[*.{cpp,h}] + +# Naming convention rules (note: currently need to be ordered from more to less specific) + +cpp_naming_rule.aactor_prefixed.symbols = aactor_class +cpp_naming_rule.aactor_prefixed.style = aactor_style + +cpp_naming_rule.swidget_prefixed.symbols = swidget_class +cpp_naming_rule.swidget_prefixed.style = swidget_style + +cpp_naming_rule.uobject_prefixed.symbols = uobject_class +cpp_naming_rule.uobject_prefixed.style = uobject_style + +cpp_naming_rule.booleans_prefixed.symbols = boolean_vars +cpp_naming_rule.booleans_prefixed.style = boolean_style + +cpp_naming_rule.structs_prefixed.symbols = structs +cpp_naming_rule.structs_prefixed.style = unreal_engine_structs + +cpp_naming_rule.enums_prefixed.symbols = enums +cpp_naming_rule.enums_prefixed.style = unreal_engine_enums + +cpp_naming_rule.templates_prefixed.symbols = templates +cpp_naming_rule.templates_prefixed.style = unreal_engine_templates + +cpp_naming_rule.general_names.symbols = all_symbols +cpp_naming_rule.general_names.style = unreal_engine_default + +# Naming convention symbols + +cpp_naming_symbols.aactor_class.applicable_kinds = class +cpp_naming_symbols.aactor_class.applicable_type = AActor + +cpp_naming_symbols.swidget_class.applicable_kinds = class +cpp_naming_symbols.swidget_class.applicable_type = SWidget + +cpp_naming_symbols.uobject_class.applicable_kinds = class +cpp_naming_symbols.uobject_class.applicable_type = UObject + +cpp_naming_symbols.boolean_vars.applicable_kinds = local,parameter,field +cpp_naming_symbols.boolean_vars.applicable_type = bool + +cpp_naming_symbols.enums.applicable_kinds = enum + +cpp_naming_symbols.templates.applicable_kinds = template_class + +cpp_naming_symbols.structs.applicable_kinds = struct + +cpp_naming_symbols.all_symbols.applicable_kinds = * + +# Naming convention styles + +cpp_naming_style.unreal_engine_default.capitalization = pascal_case +cpp_naming_style.unreal_engine_default.required_prefix = +cpp_naming_style.unreal_engine_default.required_suffix = +cpp_naming_style.unreal_engine_default.word_separator = + +cpp_naming_style.unreal_engine_enums.capitalization = pascal_case +cpp_naming_style.unreal_engine_enums.required_prefix = E +cpp_naming_style.unreal_engine_enums.required_suffix = +cpp_naming_style.unreal_engine_enums.word_separator = + +cpp_naming_style.unreal_engine_templates.capitalization = pascal_case +cpp_naming_style.unreal_engine_templates.required_prefix = T +cpp_naming_style.unreal_engine_templates.required_suffix = +cpp_naming_style.unreal_engine_templates.word_separator = + +cpp_naming_style.unreal_engine_structs.capitalization = pascal_case +cpp_naming_style.unreal_engine_structs.required_prefix = F +cpp_naming_style.unreal_engine_structs.required_suffix = +cpp_naming_style.unreal_engine_structs.word_separator = + +cpp_naming_style.uobject_style.capitalization = pascal_case +cpp_naming_style.uobject_style.required_prefix = U +cpp_naming_style.uobject_style.required_suffix = +cpp_naming_style.uobject_style.word_separator = + +cpp_naming_style.aactor_style.capitalization = pascal_case +cpp_naming_style.aactor_style.required_prefix = A +cpp_naming_style.aactor_style.required_suffix = +cpp_naming_style.aactor_style.word_separator = + +cpp_naming_style.swidget_style.capitalization = pascal_case +cpp_naming_style.swidget_style.required_prefix = S +cpp_naming_style.swidget_style.required_suffix = +cpp_naming_style.swidget_style.word_separator = + +cpp_naming_style.boolean_style.capitalization = pascal_case +cpp_naming_style.boolean_style.required_prefix = b +cpp_naming_style.boolean_style.required_suffix = +cpp_naming_style.boolean_style.word_separator = \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f59388 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Unreal Engine - minimal tracked project +# Goal: only keep source/config/plugins + required game content (Content/AGame) + +######################## +# Always keep (tracked) +######################## +!.gitignore + +# Project files +!*.uproject +!*.uplugin + +# Config + source +!Config/** +!Source/** +!Plugins/** + +######################## +# Content: keep only required game content +######################## +# Ignore all cooked/asset content by default... +Content/** +# ...but keep the minimal required folders +!Content/AGame/** + +# (Optional) keep top-level Content structure files if you use them +!Content/*.uprojectdirs + +######################## +# Build / generated / cache (ignore) +######################## +Binaries/** +Build/** +DerivedDataCache/** +Intermediate/** +Saved/** +.vs/** +.idea/** +*.VC.db +*.VC.VC.opendb +*.suo +*.user +*.userprefs +*.opensdf +*.sdf +*.tmp +*.log + +######################## +# Visual Studio / Rider / JetBrains +######################## +.vscode/** +*.code-workspace +*.DotSettings.user + +######################## +# Platform-specific +######################## +.DS_Store +Thumbs.db +Desktop.ini + +######################## +# UBT / UHT / build metadata +######################## +*.modules +*.target +*.version + +######################## +# Optional: ignore solution (regen-able) +######################## +*.sln + +######################## +# Plugins: ignore build artifacts inside plugins +######################## +Plugins/**/Binaries/** +Plugins/**/Intermediate/** +Plugins/**/DerivedDataCache/** +Plugins/**/Saved/** +Plugins/**/.vs/** +Plugins/**/.idea/** diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..b981b2e --- /dev/null +++ b/.vsconfig @@ -0,0 +1,19 @@ +{ + "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.14.44.17.14.ATL", + "Microsoft.VisualStudio.Component.VC.14.44.17.14.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/Config/DefaultEditor.ini b/Config/DefaultEditor.ini new file mode 100644 index 0000000..7c1f0ef --- /dev/null +++ b/Config/DefaultEditor.ini @@ -0,0 +1,5 @@ +[/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_BloomGaussianIntensity=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_BloomConvolutionIntensity=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,BloomGaussianIntensity=1.000000,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),BloomConvolutionIntensity=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_BloomGaussianIntensity=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_BloomConvolutionIntensity=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,BloomGaussianIntensity=1.000000,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),BloomConvolutionIntensity=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_BloomGaussianIntensity=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_BloomConvolutionIntensity=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,BloomGaussianIntensity=1.000000,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),BloomConvolutionIntensity=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/DefaultEngine.ini b/Config/DefaultEngine.ini new file mode 100644 index 0000000..901d060 --- /dev/null +++ b/Config/DefaultEngine.ini @@ -0,0 +1,162 @@ + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Game/AGame/Map/Map_Test.Map_Test +EditorStartupMap=/Game/AGame/Map/Map_Test.Map_Test +GameInstanceClass=/Game/AGame/Gameplay/GameInstance_Default.GameInstance_Default_C +GlobalDefaultGameMode=/Game/AGame/Gameplay/GameMode_InGame.GameMode_InGame_C + +[/Script/Engine.RendererSettings] +r.AllowStaticLighting=False + +r.GenerateMeshDistanceFields=True + +r.DynamicGlobalIlluminationMethod=1 + +r.ReflectionMethod=1 + +r.SkinCache.CompileShaders=True + +r.RayTracing=True + +r.RayTracing.RayTracingProxies.ProjectEnabled=True + +r.Substrate=True + +r.Substrate.ProjectGBufferFormat=0 + +r.Shadow.Virtual.Enable=1 + +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True + +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8 + +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8 + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +-D3D12TargetedShaderFormats=PCD3D_SM5 ++D3D12TargetedShaderFormats=PCD3D_SM6 +-D3D11TargetedShaderFormats=PCD3D_SM5 ++D3D11TargetedShaderFormats=PCD3D_SM5 +Compiler=VisualStudio2026 +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/MacTargetPlatform.MacTargetSettings] +-TargetedRHIs=SF_METAL_SM5 ++TargetedRHIs=SF_METAL_SM6 + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/WorldPartitionEditor.WorldPartitionEditorSettings] +CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet' + +[/Script/Engine.UserInterfaceSettings] +bAuthorizeAutomaticWidgetVariableCreation=False +FontDPIPreset=Standard +FontDPI=72 + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/PHY") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/PHY") + +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=66F18DE642E3AA7501A711AC1A3E40C9 +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= + +[/Script/Engine.CollisionProfile] +-Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision",bCanModify=False) +-Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False) +-Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) +-Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False) +-Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) +-Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.",bCanModify=False) +-Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ",bCanModify=False) +-Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ",bCanModify=False) +-Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic",Response=ECR_Block),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.",bCanModify=False) +-Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.",bCanModify=False) +-Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors",bCanModify=False) +-Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors",bCanModify=False) +-Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.",bCanModify=False) +-Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.",bCanModify=False) +-Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.",bCanModify=False) +-Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.",bCanModify=False) +-Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.",bCanModify=False) +-Profiles=(Name="UI",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Block),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) ++Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision") ++Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ") ++Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ") ++Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ") ++Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ") ++Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.") ++Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ") ++Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ") ++Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic"),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.") ++Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.") ++Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors") ++Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors") ++Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.") ++Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.") ++Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.") ++Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.") ++Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.") ++Profiles=(Name="UI",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility"),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ") ++Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)") +-ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall") +-ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn") +-ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic") +-ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor") +-ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic") ++ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall") ++ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn") ++ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic") ++ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor") ++ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic") +-CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic") +-CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic") +-CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle") +-CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn") ++CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic") ++CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic") ++CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle") ++CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn") + diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini new file mode 100644 index 0000000..5107308 --- /dev/null +++ b/Config/DefaultGame.ini @@ -0,0 +1,11 @@ +[/Script/CommonUI.CommonUISettings] +CommonButtonAcceptKeyHandling=TriggerClick + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=7DF2A45E490F29FEA209488756C669ED +CopyrightNotice= + +[/Script/GenericUISystem.GUIS_GenericUISystemSettings] +; Set this in editor to a BP subclass of UPHYGameUIPolicy (recommended), e.g. +; /Game/AGame/UI/BP_PHYGameUIPolicy.BP_PHYGameUIPolicy_C +GameUIPolicyClass= diff --git a/Config/DefaultGameplayTags.ini b/Config/DefaultGameplayTags.ini new file mode 100644 index 0000000..87e9fa4 --- /dev/null +++ b/Config/DefaultGameplayTags.ini @@ -0,0 +1,14 @@ +;METADATA=(Diff=true, UseCommands=true) +[/Script/GameplayTags.GameplayTagsSettings] +ImportTagsFromConfig=True +WarnOnInvalidTags=True +ClearInvalidTags=False +AllowEditorTagUnloading=True +AllowGameTagUnloading=False +FastReplication=False +bDynamicReplication=False +InvalidTagCharacters="\"\'," +NumBitsForContainerSize=6 +NetIndexFirstBitSegment=16 ++GameplayTagList=(Tag="Data.Regen.InnerPower",DevComment="SetByCaller: InnerPower regen per tick") + diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini new file mode 100644 index 0000000..a919105 --- /dev/null +++ b/Config/DefaultInput.ini @@ -0,0 +1,84 @@ +[/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="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="Mouse2D",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_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)) +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=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.DefaultVirtualJoysticks +-ConsoleKeys=Tilde ++ConsoleKeys=Tilde + diff --git a/GAS_AttributeSystem_Notes.md b/GAS_AttributeSystem_Notes.md new file mode 100644 index 0000000..3a9e22b --- /dev/null +++ b/GAS_AttributeSystem_Notes.md @@ -0,0 +1,254 @@ +# GAS 属性系统实现记录(PHY) + +> 说明:本文记录在本项目中,为古风游戏(四维:臂力/根骨/内息/身法)搭建 GAS 属性体系、派生属性、职业初始化、以及服务器端定时回复(回血/回内)等功能的实现过程。 +> +> 本项目模块不对外暴露,因此代码全部放在 `Source/PHY/Private` 内(AttributeSet/GE/MMC/GameplayTags 等)。 + +--- + +## 0. 总体目标 + +- 基础属性(Primary): + - **Strength(臂力)** + - **Constitution(根骨)** + - **InnerBreath(内息)** + - **Agility(身法)** +- 其他属性由四维派生(MMC 计算)或由装备/功法 GE 附加。 +- 不对外提供公共模块 API:代码全部在 `Private`。 + +--- + +## 1. GAS 组件挂载位置与初始化 + +### 1.1 AbilitySystemComponent 放在 PlayerState + +- 在 `APHYPlayerState` 的构造函数中创建: + - `UAbilitySystemComponent* AbilitySystemComponent` + - `UPHYAttributeSet* AttributeSet` +- 设置复制: + - `AbilitySystemComponent->SetIsReplicated(true)` + - `AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal)`(适合 PlayerState) + +相关文件: +- `Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp` + +### 1.2 Character 初始化 GAS:Owner=PlayerState,Avatar=Character + +在 `APHYPlayerCharacter`: +- `PossessedBy()`(服务器)调用 `InitializeGAS()` +- `OnRep_PlayerState()`(客户端)也调用 `InitializeGAS()` + +初始化逻辑(核心): +- `ASC->InitAbilityActorInfo(PlayerState, this)` +- 服务器: + - Apply `UPHYGE_InitPrimary`(设置四维初值) + - Apply `UPHYGE_DerivedAttributes`(无限期派生属性:Override + MMC) + - 设置资源/当前值: + - `Health = MaxHealth` + - `InnerPower = MaxInnerPower` + +相关文件: +- `Source/PHY/Private/Character/PHYPlayerCharacter.h/.cpp` + +--- + +## 2. AttributeSet(全部在 Private) + +### 2.1 AttributeSet 位置 + +- `Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.h` +- `Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.cpp` + +### 2.2 属性分组 + +#### Primary(基础四维) +- `Strength` +- `Constitution` +- `InnerBreath` +- `Agility` + +#### Vitals(生命) +- `Health` +- `MaxHealth` + +#### Resource(资源) +- `InnerPower` +- `MaxInnerPower` + +#### Derived(示例派生) +- `MoveSpeed` +- `PhysicalAttack` +- `PhysicalDefense` + +#### Secondary(二级属性) +- `Tenacity`(韧性) +- `CritChance`(暴击率 0~1) +- `CritDamage`(暴击伤害倍率 >=1) +- `DodgeChance`(闪避 0~1) +- `HitChance`(命中 0~1) +- `ParryChance`(招架 0~1) +- `CounterChance`(反击 0~1;业务逻辑要求闪避/招架成功后再判定) +- `ArmorPenetration`(穿甲 0~1;目前字段存在,数值来源更多偏装备/功法) +- `DamageReduction`(免伤 0~1) +- `LifeSteal`(吸血 0~1;目前字段存在,数值来源更多偏装备/功法) +- `HealthRegenRate`(生命回复/秒) +- `InnerPowerRegenRate`(内力回复/秒) + +### 2.3 Clamp 与复制 + +在 AttributeSet 内实现: +- `PreAttributeChange`: + - `Health` / `InnerPower` clamp 到 `[0, Max]` + - 概率类 clamp 到 `[0, 1]` + - `CritDamage` clamp `>= 1` +- `PostGameplayEffectExecute`: + - 对关键属性再兜底 clamp +- 完整实现: + - `OnRep_XXX` + - `GetLifetimeReplicatedProps` + `DOREPLIFETIME_CONDITION_NOTIFY` + +--- + +## 3. 派生属性计算(MMC) + +### 3.1 MMC 放置位置 + +`Source/PHY/Private/AbilitySystem/MMC/` + +### 3.2 已实现的 MMC(示例) + +#### 基础派生 +- `UPHY_MMC_MaxHealth`:`MaxHealth = 100 + Constitution * 25` +- `UPHY_MMC_MoveSpeed`:`MoveSpeed = 600 + Agility * 2` +- `UPHY_MMC_PhysicalAttack`:`PhysicalAttack = 10 + Strength * 2` +- `UPHY_MMC_PhysicalDefense`:`PhysicalDefense = 5 + Constitution * 1.5` + +#### 二级属性派生(示例系数,后续建议数据化) +- `UPHY_MMC_MaxInnerPower`:`MaxInnerPower = 100 + InnerBreath * 20` +- `UPHY_MMC_Tenacity`:`Tenacity = InnerBreath * 1` +- `UPHY_MMC_CritChance`:`CritChance = 0.05 + Agility * 0.002 (clamp 0..1)` +- `UPHY_MMC_CritDamage`:`CritDamage = 1.5 + Strength * 0.005 (>=1)` +- `UPHY_MMC_DodgeChance`:`DodgeChance = 0.02 + Agility * 0.002` +- `UPHY_MMC_HitChance`:`HitChance = 0.9 + Agility * 0.001` +- `UPHY_MMC_ParryChance`:`ParryChance = 0.03 + Strength * 0.001` +- `UPHY_MMC_CounterChance`:`CounterChance = 0.1 + Agility * 0.001` +- `UPHY_MMC_DamageReduction`:`DamageReduction = Constitution * 0.001` +- `UPHY_MMC_HealthRegenRate`:`HealthRegenRate = Constitution * 0.1` +- `UPHY_MMC_InnerPowerRegenRate`:`InnerPowerRegenRate = InnerBreath * 0.15` + +> 注:部分属性如穿甲/吸血更适合由装备/功法 GE 直接加成,而非完全从四维硬算。 + +--- + +## 4. GameplayEffect(初始化 + 派生) + +### 4.1 初始化四维:`UPHYGE_InitPrimary` + +- 文件:`Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.h/.cpp` +- 类型:Instant +- 用 `SetByCaller` 写入四维(复用同一个 GE 支持所有职业) + +SetByCaller Tag(NativeGameplayTags 管理): +- `Data.Init.Primary.Strength` +- `Data.Init.Primary.Constitution` +- `Data.Init.Primary.InnerBreath` +- `Data.Init.Primary.Agility` + +### 4.2 派生属性:`UPHYGE_DerivedAttributes` + +- 文件:`Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.h/.cpp` +- 类型:Infinite +- Modifiers:全部使用 `Override + MMC` + - 包含 `MaxHealth/MoveSpeed/PhysicalAttack/PhysicalDefense` + - 包含新增二级属性/资源上限(如 `MaxInnerPower`、暴击等) + +--- + +## 5. GameplayTags 组织(集中管理,避免硬编码 FName) + +### 5.1 Init 属性 Tag + +- `Source/PHY/Private/GameplayTags/InitAttributeTags.h/.cpp` + +### 5.2 Regen Tag + +- `Source/PHY/Private/GameplayTags/RegenTags.h/.cpp` + +### 5.3 ini 配置 + +- 新增/维护:`Config/DefaultGameplayTags.ini` + - 包含 `Data.Init.Primary.*` + - 包含 `Data.Regen.*` + +--- + +## 6. 职业/门派初始属性(全局配置) + +### 6.1 职业枚举 + +- `Source/PHY/Private/AbilitySystem/PHYCharacterClass.h` + +### 6.2 DataAsset:全局职业默认值表 + +- `Source/PHY/Private/AbilitySystem/PHYClassDefaults.h` +- `UPHYClassDefaults`: + - `FallbackPrimary` + - `Classes[]`:每个职业对应一套 `FPHYPrimaryAttributes` + +### 6.3 配置位置:GameInstance + +考虑到 `ClassDefaults` 是全局数据,不应放在每个 Character 上: +- `UPHYGameInstance` 增加 `ClassDefaults` 引用 + - `GetClassDefaults()` + +文件: +- `Source/PHY/Private/Gameplay/PHYGameInstance.h/.cpp` + +使用方式: +- 在编辑器中的项目 GameInstance 蓝图(`DefaultEngine.ini` 指定的 `GameInstance_Default`)里配置 `ClassDefaults` 资产。 + +--- + +## 7. 服务器端定时回复(方案 B:代码驱动) + +> 选择方案 B:不使用 Period GE,而使用 **服务器 Timer + Instant GE**。 + +### 7.1 Regen 每跳 GE:`UPHYGE_RegenTick` + +- 文件:`Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.h/.cpp` +- 类型:Instant +- 使用 SetByCaller: + - `Data.Regen.Health`(加到 Health) + - `Data.Regen.InnerPower`(加到 InnerPower) + +### 7.2 Character 计时器逻辑 + +在 `APHYPlayerCharacter`: +- 属性:`RegenInterval`(默认 1 秒) +- 在 `InitializeGAS()` 服务器端启动 Timer: + - `SetTimer(RegenTimerHandle, RegenTick, RegenInterval, true)` +- `RegenTick()`: + - `HealthDelta = HealthRegenRate * RegenInterval` + - `InnerPowerDelta = InnerPowerRegenRate * RegenInterval` + - Apply `UPHYGE_RegenTick`(SetByCaller 写入本次增量) + +> 后续可拓展:脱战回复/坐下打坐/战斗中禁回等,均通过 GameplayTag 或状态判断来控制 `RegenTick()` 是否执行。 + +--- + +## 8. 代码风格/工程注意事项 + +### 8.1 UTF-8 BOM 引发的编译/IDE 问题 + +过程中多次遇到文件头 BOM 导致的解析错误(如 `无法解析符号 ''`)。 +已对相关文件移除 BOM(例如 PlayerState / PlayerCharacter / GameInstance 等)。 + +--- + +## 9. 下一步建议(可选) + +- 将二级属性派生系数数据化(DataAsset/CurveTable),减少硬编码常量。 +- 装备/功法:用 GE 直接对二级属性加成(穿甲/吸血等更适合走装备词条)。 +- 战斗结算库:统一命中/闪避/招架/反击触发顺序与公式。 +- Regen 增加“脱战 N 秒后生效”等状态控制(GameplayTag 驱动)。 + diff --git a/PHY.uproject b/PHY.uproject new file mode 100644 index 0000000..81832fe --- /dev/null +++ b/PHY.uproject @@ -0,0 +1,30 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.7", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "PHY", + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine" + ] + }, + { + "Name": "PHYInventory", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + } + ] +} \ No newline at end of file diff --git a/PHY_MMC_MaxHealth.cpp b/PHY_MMC_MaxHealth.cpp new file mode 100644 index 0000000..7227a77 --- /dev/null +++ b/PHY_MMC_MaxHealth.cpp @@ -0,0 +1,2 @@ +// This file is deprecated and intentionally left empty. + diff --git a/Plugins/GCS/Config/BaseGenericCombatSystem.ini b/Plugins/GCS/Config/BaseGenericCombatSystem.ini new file mode 100644 index 0000000..e0e6dce --- /dev/null +++ b/Plugins/GCS/Config/BaseGenericCombatSystem.ini @@ -0,0 +1,29 @@ +[CoreRedirects] +;GGA1.5 migration. ++ClassRedirects = (OldName="/Script/GenericGameplayAbilities.GGA_AbilitySystemBPLibrary",NewName="/Script/GenericGameplayAbilities.GGA_AbilitySystemFunctionLibrary") ++FunctionRedirects = (OldName="/Script/GenericGameplayAbilities.GGA_GameplayEffectContainerFunctionLibrary.MakeEffectContainerSpecFromContainerWithEventData",NewName="/Script/GenericGameplayAbilities.GGA_GameplayEffectContainerFunctionLibrary.MakeEffectContainerSpec") ++FunctionRedirects = (OldName="/Script/GenericGameplayAttributes.GGA_AttributeSystemComponent.HandlePoseGameplayEffectExecute",NewName="/Script/GenericGameplayAttributes.GGA_AttributeSystemComponent.HandlePostGameplayEffectExecute") + + ++PropertyRedirects = (OldName="/Script/GenericCombatSystem.GCS_TraceDefinition.DistanceTickInterval",NewName="/Script/GenericCombatSystem.GCS_TraceDefinition.DistanceTickThreshold") ++EnumRedirects = (OldName="/Script/GenericCombatSystem.EGCS_AttackResultProcessorPolicy",ValueChanges=(("Always","Default"))) ++PropertyRedirects = (OldName="/Script/GenericCombatSystem.GCS_ComboDefinition.PayloadTag",NewName="/Script/GenericCombatSystem.GCS_ComboDefinition.EventTag") + +;GCS1.5 migration ++PropertyRedirects = (OldName="/Script/GenericCombatSystem.GCS_CollisionSystemComponent.OnTraceInstanceHitEvent",NewName="/Script/GenericCombatSystem.GCS_TraceSystemComponent.OnTraceHitEvent") ++ClassRedirects = (OldName="/Script/GenericCombatSystem.GCS_CollisionSystemComponent",NewName="/Script/GenericCombatSystem.GCS_TraceSystemComponent") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_TraceSystemComponent.GetCollisionSystemComponent",NewName="/Script/GenericCombatSystem.GCS_TraceSystemComponent.GetTraceSystemComponent") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_TraceSystemComponent.FindCollisionSystemComponent",NewName="/Script/GenericCombatSystem.GCS_TraceSystemComponent.FindTraceSystemComponent") ++StructRedirects = (OldName="/Script/GenericCombatSystem.GCS_CollisionTraceDefinition",NewName="/Script/GenericCombatSystem.GCS_TraceDefinition") ++PackageRedirects = (OldName="/Game/GenericGame/CombatSystem/Core/Abilities/LightAttack/GA_GCS_SkillAttack",NewName="/Game/GenericGame/CombatSystem/Core/Abilities/Attack/GA_GCS_SkillAttack") ++PackageRedirects = (OldName="GA_GCS_HoldAttack_C",NewName="GA_GCS_SkillAttack_Charged_C") ++ClassRedirects = (OldName="/Game/GenericGame/CombatSystem/Core/BC_GCS_AttributeSystemComponent.BC_GCS_AttributeSystemComponent_C",NewName="/Game/GenericGame/CombatSystem/Core/BC_GCS_AttributeSystem.BC_GCS_AttributeSystem_C") ++ClassRedirects = (OldName="/Game/GenericGame/CombatSystem/Core/BC_GCS_CombatComponent.BC_GCS_CombatComponent_C",NewName="/Game/GenericGame/CombatSystem/Core/BC_GCS_CombatSystem.BC_GCS_CombatSystem_C") ++ClassRedirects = (OldName="/Game/GenericGame/CombatSystem/Core/BC_GCS_CombatCore.BC_GCS_CombatCore_C",NewName="/Game/GenericGame/CombatSystem/Core/BC_GCS_CombatEntity.BC_GCS_CombatEntity_C") ++ClassRedirects = (OldName="/Game/GenericDemo/GCS/Blueprints/BC_DemoCombatCore.BC_DemoCombatCore_C",NewName="/Game/GenericDemo/GCS/Blueprints/BC_DemoCombatEntity.BC_DemoCombatEntity_C") ++ClassRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatInterface", NewName="/Script/GenericCombatSystem.GCS_CombatEntityInterface") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GCS_SetWeapon",NewName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.SetCurrentWeapon") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GCS_GetWeapon",NewName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GetCurrentWeapon") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GCS_GetRelativeTransformToSocket",NewName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GetRelativeTransformToSocket") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GetMovementInputDirection",NewName="/Script/GenericCombatSystem.GCS_CombatEntityInterface.GetMovementIntent") ++FunctionRedirects = (OldName="/Script/GenericCombatSystem.GCS_CombatFunctionLibrary.GetCombatInterface",NewName="/Script/GenericCombatSystem.GCS_CombatFunctionLibrary.GetCombatEntityInterface") \ No newline at end of file diff --git a/Plugins/GCS/Config/FilterPlugin.ini b/Plugins/GCS/Config/FilterPlugin.ini new file mode 100644 index 0000000..386e260 --- /dev/null +++ b/Plugins/GCS/Config/FilterPlugin.ini @@ -0,0 +1,9 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll +/Config/* \ No newline at end of file diff --git a/Plugins/GCS/GenericCombatSystem.uplugin b/Plugins/GCS/GenericCombatSystem.uplugin new file mode 100644 index 0000000..b22ef90 --- /dev/null +++ b/Plugins/GCS/GenericCombatSystem.uplugin @@ -0,0 +1,96 @@ +{ + "FileVersion": 3, + "Version": 8, + "VersionName": "1.5", + "FriendlyName": "GenericCombatSystem", + "Description": "Advanced GAS based Multiplayer combat framework.", + "Category": "Gameplay", + "CreatedBy": "YuewuDev", + "CreatedByURL": "https://yuewu.dev/en", + "DocsURL": "https://www.yuewu.dev/en/wiki", + "MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/d4d45c2c-c698-4274-bb14-5474b7880a01", + "SupportURL": "https://discord.com/invite/xMRXAB2", + "EngineVersion": "5.7.0", + "CanContainContent": false, + "Installed": true, + "Modules": [ + { + "Name": "GenericInputSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericGameplayAbilities", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericGameplayAttributes", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericGameplayAbilitiesEditor", + "Type": "Editor", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64" + ] + }, + { + "Name": "GenericCombatSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + } + ], + "Plugins": [ + { + "Name": "EnhancedInput", + "Enabled": true + }, + { + "Name": "GameplayAbilities", + "Enabled": true + }, + { + "Name": "ModularGameplay", + "Enabled": true + }, + { + "Name": "TargetingSystem", + "Enabled": true + }, + { + "Name": "StateTree", + "Enabled": true + }, + { + "Name": "GameplayStateTree", + "Enabled": true + }, + { + "Name": "MotionWarping", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/GCS/Resources/Icon128.png b/Plugins/GCS/Resources/Icon128.png new file mode 100644 index 0000000..d617ef1 Binary files /dev/null and b/Plugins/GCS/Resources/Icon128.png differ diff --git a/Plugins/GCS/Source/GenericCombatSystem/GenericCombatSystem.Build.cs b/Plugins/GCS/Source/GenericCombatSystem/GenericCombatSystem.Build.cs new file mode 100644 index 0000000..8d5c95b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/GenericCombatSystem.Build.cs @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using System.IO; +using UnrealBuildTool; + +public class GenericCombatSystem : ModuleRules +{ + public GenericCombatSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new[] + { + Path.Combine(ModuleDirectory, "Public/AbilitySystem"), + // ... add public include paths required here ... + } + ); + + PrivateIncludePaths.AddRange( + new[] + { + Path.Combine(ModuleDirectory, "Public/AbilitySystem"), + // ... add public include paths required here ... + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "GameplayTags", + "GameplayTasks", + "GameplayAbilities", + "GenericGameplayAbilities", + "GenericGameplayAttributes", + "ModularGameplay", + "TargetingSystem" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "NetCore", + "Engine", + "Niagara", + "AIModule", + "StateTreeModule", + "DeveloperSettings" + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_CombatAbility.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_CombatAbility.cpp new file mode 100644 index 0000000..683a7a2 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_CombatAbility.cpp @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Abilities/GCS_CombatAbility.h" + +#include "GCS_CombatSystemComponent.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +UGCS_CombatSystemComponent* UGCS_CombatAbility::GetCombatSystemFromActorInfo() const +{ + if (UGCS_CombatSystemComponent* CSS = UGCS_CombatSystemComponent::GetCombatSystemComponent(GetAvatarActorFromActorInfo())) + { + return CSS; + } + return nullptr; +} + +UObject* UGCS_CombatAbility::GetCombatEntityFromActorInfo() const +{ + return UGCS_CombatFunctionLibrary::GetCombatEntity(GetAvatarActorFromActorInfo()); +} + +TScriptInterface UGCS_CombatAbility::GetCombatEntityInterfaceFromActorInfo() const +{ + return UGCS_CombatFunctionLibrary::GetCombatEntityInterface(GetAvatarActorFromActorInfo()); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_ComboAbility.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_ComboAbility.cpp new file mode 100644 index 0000000..cfdf0db --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Abilities/GCS_ComboAbility.cpp @@ -0,0 +1,357 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Abilities/GCS_ComboAbility.h" + +#include "AbilitySystemComponent.h" +#include "GCS_CombatEntityInterface.h" +#include "GCS_CombatSystemComponent.h" +#include "GCS_LogChannels.h" +#include "GGA_GameplayTags.h" +#include "Abilities/Tasks/AbilityTask_WaitInputPress.h" +#include "Combo/GCS_ComboDefinition.h" +#include "Utilities/GGA_AbilitySystemFunctionLibrary.h" +#include "Weapon/GCS_WeaponInterface.h" + +UGCS_ComboAbility::UGCS_ComboAbility() +{ + ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateYes; + NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted; + AbilityTags.AddTagFast(GGA_AbilityTraitTags::ActivationOnSpawn); +} + +void UGCS_ComboAbility::PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData) +{ + Super::PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData); +} + +void UGCS_ComboAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) +{ + UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); + if (!CombatSys) + { + EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false); + return; + } + + UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); + AbilityEndedDelegateHandle = ASC->OnAbilityEnded.AddUObject(this, &ThisClass::HandleAbilityEnd); + + Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); +} + +bool UGCS_ComboAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, + const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const +{ + return Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags); +} + +void UGCS_ComboAbility::OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) +{ + Super::OnGiveAbility(ActorInfo, Spec); + // GiveSubAbilities(Spec); +} + +void UGCS_ComboAbility::OnRemoveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) +{ + Super::OnRemoveAbility(ActorInfo, Spec); + // RemoveSubAbilities(); +} + +void UGCS_ComboAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, + bool bWasCancelled) +{ + UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); + + if (IsValid(ASC) && AbilityEndedDelegateHandle.IsValid()) + { + ASC->OnAbilityEnded.Remove(AbilityEndedDelegateHandle); + AbilityEndedDelegateHandle.Reset(); + } + + // final check to make current ability ends(no callback will fired.)! + if (CurrentAbility.IsValid() && !bCurrentAbilityEnded) + { + ASC->CancelAbilityHandle(CurrentAbility); + CurrentAbility = FGameplayAbilitySpecHandle(); + CurrentAbilityClass = nullptr; + } + + ResetCombo(); + + Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); +} + +bool UGCS_ComboAbility::AllowAdvanceCombo_Implementation() const +{ + return true; +} + +void UGCS_ComboAbility::StartCombo_Implementation(const FGameplayEventData& ComboEvent) +{ + HandleComboExecution(ComboEvent); +} + +void UGCS_ComboAbility::AdvanceCombo_Implementation(const FGameplayEventData& ComboEventData) +{ + if (!AllowAdvanceCombo()) + { + GCS_CLOG(Verbose, "Can't advanced combo due to AllowAdvanceCombo.") + return; + } + + HandleComboExecution(ComboEventData); +} + +void UGCS_ComboAbility::ResetCombo_Implementation() +{ + DesiredComboStep = INDEX_NONE; + bCurrentAbilityEnded = false; + UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); + if (IsValid(CombatSys)) + { + CombatSys->ResetComboState(); + } +} + +void UGCS_ComboAbility::HandleAbilityEnd(const FAbilityEndedData& AbilityEndedData) +{ + //Already ends + if (bCurrentAbilityEnded) + { + return; + } + + //no ability running. + if (!CurrentAbility.IsValid() || CurrentAbility != AbilityEndedData.AbilitySpecHandle) + { + return; + } + + // GCS_CLOG(Verbose, "Current ability:%s ended. bWasCancelled:%d", *CurrentAbilityClass->GetName(), AbilityEndedData.bWasCancelled) + bCurrentAbilityEnded = true; + CurrentAbility = FGameplayAbilitySpecHandle(); + CurrentAbilityClass = nullptr; + + // No next combo. + if (DesiredComboStep == INDEX_NONE) + { + ResetCombo(); + } +} + +bool UGCS_ComboAbility::SelectComboDefinition(const FGameplayEventData& ComboEventData, int32 CurrentStep, FGCS_ComboDefinition& OutDefinition) +{ + UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); + UObject* Combat = GetCombatEntityFromActorInfo(); + if (!ASC || !Combat) return false; + + TObjectPtr ComboDefinitionTable = IGCS_CombatEntityInterface::Execute_GetComboDefinitionTable(Combat); + if (ComboDefinitionTable == nullptr) + { + GCS_CLOG(Error, "No combo definition table found from combat interface:%s, check your GetComboDefinitionTable implementation!", *GetNameSafe(Combat)); + return false; + } + + FGameplayTagContainer CurrentTags; + ASC->GetOwnedGameplayTags(CurrentTags); + + // Find best matching row in data table + for (const auto& RowPair : ComboDefinitionTable->GetRowMap()) + { + const FGCS_ComboDefinition* Row = reinterpret_cast(RowPair.Value); + if (!Row || Row->AbilityClass.IsNull()) continue; + + if (!Row->TagQuery.IsEmpty() && !Row->TagQuery.Matches(CurrentTags)) + { + continue; + } + + // skip if event tag doesn't match. + if (ComboEventData.EventTag.IsValid() && Row->EventTag.IsValid() && ComboEventData.EventTag != Row->EventTag) + { + continue; + } + + // skip if event instigatorTags + if (!Row->EventInstigatorTagQuery.IsEmpty() && !Row->EventInstigatorTagQuery.Matches(ComboEventData.InstigatorTags)) + { + continue; + } + + if (CurrentStep > 0 && Row->MinComboStep == 0) + { + continue; + } + + // skip if MinComboStep < CurrentStep(only for non resetting combo) + if (!Row->bResetComboStep && Row->MinComboStep > 0 && Row->MinComboStep < CurrentStep) continue; + + // skip if ability class is invalid. + TSubclassOf TempAbilityClass = Row->AbilityClass.LoadSynchronous(); + if (TempAbilityClass == nullptr) + { + GCS_CLOG(Verbose, "invalid ability class found on row name:%s", *RowPair.Key.ToString()); + continue; + } + + FGameplayAbilitySpecHandle FoundAbility; + if (!UGGA_AbilitySystemFunctionLibrary::FindAbilityFromClass(ASC, FoundAbility, TempAbilityClass, GetCurrentSourceObject())) + { + GCS_CLOG(Verbose, "skipped ability of class(%s) as it is not exists on ASC.", *TempAbilityClass->GetName()); + continue; + } + + if (Row->bRunActivationTest) + { + FGameplayTagContainer RelevantTags; + if (!UGGA_AbilitySystemFunctionLibrary::CanActivateAbility(ASC, FoundAbility, RelevantTags)) + { + if (Row->bAbortIfActivationTestFailed) + { + GCS_CLOG(Verbose, "Activation test failed for ability:%s, reason:%s. Combo will be aborted.", *TempAbilityClass->GetName(), *RelevantTags.ToString()); + return false; + } + { + GCS_CLOG(Verbose, "skipped ability of class(%s) due to activation test failed, reason:%s", *TempAbilityClass->GetName(), *RelevantTags.ToString()); + continue; + } + } + } + + // skip if custom rule doesn't met. + if (!CanSelectedComboDefinition(ComboEventData, CurrentStep, OutDefinition)) + { + continue; + } + + GCS_CLOG(Verbose, "selected ability of class(%s) as next combo(%d).", *TempAbilityClass->GetName(), CurrentStep); + OutDefinition = *Row; + return true; + } + return false; +} + +bool UGCS_ComboAbility::CanSelectedComboDefinition_Implementation(const FGameplayEventData& ComboEvent, int32 CurrentStep, const FGCS_ComboDefinition& ComboDefinition) const +{ + // Example code. + // FYourCustomData Data = ComboDefinition.Extension.Get(); + // UObject* Weapon = IGCS_CombatInterface::Execute_GCS_GetWeapon(GetCombatInterfaceFromActorInfo(), nullptr); + // return IGCS_WeaponInterface::Execute_GetWeaponTags(Weapon).MatchesQuery(Data.WeaponTagRequirements); + return true; +} + +void UGCS_ComboAbility::HandleComboExecution(const FGameplayEventData& ComboEventData) +{ + if (!IsLocallyControlled()) + { + return; + } + + UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); + if (!ASC) return; + + UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); + int32 CurrentStep = CombatSys ? CombatSys->GetComboStep() : 0; + + bool bStartingCombo = CurrentStep == 0; + + FGCS_ComboDefinition ComboDefinition; + + if (!SelectComboDefinition(ComboEventData, CurrentStep, ComboDefinition)) + { + GCS_CLOG(Verbose, "No next combo definition with current combo step:%d", CurrentStep); + DesiredComboStep = INDEX_NONE; // mark for no next action. + return; + } + + TSubclassOf FoundAbilityClass = ComboDefinition.AbilityClass.Get(); + + FGameplayAbilitySpecHandle FoundAbility; + UGGA_AbilitySystemFunctionLibrary::FindAbilityFromClass(ASC, FoundAbility, FoundAbilityClass, GetCurrentSourceObject()); + check(FoundAbility.IsValid()) + + bool bShouldResetComboStep = !bStartingCombo && ComboDefinition.MinComboStep > 0 && ComboDefinition.bResetComboStep; + + // Mark the desired step. + DesiredComboStep = bShouldResetComboStep ? 0 : CombatSys->GetComboStep() + 1; + + // Cancel current ability if still running. + if (CurrentAbility.IsValid() && !bCurrentAbilityEnded) + { + ASC->CancelAbilityHandle(CurrentAbility); + CurrentAbility = FGameplayAbilitySpecHandle(); + CurrentAbilityClass = nullptr; + } + + bCurrentAbilityEnded = false; + + //Combo Ability本身是预测激活的. + if (ASC->TryActivateAbility(FoundAbility, true)) + { + CurrentAbility = FoundAbility; + CurrentAbilityClass = FoundAbilityClass; + CombatSys->UpdateComboStep(DesiredComboStep); + GCS_CLOG(Verbose, "combo advanced from step %d to %d with ability:%s", CurrentStep, DesiredComboStep, *FoundAbilityClass->GetName()); + DesiredComboStep = INDEX_NONE; + } + else + { + GCS_CLOG(Verbose, "combo reset as the new ability(%s) can't be activated when advancing from step %d to %d.", *FoundAbilityClass->GetName(), CurrentStep, DesiredComboStep); + ResetCombo(); + } +} + +// void UGCS_ComboAbility::GiveSubAbilities(const FGameplayAbilitySpec& CurrentSpec) +// { +// UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); +// if (!IsValid(ASC) || !ASC->IsOwnerActorAuthoritative()) +// { +// return; +// } +// +// // Find best matching row in data table +// for (const auto& RowPair : ComboDefinitionTable->GetRowMap()) +// { +// const FGCS_ComboDefinition* Row = reinterpret_cast(RowPair.Value); +// if (!Row || Row->AbilityClass.IsNull()) continue; +// TSubclassOf AbilityClass = Row->AbilityClass.LoadSynchronous(); +// +// if (FGameplayAbilitySpec* ExistingSpec = UGGA_AbilitySystemFunctionLibrary::FindAbilitySpecFromClass(ASC, AbilityClass, CurrentSpec.SourceObject.Get())) +// { +// GCS_CLOG(Warning, "Found existing ability with same class(%s) and source object(%s),skipped.", *AbilityClass->GetName(), *GetNameSafe(CurrentSpec.SourceObject.Get())) +// continue; +// } +// +// FGameplayAbilitySpec NewSpec = ASC->BuildAbilitySpecFromClass(AbilityClass, Row->MinComboStep > 0 ? Row->MinComboStep : 0); +// NewSpec.SourceObject = CurrentSpec.SourceObject; +// const FGameplayAbilitySpecHandle& AbilitySpecHandle = ASC->GiveAbility(NewSpec); +// if (AbilitySpecHandle.IsValid()) +// { +// AvailableAbilities.Add(AbilitySpecHandle); +// } +// } +// } +// +// void UGCS_ComboAbility::RemoveSubAbilities() +// { +// UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); +// if (!IsValid(ASC)) +// { +// return; +// } +// for (FGameplayAbilitySpecHandle SubAbilityHandle : AvailableAbilities) +// { +// ASC->ClearAbility(SubAbilityHandle); +// } +// AvailableAbilities.Empty(); +// } + +#if WITH_EDITOR +EDataValidationResult UGCS_ComboAbility::IsDataValid(class FDataValidationContext& Context) const +{ + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Attributes/AS_Poise.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Attributes/AS_Poise.cpp new file mode 100644 index 0000000..69b9c3f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Attributes/AS_Poise.cpp @@ -0,0 +1,255 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "AbilitySystem/Attributes/AS_Poise.h" + +#include "Net/UnrealNetwork.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameplayEffectExtension.h" +#include "GGA_GameplayAttributesHelper.h" +#include "GGA_AttributeSystemComponent.h" + +namespace AS_Poise +{ + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Poise, TEXT("GGF.Attribute.PoiseSet.Poise"), "Current Poise value of an actor.(actor的当前抗打击值)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(MaxPoise, TEXT("GGF.Attribute.PoiseSet.MaxPoise"), "Max Poise value of an actor.(actor的最大抗打击值)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(PoiseRecover, TEXT("GGF.Attribute.PoiseSet.PoiseRecover"), "How many Poise to recover per second.(每秒恢复抗打击值)") + + +} + +UAS_Poise::UAS_Poise() +{ + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Poise::Poise,GetPoiseAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Poise::MaxPoise,GetMaxPoiseAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Poise::PoiseRecover,GetPoiseRecoverAttribute()); + + +} + +void UAS_Poise::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, Poise, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, MaxPoise, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, PoiseRecover, COND_None, REPNOTIFY_Always); + +} + + +void UAS_Poise::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + + if (Attribute == GetPoiseAttribute()) + { + NewValue = FMath::Clamp(NewValue,0,GetMaxPoise()); + } + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePreAttributeChange(this,Attribute,NewValue); + } + } +} + +bool UAS_Poise::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data) +{ + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + return ASS->ReceivePreGameplayEffectExecute(this, Data); + } + } + + return Super::PreGameplayEffectExecute(Data); +} + +void UAS_Poise::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + Super::PostAttributeChange(Attribute, OldValue, NewValue); + + + + if (Attribute == GetMaxPoiseAttribute()) + { + AdjustAttributeForMaxChange(Poise, OldValue, NewValue, GetPoiseAttribute()); + } + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostAttributeChange(this, Attribute, OldValue, NewValue); + } + } +} + +void UAS_Poise::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + + if (Data.EvaluatedData.Attribute == GetPoiseAttribute()) + { + SetPoise(FMath::Clamp(GetPoise(),0,GetMaxPoise())); + } + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostGameplayEffectExecute(this,Data); + } + } +} + +void UAS_Poise::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, + const FGameplayAttribute& AffectedAttributeProperty) +{ + UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); + const float CurrentMaxValue = MaxAttribute.GetCurrentValue(); + if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp) + { + // Change current value to maintain the current Val / Max percent + const float CurrentValue = AffectedAttribute.GetCurrentValue(); + float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue; + + AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta); + } +} + + + +FGameplayAttribute UAS_Poise::Bp_GetPoiseAttribute() +{ + return ThisClass::GetPoiseAttribute(); +} + +float UAS_Poise::Bp_GetPoise() const +{ + return GetPoise(); +} + +void UAS_Poise::Bp_SetPoise(float NewValue) +{ + SetPoise(NewValue); +} + +void UAS_Poise::Bp_InitPoise(float NewValue) +{ + InitPoise(NewValue); +} + +void UAS_Poise::OnRep_Poise(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, Poise, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetPoiseAttribute(),GetPoise(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Poise::Bp_GetMaxPoiseAttribute() +{ + return ThisClass::GetMaxPoiseAttribute(); +} + +float UAS_Poise::Bp_GetMaxPoise() const +{ + return GetMaxPoise(); +} + +void UAS_Poise::Bp_SetMaxPoise(float NewValue) +{ + SetMaxPoise(NewValue); +} + +void UAS_Poise::Bp_InitMaxPoise(float NewValue) +{ + InitMaxPoise(NewValue); +} + +void UAS_Poise::OnRep_MaxPoise(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, MaxPoise, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetMaxPoiseAttribute(),GetMaxPoise(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Poise::Bp_GetPoiseRecoverAttribute() +{ + return ThisClass::GetPoiseRecoverAttribute(); +} + +float UAS_Poise::Bp_GetPoiseRecover() const +{ + return GetPoiseRecover(); +} + +void UAS_Poise::Bp_SetPoiseRecover(float NewValue) +{ + SetPoiseRecover(NewValue); +} + +void UAS_Poise::Bp_InitPoiseRecover(float NewValue) +{ + InitPoiseRecover(NewValue); +} + +void UAS_Poise::OnRep_PoiseRecover(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, PoiseRecover, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetPoiseRecoverAttribute(),GetPoiseRecover(),OldValue.GetCurrentValue()); + } + } +} + + + diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.cpp new file mode 100644 index 0000000..5ebdecc --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.cpp @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.h" + +FGameplayEffectContext* UDEPRECATED_GCS_AbilitySystemGlobals::AllocGameplayEffectContext() const +{ + return Super::AllocGameplayEffectContext(); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.cpp new file mode 100644 index 0000000..6f82084 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.cpp @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.h" + +#include "GameplayEffect.h" +#include "GCS_LogChannels.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +void UGCS_GEComponent_PredictivelyExecute::OnGameplayEffectApplied(FActiveGameplayEffectsContainer& ActiveGEContainer, FGameplayEffectSpec& GESpec, FPredictionKey& PredictionKey) const +{ + FGCS_ContextPayload_Combat* Payload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(GESpec.GetEffectContext()); + + if (Payload) + { + Payload->PredictionKey = PredictionKey; + } + + if (GESpec.GetEffectContext().GetInstigator()->GetLocalRole() == ROLE_AutonomousProxy) + { + if (Payload) + { + Payload->bIsPredictingContext = true; + } + GCS_LOG(Verbose, "Ctx:%s %s predictively execute:%s", *GetClientServerContextString(GESpec.GetEffectContext().GetInstigator()), *GetNameSafe(GESpec.GetEffectContext().GetInstigator()), + *GetNameSafe(GESpec.Def)) + ActiveGEContainer.PredictivelyExecuteEffectSpec(GESpec, PredictionKey, bPredictGameplayCues); + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/GCS_GameplayEffectContext.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/GCS_GameplayEffectContext.cpp new file mode 100644 index 0000000..ba90c24 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/GCS_GameplayEffectContext.cpp @@ -0,0 +1,44 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilitySystem/GCS_GameplayEffectContext.h" + +void FGCS_ContextPayload_Combat::SetTaggedValue(const FGameplayTag& Tag, float NewValue) +{ + if (Tag.IsValid()) + { + bool bFound = false; + for (FGCS_TaggedValue& TaggedValue : TaggedValues) + { + if (TaggedValue.Attribute == Tag) + { + TaggedValue.Value = NewValue; + bFound = true; + break; + } + } + + if (!bFound) + { + FGCS_TaggedValue Temp; + Temp.Attribute = Tag; + Temp.Value = NewValue; + TaggedValues.Add(Temp); + } + } +} + +float FGCS_ContextPayload_Combat::GetTaggedValue(const FGameplayTag& Tag) const +{ + if (Tag.IsValid()) + { + for (const FGCS_TaggedValue& TaggedValue : TaggedValues) + { + if (TaggedValue.Attribute == Tag) + { + return TaggedValue.Value; + } + } + } + return 0.0f; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.cpp new file mode 100644 index 0000000..9811455 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.cpp @@ -0,0 +1,122 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GenericCombatSystem/Public/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.h" +#include "GCS_LogChannels.h" +#include "Collision/GCS_TraceSystemComponent.h" +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" +#include "CombatFlow/GCS_AttackRequest.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + + +UGCS_AbilityTask_CollisionTrace::UGCS_AbilityTask_CollisionTrace() +{ + // make sure this task runs on simulated proxy. + bSimulatedTask = true; +} + +UGCS_AbilityTask_CollisionTrace* UGCS_AbilityTask_CollisionTrace::HandleCollisionTraces(UGameplayAbility* OwningAbility, FName TaskInstanceName, bool bAdjustVisibilityBasedAnimTickOption) +{ + UGCS_AbilityTask_CollisionTrace* MyTask = NewAbilityTask(OwningAbility, TaskInstanceName); + MyTask->bAdjustAnimTickOption = bAdjustVisibilityBasedAnimTickOption; + return MyTask; +} + +void UGCS_AbilityTask_CollisionTrace::Activate() +{ + Super::Activate(); + + if (bAdjustAnimTickOption && GetAvatarActor()->GetNetMode() == NM_DedicatedServer) + { + if (USkeletalMeshComponent* SkeletalMeshComponent = UGCS_CombatFunctionLibrary::GetMainCharacterMeshComponent(GetAvatarActor())) + { + if (SkeletalMeshComponent->VisibilityBasedAnimTickOption != EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones) + { + PrevAnimTickOption = SkeletalMeshComponent->VisibilityBasedAnimTickOption; + SkeletalMeshComponent->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones; + bAdjustAnimTickOption = true; + } + } + } + + if (UGCS_TraceSystemComponent* TSC = UGCS_TraceSystemComponent::GetTraceSystemComponent(GetAvatarActor())) + { + TSC->OnTraceHitEvent.AddDynamic(this, &ThisClass::TraceHitCallback); + } +} + +void UGCS_AbilityTask_CollisionTrace::OnDestroy(bool bInOwnerFinished) +{ + if (bAdjustAnimTickOption && bAdjustedAnimTickOption) + { + if (USkeletalMeshComponent* SkeletalMeshComponent = UGCS_CombatFunctionLibrary::GetMainCharacterMeshComponent(GetAvatarActor())) + { + SkeletalMeshComponent->VisibilityBasedAnimTickOption = PrevAnimTickOption; + } + } + if (UGCS_TraceSystemComponent* TSC = UGCS_TraceSystemComponent::GetTraceSystemComponent(GetAvatarActor())) + { + TSC->OnTraceHitEvent.RemoveDynamic(this, &ThisClass::TraceHitCallback); + for (auto& MeleeRequest : MeleeRequests) + { + TSC->StopTraces(MeleeRequest.Value); + } + } + + + MeleeRequests.Empty(); + + Super::OnDestroy(bInOwnerFinished); +} + +void UGCS_AbilityTask_CollisionTrace::TraceHitCallback(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult) +{ + if (ShouldBroadcastAbilityTaskDelegates() && !MeleeRequests.IsEmpty() && TraceHandle.IsValidHandle()) + { + TObjectPtr Req = nullptr; + bool bFound = false; + for (auto& MeleeRequest : MeleeRequests) + { + if (!MeleeRequest.Value.Contains(TraceHandle)) + { + continue; + } + Req = MeleeRequest.Key; + bFound = true; + break; + } + if (bFound) + { + OnTargetsFound.Broadcast(Req, TraceHandle, HitResult); + } + } +} + +void UGCS_AbilityTask_CollisionTrace::AddMeleeRequest(const UGCS_AttackRequest_Melee* Request, UObject* SourceObject) +{ + if (IsValid(Request) && !MeleeRequests.Contains(Request)) + { + const FGameplayTagContainer& TracesToControl = Request->TracesToControl; + if (UGCS_TraceSystemComponent* TSC = UGCS_TraceSystemComponent::GetTraceSystemComponent(GetAvatarActor())) + { + TArray Handles = TSC->StartTraces(TracesToControl, SourceObject); + if (Handles.IsEmpty()) + { + GCS_LOG(Warning, "Ability:(%s), No any trace started by melee request(%s) with source object(%s)", *GetNameSafe(Ability.Get()), *Request->GetPathName(), *GetNameSafe(SourceObject)); + } + MeleeRequests.Emplace(Request, Handles); + } + } +} + +void UGCS_AbilityTask_CollisionTrace::RemoveMeleeRequest(const UGCS_AttackRequest_Melee* Request) +{ + if (IsValid(Request) && MeleeRequests.Contains(Request)) + { + if (UGCS_TraceSystemComponent* TSC = UGCS_TraceSystemComponent::GetTraceSystemComponent(GetAvatarActor())) + { + TSC->StopTraces(MeleeRequests[Request]); + } + MeleeRequests.Remove(Request); + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletContainer.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletContainer.cpp new file mode 100644 index 0000000..9cdbde4 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletContainer.cpp @@ -0,0 +1,25 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_BulletContainer.h" + +void FGCS_BulletContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ +} + +void FGCS_BulletContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ +} + +void FGCS_BulletContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ +} + +int32 FGCS_BulletContainer::IndexOfById(const FGuid& Id) const +{ + if (!Id.IsValid()) + { + return INDEX_NONE; + } + return -1; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletInstance.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletInstance.cpp new file mode 100644 index 0000000..6d8c31e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletInstance.cpp @@ -0,0 +1,441 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_BulletInstance.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" +#include "GCS_GameplayTags.h" +#include "GCS_LogChannels.h" +#include "Engine/World.h" +#include "GameFramework/Pawn.h" +#include "Bullet/GCS_BulletStructLibrary.h" +#include "Bullet/GCS_BulletSubsystem.h" +#include "CombatFlow/GCS_AttackDefinition.h" +#include "GameFramework/ProjectileMovementComponent.h" +#include "NiagaraSystem.h" +#include "Net/UnrealNetwork.h" +#include "Utilities/GGA_GameplayEffectContainerFunctionLibrary.h" +#include "Utilities/GGA_GameplayEffectFunctionLibrary.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +// Sets default values +AGCS_BulletInstance::AGCS_BulletInstance() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bAllowTickBatching = true; + + bReplicates = true; + + ProjectileMovement = CreateDefaultSubobject("ProjectileMovement"); +} + +void AGCS_BulletInstance::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams SharedParams; + SharedParams.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, DefinitionHandle, SharedParams); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, BulletId, SharedParams); +} + +UProjectileMovementComponent* AGCS_BulletInstance::GetProjectileMovementComponent() const +{ + return ProjectileMovement; +} + +void AGCS_BulletInstance::SetDefinitionHandle(FDataTableRowHandle NewHandle) +{ + if ((GetOwner() != nullptr && GetOwner()->HasAuthority()) || HasAuthority()) + { + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, DefinitionHandle, this); + DefinitionHandle = NewHandle; + ForceNetUpdate(); + OnRep_BulletDefinition(); + } +} + +void AGCS_BulletInstance::SetBulletId(const FGuid& NewId) +{ + if (NewId.IsValid()) + { + if ((GetOwner() != nullptr && GetOwner()->HasAuthority()) || HasAuthority()) + { + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, DefinitionHandle, this); + } + BulletId = NewId; + } + else + { + UE_LOG(LogGCS, Error, TEXT("Attempt to set invalid guid for bullet(%s)"), *GetName()) + } +} + +FGuid AGCS_BulletInstance::GetBulletId() const +{ + return BulletId; +} + +void AGCS_BulletInstance::SetParentBulletId_Implementation(FGuid NewParentId) +{ + ParentBulletId = NewParentId; +} + +FGuid AGCS_BulletInstance::GetParentBulletId() const +{ + return ParentBulletId; +} + +void AGCS_BulletInstance::SetHitResult(const FHitResult& NewHitResult) +{ + LastHitResult = NewHitResult; + // HitActors.Push(NewHitResult.GetActor()); +} + +const FHitResult& AGCS_BulletInstance::GetHitResult() const +{ + return LastHitResult; +} + +bool AGCS_BulletInstance::HasGameplayAuthority() const +{ + return HasAuthority() && !bIsLocalPredicting; +} + +void AGCS_BulletInstance::LaunchBullet_Implementation() +{ +} + +bool AGCS_BulletInstance::GetEffectSpecHandle_Implementation(FGameplayEffectSpecHandle& OutHandle) +{ + OutHandle = EffectSpecHandle; + return EffectSpecHandle.IsValid(); +} + +FGGA_GameplayEffectContainer AGCS_BulletInstance::GetEffectContainer_Implementation() const +{ + if (FGCS_AttackDefinition* AtkDef = Definition.AttackDefinition.GetRow(TEXT("AGCS_BulletInstance::GetEffectContainer"))) + { + return AtkDef->TargetEffectContainer; + } + return FGGA_GameplayEffectContainer(); +} + +int32 AGCS_BulletInstance::GetEffectContainerLevelOverride_Implementation() const +{ + return 0; +} + +void AGCS_BulletInstance::SetEffectContainerSpec_Implementation(const FGGA_GameplayEffectContainerSpec& InEffectContainerSpec) +{ + EffectContainerSpec = InEffectContainerSpec; +} + +void AGCS_BulletInstance::SetEffectSpec_Implementation(FGameplayEffectSpecHandle& InEffectSpec) +{ + EffectSpecHandle = InEffectSpec; +} + +UShapeComponent* AGCS_BulletInstance::GetBulletShape_Implementation() const +{ + return nullptr; +} + +FGGA_GameplayEffectContainerSpec AGCS_BulletInstance::GetEffectContainerSpec_Implementation() const +{ + return EffectContainerSpec; +} + +void AGCS_BulletInstance::PostNetInit() +{ + Super::PostNetInit(); +} + +void AGCS_BulletInstance::PostNetReceive() +{ + Super::PostNetReceive(); +} + +void AGCS_BulletInstance::FoundLocalPredictedBullet_Implementation(AGCS_BulletInstance* PredictedBullet) +{ +} + +TSubclassOf AGCS_BulletInstance::GetEffectClass_Implementation() const +{ + if (FGCS_AttackDefinition* AtkDef = Definition.AttackDefinition.GetRow(TEXT("AGCS_BulletInstance::GetEffectClass"))) + { + if (!AtkDef->TargetEffectClass.IsNull()) + { + return AtkDef->TargetEffectClass.LoadSynchronous(); + } + } + return nullptr; +} + +int32 AGCS_BulletInstance::GetEffectLevel_Implementation() const +{ + if (FGCS_AttackDefinition* AtkDef = Definition.AttackDefinition.GetRow(TEXT("AGCS_BulletInstance::GetEffectClass"))) + { + return AtkDef->TargetEffectClassLevel; + } + return 0; +} + + +// Called when the game starts or when spawned +void AGCS_BulletInstance::BeginPlay() +{ + Super::BeginPlay(); +} + +void AGCS_BulletInstance::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + OnBulletEndPlay(); + Super::EndPlay(EndPlayReason); +} + +void AGCS_BulletInstance::OnBulletBeginPlay_Implementation() +{ + SetActorHiddenInGame(false); + SetActorTickEnabled(true); + SetActorEnableCollision(true); +} + +void AGCS_BulletInstance::OnBulletEndPlay_Implementation() +{ + SetActorHiddenInGame(true); + SetActorTickEnabled(false); + SetActorEnableCollision(false); + bIsLocalPredicting = false; + Definition = FGCS_BulletDefinition(); + Request = nullptr; + EffectSpecHandle = FGameplayEffectSpecHandle(); + EffectContainerSpec = FGGA_GameplayEffectContainerSpec(); + SetActorTransform(FTransform::Identity); +} + +void AGCS_BulletInstance::SetupInitialLocationAndRotation() +{ + InitialActorLocation = GetActorLocation(); + InitialActorRotation = GetActorRotation(); +} + +void AGCS_BulletInstance::RefreshTravelStates() +{ + if (HasAuthority() && GetProjectileMovementComponent() && GetProjectileMovementComponent()->IsActive()) + { + // update Traveled distance and gravity scale. + TraveledDistance = FVector::Dist2D(GetActorLocation(), InitialActorLocation); + float DesiredGravityScale = TraveledDistance <= Definition.AttenuationRange ? Definition.GravityScaleInRange : Definition.GravityScaleOutRage; + if (GetProjectileMovementComponent()->ProjectileGravityScale != DesiredGravityScale) + { + GetProjectileMovementComponent()->ProjectileGravityScale = DesiredGravityScale; + } + } +} + +// bool AGCS_BulletInstance::AlreadyHit(const FHitResult& InHitResult) const +// { +// for (int i = 0; i < HitActors.Num(); ++i) +// { +// if (HitActors[i] == InHitResult.GetActor()) +// { +// return true; +// } +// } +// return false; +// } + +bool AGCS_BulletInstance::ShouldPenetrateHitResult(const FHitResult& InHitResult) const +{ + if (InHitResult.GetActor() != nullptr) + { + if (Definition.bPenetrateCharacter && InHitResult.GetActor()->GetClass()->IsChildOf(APawn::StaticClass())) + { + return true; + } + return Definition.bPenetrateMap; + } + return false; +} + +bool AGCS_BulletInstance::ShouldGenerateBullet_Implementation() +{ + if (Definition.HitBulletDefinition.IsNull() || Definition.HitBulletDefinition == DefinitionHandle) + { + return false; + } + if (Definition.LaunchCondition == GCS_BulletLaunch::Always || Definition.LaunchCondition == FGameplayTag::EmptyTag) + { + return true; + } + if (Definition.LaunchCondition == GCS_BulletLaunch::DidNotHitPawn) + { + if (LastHitResult.GetActor() && !LastHitResult.GetActor()->GetClass()->IsChildOf(APawn::StaticClass())) + { + return true; + } + } + if (Definition.LaunchCondition == GCS_BulletLaunch::HitPawn) + { + if (LastHitResult.GetActor() && LastHitResult.GetActor()->GetClass()->IsChildOf(APawn::StaticClass())) + { + return true; + } + } + return false; +} + + +void AGCS_BulletInstance::HandleBulletHitChains_Implementation() +{ + if (!HasGameplayAuthority() || !ShouldGenerateBullet()) + { + return; + } + + UAbilitySystemComponent* AbilitySystem = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner()); + if (AbilitySystem == nullptr) + { + return; + } + + FGCS_BulletDefinition* SubBullet = Definition.HitBulletDefinition.GetRow(TEXT("HandleBulletHitChains")); + if (SubBullet == nullptr) + { + return; + } + + + // Setup bullet gameplay effect instance and launch. + + FGCS_BulletSpawnParameters SpawnParams; + SpawnParams.Owner = GetOwner(); + SpawnParams.DefinitionHandle = Definition.HitBulletDefinition; + + //TODO Various different launch location. + FTransform SpawnTransform = FTransform::Identity; + SpawnTransform.SetLocation(GetHitResult().Location); + SpawnTransform.SetRotation(GetActorRotation().Quaternion()); + SpawnTransform.SetScale3D(FVector::One()); + SpawnParams.SpawnTransform = SpawnTransform; + SpawnParams.Request = Request; + SpawnParams.ParentId = BulletId; + + FGameplayEventData EventData; + EventData.Instigator = GetOwner(); + EventData.EventMagnitude = GetEffectContainerLevelOverride_Implementation(); + + TArray BulletInstances = GetWorld()->GetSubsystem()->SpawnBullets(SpawnParams); + + //Setup each bullets + for (AGCS_BulletInstance* BulletInstance : BulletInstances) + { + // Setup normal gameplay effects. + TSubclassOf GE = Execute_GetEffectClass(BulletInstance); + int32 GELevel = Execute_GetEffectLevel(BulletInstance); + if (GE != nullptr) + { + FGameplayEffectSpecHandle GESpec = AbilitySystem->MakeOutgoingSpec(GE, GELevel, AbilitySystem->MakeEffectContext()); + UGCS_CombatFunctionLibrary::AddAttackHandleToGameplayEffectSpec(GESpec, SubBullet->AttackDefinition); + FGameplayEffectContextHandle ContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(GESpec); + UGGA_GameplayEffectFunctionLibrary::SetEffectCauser(ContextHandle, BulletInstance); + Execute_SetEffectSpec(BulletInstance, GESpec); + } + + // Setup gameplay effects container. + const FGGA_GameplayEffectContainer& GEContainer = Execute_GetEffectContainer(BulletInstance); + + if (UGGA_GameplayEffectContainerFunctionLibrary::IsValidContainer(GEContainer)) + { + FGGA_GameplayEffectContainerSpec GEContainerSpec = UGGA_GameplayEffectContainerFunctionLibrary::MakeEffectContainerSpec( + GEContainer, EventData); + + // Setup each gameplay effect instance. + for (const FGameplayEffectSpecHandle& GESpec : GEContainerSpec.TargetGameplayEffectSpecs) + { + UGCS_CombatFunctionLibrary::AddAttackHandleToGameplayEffectSpec(GESpec, SubBullet->AttackDefinition); + FGameplayEffectContextHandle ContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(GESpec); + UGGA_GameplayEffectFunctionLibrary::SetEffectCauser(ContextHandle, BulletInstance); + } + Execute_SetEffectContainerSpec(BulletInstance, GEContainerSpec); + } + } + + //Launch each bullets + for (AGCS_BulletInstance* BulletInstance : BulletInstances) + { + BulletInstance->LaunchBullet(); + } +} + +void AGCS_BulletInstance::ApplyGameplayEffects_Implementation(FHitResult HitResult) +{ + if (!HasGameplayAuthority()) + { + return; + } + if (UAbilitySystemComponent* TargetAsc = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor())) + { + FGameplayEffectSpecHandle SpecHandle; + if (Execute_GetEffectSpecHandle(this, SpecHandle)) + { + FGameplayEffectContextHandle ContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(SpecHandle); + ContextHandle.AddHitResult(HitResult, true); + ContextHandle.GetInstigatorAbilitySystemComponent()->BP_ApplyGameplayEffectSpecToTarget(SpecHandle, TargetAsc); + } + + FGGA_GameplayEffectContainerSpec ContainerSpec = Execute_GetEffectContainerSpec(this); + if (ContainerSpec.HasValidEffects()) + { + FGameplayAbilityTargetData_SingleTargetHit* NewData = new FGameplayAbilityTargetData_SingleTargetHit(HitResult); + ContainerSpec.TargetData.Add(NewData); + for (const FGameplayEffectSpecHandle& TargetGameplayEffectSpec : ContainerSpec.TargetGameplayEffectSpecs) + { + TargetGameplayEffectSpec.Data->GetContext().AddHitResult(HitResult, true); + } + } + UGGA_GameplayEffectContainerFunctionLibrary::ApplyExternalEffectContainerSpec(ContainerSpec); + } +} + +void AGCS_BulletInstance::OnRep_BulletId(FGuid Prev) +{ + if (UGCS_BulletSubsystem* BulletSubsystem = GetWorld()->GetSubsystem()) + { + if (!bIsLocalPredicting && BulletSubsystem->BulletInstances.Contains(BulletId)) + { + UE_LOG(LogGCS, Warning, TEXT("Found local predicted bullet(%s)"), *BulletSubsystem->BulletInstances[BulletId]->GetName()); + FoundLocalPredictedBullet(BulletSubsystem->BulletInstances[BulletId]); + } + } +} + +void AGCS_BulletInstance::OnRep_BulletDefinition() +{ + if (DefinitionHandle.IsNull()) + { + Definition = FGCS_BulletDefinition(); + OnBulletEndPlay(); + } + else + { + if (const FGCS_BulletDefinition* NewDefinition = DefinitionHandle.GetRow(TEXT("RefreshDefinition"))) + { + Definition = *NewDefinition; + OnBulletBeginPlay(); + } + else + { + UE_LOG(LogGCS, Verbose, TEXT("Failed to load definition(%s) for bullet(%s)"), *DefinitionHandle.ToDebugString(), *GetPathName(this)); + } + } +} + +// Called every frame +void AGCS_BulletInstance::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletStructLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletStructLibrary.cpp new file mode 100644 index 0000000..12eba2b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletStructLibrary.cpp @@ -0,0 +1,10 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_BulletStructLibrary.h" +#include "CombatFlow/GCS_AttackRequest.h" + +FString FGCS_BulletSpawnParameters::ToDebugString() const +{ + return FString::Format(TEXT("Owner:{0} DefinitionHandle:{1}"), {Owner ? Owner->GetName() : "Null", DefinitionHandle.ToDebugString()}); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSubsystem.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSubsystem.cpp new file mode 100644 index 0000000..52f89b8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSubsystem.cpp @@ -0,0 +1,197 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_BulletSubsystem.h" + +#include "GCS_LogChannels.h" +#include "Bullet/GCS_BulletInstance.h" +#include "Engine/World.h" +#include "Kismet/KismetMathLibrary.h" + +UGCS_BulletSubsystem* UGCS_BulletSubsystem::Get(const UWorld* World) +{ + if (IsValid(World)) + { + return World->GetSubsystem(); + } + return nullptr; +} + +TArray UGCS_BulletSubsystem::SpawnBullets(const FGCS_BulletSpawnParameters& SpawnParameters) +{ + TArray RetInstances{}; + + FGCS_BulletDefinition Definition; + if (SpawnParameters.DefinitionHandle.IsNull() || !LoadBulletDefinition(SpawnParameters.DefinitionHandle, Definition)) + { + return RetInstances; + } + + RetInstances = GetOrCreateBulletInstances(SpawnParameters, Definition); + + for (int i = 0; i < RetInstances.Num(); ++i) + { + AGCS_BulletInstance* Instance = RetInstances[i]; + + Instance->bServerInitiated = GetWorld()->GetNetMode() < NM_Client; + Instance->bIsLocalPredicting = SpawnParameters.bIsLocalPredicting; + + if (SpawnParameters.OverrideBulletIds.IsValidIndex(i)) + { + Instance->SetBulletId(SpawnParameters.OverrideBulletIds[i]); + } + else + { + Instance->SetBulletId(FGuid::NewGuid()); + } + + if (SpawnParameters.ParentId.IsValid() && BulletInstances.Contains(SpawnParameters.ParentId)) + { + Instance->SetParentBulletId(SpawnParameters.ParentId); + // if (AGCS_BulletInstance* ParentBulletInstance = BulletInstances[SpawnParameters.ParentId]) + // { + // if (ParentBulletInstance->Definition.bUseSharedHitList) + // { + // Instance->HitActors = ParentBulletInstance->HitActors; + // } + // } + } + + if (SpawnParameters.Request) + { + Instance->Request = SpawnParameters.Request; + } + + if (SpawnParameters.Owner) + { + Instance->SetOwner(SpawnParameters.Owner); + } + Instance->SetDefinitionHandle(SpawnParameters.DefinitionHandle); + BulletInstances.Emplace(Instance->BulletId, Instance); + } + for (int i = 0; i < RetInstances.Num(); ++i) + { + FRotator RotationAdjustment(Definition.LaunchElevationAngle, Definition.LaunchAngle + Definition.LaunchAngleInterval * i, 0); + + // Start with the spawn transform + // FTransform ModifiedTransform = SpawnParameters.SpawnTransform; + + // Compose the rotations: apply adjustment relative to original rotation + // FRotator FinalRotation = UKismetMathLibrary::ComposeRotators(ModifiedTransform.Rotator(), RotationAdjustment); + // ModifiedTransform.SetRotation(FinalRotation.Quaternion()); + + RetInstances[i]->SetActorTransform(FTransform(RotationAdjustment, FVector::ZeroVector) * SpawnParameters.SpawnTransform, false, nullptr, ETeleportType::ResetPhysics); + } + + //batch beginplay. + for (AGCS_BulletInstance* Instance : RetInstances) + { + Instance->OnBulletBeginPlay(); + } + return RetInstances; +} + +TArray UGCS_BulletSubsystem::GetIdsFromBullets(TArray Instances) +{ + TArray Ids; + for (AGCS_BulletInstance* BulletInstance : Instances) + { + Ids.Add(BulletInstance->BulletId); + } + return Ids; +} + +TArray UGCS_BulletSubsystem::GetOrCreateBulletInstances(const FGCS_BulletSpawnParameters& SpawnParameters, const FGCS_BulletDefinition& Definition) +{ + TArray OutInstances; + + static int32 MaxAllowedLoops = 30; + + int32 Counter = 0; + while (OutInstances.Num() < Definition.BulletCount) + { + if (AGCS_BulletInstance* Instance = TakeBulletFromPool(Definition.BulletActorClass.LoadSynchronous())) + { + OutInstances.Add(Instance); + } + else if (AGCS_BulletInstance* Instance2 = CreateBulletInstance(SpawnParameters, Definition)) + { + OutInstances.Add(Instance2); + } + Counter++; + if (Counter >= MaxAllowedLoops) + { + UE_LOG(LogGCS, Warning, TEXT("BulletSubsystem reach max allowed bullet spawn loops(%d)."), MaxAllowedLoops); + break; + } + } + return OutInstances; +} + +AGCS_BulletInstance* UGCS_BulletSubsystem::TakeBulletFromPool(TSubclassOf BulletClass) +{ + int32 Found = INDEX_NONE; + for (int i = 0; i < BulletPools.Num(); i++) + { + if (BulletPools[i].GetClass() == BulletClass) + { + Found = i; + break; + } + } + if (Found != INDEX_NONE) + { + AGCS_BulletInstance* FoundInstance = BulletPools[Found]; + UE_LOG(LogGCS, Verbose, TEXT("Taking bullet(%s) from pool."), *BulletClass->GetName()); + BulletPools.RemoveAtSwap(Found); + return FoundInstance; + } + return nullptr; +} + +void UGCS_BulletSubsystem::DestroyBullet(FGuid BulletId) +{ + if (BulletInstances.Contains(BulletId)) + { + AGCS_BulletInstance* BulletToRemove = BulletInstances[BulletId]; + BulletToRemove->SetDefinitionHandle(FDataTableRowHandle()); + BulletToRemove->SetOwner(nullptr); + BulletInstances.Remove(BulletId); + BulletPools.Add(BulletToRemove); + UE_LOG(LogGCS, Verbose, TEXT("Return bullet(%s) back to pool."), *BulletToRemove->GetClass()->GetName()); + } +} + +AGCS_BulletInstance* UGCS_BulletSubsystem::CreateBulletInstance(const FGCS_BulletSpawnParameters& SpawnParameters, const FGCS_BulletDefinition& Definition) +{ + if (Definition.BulletActorClass.IsNull()) + { + UE_LOG(LogGCS, Error, TEXT("Failed to create bullet instance for definition(%s),missing BulletActorClass!!!"), *SpawnParameters.DefinitionHandle.ToDebugString()); + return nullptr; + } + + UClass* BulletClass = Definition.BulletActorClass.LoadSynchronous(); + check(BulletClass); + FActorSpawnParameters ActorSpawnParameters; + ActorSpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AGCS_BulletInstance* NewInstance = GetWorld()->SpawnActor(BulletClass, FTransform::Identity, ActorSpawnParameters); + if (NewInstance) + { + UE_LOG(LogGCS, Verbose, TEXT("Create new bullet instance for class(%s)"), *BulletClass->GetName()); + return NewInstance; + } + + UE_LOG(LogGCS, Error, TEXT("Failed to create new bullet instance for class(%s)"), *BulletClass->GetName()); + return nullptr; +} + +bool UGCS_BulletSubsystem::LoadBulletDefinition(const FDataTableRowHandle& Handle, FGCS_BulletDefinition& OutDefinition) +{ + if (FGCS_BulletDefinition* Definition = Handle.GetRow(TEXT("LoadBulletDefinition"))) + { + OutDefinition = *Definition; + return true; + } + return false; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSystemComponent.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSystemComponent.cpp new file mode 100644 index 0000000..a36acf8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_BulletSystemComponent.cpp @@ -0,0 +1,126 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_BulletSystemComponent.h" + +#include "GCS_LogChannels.h" +#include "Bullet/GCS_BulletInstance.h" +#include "Bullet/GCS_BulletSubsystem.h" + + +// Sets default values for this component's properties +UGCS_BulletSystemComponent::UGCS_BulletSystemComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + + // ... +} + + +// Called when the game starts +void UGCS_BulletSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... +} + +UGCS_BulletSystemComponent* UGCS_BulletSystemComponent::GetBulletSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +bool UGCS_BulletSystemComponent::FindBulletSystemComponent(const AActor* Actor, UGCS_BulletSystemComponent*& Component) +{ + Component = GetBulletSystemComponent(Actor); + return Component != nullptr; +} + +void UGCS_BulletSystemComponent::SpawnBullet(const FGCS_BulletSpawnParameters& SpawnParameters) +{ + //SpawnBulletInternal(SpawnParameters); +} + +// TArray UGCS_BulletSystemComponent::SpawnBulletInternal(const FGCS_BulletSpawnParameters& SpawnParameters) +// { +// TArray RetInstances{}; +// +// if (GetOwner()->GetLocalRole() < ROLE_AutonomousProxy) +// { +// GCS_CLOG(Warning, "No authority to spawn bullet!") +// return RetInstances; +// } +// +// UGCS_BulletSubsystem* Subsystem = UGCS_BulletSubsystem::Get(GetWorld()); +// if (Subsystem == nullptr) +// { +// return RetInstances; +// } +// +// FGCS_BulletDefinition Definition; +// if (SpawnParameters.DefinitionHandle.IsNull() || !Subsystem->LoadBulletDefinition(SpawnParameters.DefinitionHandle, Definition)) +// { +// return RetInstances; +// } +// +// RetInstances = Subsystem->GetOrCreateBulletInstances(SpawnParameters, Definition); +// +// for (int i = 0; i < RetInstances.Num(); ++i) +// { +// AGCS_BulletInstance* Instance = RetInstances[i]; +// +// Instance->bServerInitiated = GetWorld()->GetNetMode() < NM_Client; +// Instance->bIsLocalPredicting = SpawnParameters.bIsLocalPredicting; +// +// if (SpawnParameters.OverrideBulletIds.IsValidIndex(i)) +// { +// Instance->SetBulletId(SpawnParameters.OverrideBulletIds[0]); +// } +// else +// { +// Instance->SetBulletId(FGuid::NewGuid()); +// } +// +// if (SpawnParameters.ParentId.IsValid() && BulletInstances.Contains(SpawnParameters.ParentId)) +// { +// Instance->SetParentBulletId(SpawnParameters.ParentId); +// // if (AGCS_BulletInstance* ParentBulletInstance = BulletInstances[SpawnParameters.ParentId]) +// // { +// // if (ParentBulletInstance->Definition.bUseSharedHitList) +// // { +// // Instance->HitActors = ParentBulletInstance->HitActors; +// // } +// // } +// } +// +// if (SpawnParameters.Request) +// { +// Instance->Request = SpawnParameters.Request; +// } +// +// if (SpawnParameters.Owner) +// { +// Instance->SetOwner(SpawnParameters.Owner); +// } +// Instance->SetDefinitionHandle(SpawnParameters.DefinitionHandle); +// BulletInstances.Emplace(Instance->BulletId, Instance); +// } +// +// FRotator OriginalRotation = SpawnParameters.SpawnTransform.Rotator(); +// for (int i = 0; i < RetInstances.Num(); ++i) +// { +// FTransform ModifiedTransform = SpawnParameters.SpawnTransform; +// FRotator RotationYawOffset(Definition.LaunchElevationAngle, Definition.LaunchAngle + Definition.LaunchAngleInterval * i, 0); +// ModifiedTransform.SetRotation(UKismetMathLibrary::ComposeRotators(OriginalRotation, RotationYawOffset).Quaternion()); +// RetInstances[i]->SetActorTransform(ModifiedTransform, false, nullptr, ETeleportType::ResetPhysics); +// } +// +// //batch beginplay. +// for (AGCS_BulletInstance* Instance : RetInstances) +// { +// Instance->OnBulletBeginPlay(); +// } +// return RetInstances; +// } diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_SphereBulletInstance.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_SphereBulletInstance.cpp new file mode 100644 index 0000000..fecfb1a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Bullet/GCS_SphereBulletInstance.cpp @@ -0,0 +1,36 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Bullet/GCS_SphereBulletInstance.h" + +#include "Components/SphereComponent.h" + + +// Sets default values +AGCS_SphereBulletInstance::AGCS_SphereBulletInstance() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + + Sphere = CreateDefaultSubobject("Sphere"); + SetRootComponent(Sphere); +} + +UShapeComponent* AGCS_SphereBulletInstance::GetBulletShape_Implementation() const +{ + return Sphere; +} + +// Called when the game starts or when spawned +void AGCS_SphereBulletInstance::BeginPlay() +{ + Super::BeginPlay(); + +} + +// Called every frame +void AGCS_SphereBulletInstance::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); +} + diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/DEPRECATED_GCS_CollisionTraceInstance.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/DEPRECATED_GCS_CollisionTraceInstance.cpp new file mode 100644 index 0000000..7c204bb --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/DEPRECATED_GCS_CollisionTraceInstance.cpp @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" + +#include "GCS_LogChannels.h" +#include "Collision/GCS_TraceSystemComponent.h" +#include "Components/PrimitiveComponent.h" + +void UDEPRECATED_GCS_CollisionTraceInstance::OnTraceBeginPlay_Implementation() +{ + ActiveTime = 0.0f; + HitActors.Empty(); +} + +void UDEPRECATED_GCS_CollisionTraceInstance::OnTraceEndPlay_Implementation() +{ + ActiveTime = 0.0f; + bTraceActive = false; + TracePrimitiveComponent = nullptr; + TracePrimitiveComponentSocketNames.Empty(); + HitActors.Empty(); +} + +void UDEPRECATED_GCS_CollisionTraceInstance::BroadcastHit(const FHitResult& HitResult) +{ + if (!bTraceActive) + { + UE_LOG(LogGCS_Collision, Warning, TEXT("Hit while inactive,%s"), *TraceOwner->GetName()); + } +} + +void UDEPRECATED_GCS_CollisionTraceInstance::BroadcastStateChanged(bool bNewState) +{ + OnTraceStateChanged(bNewState); +} + +void UDEPRECATED_GCS_CollisionTraceInstance::OnTraceTick_Implementation(float DeltaSeconds) +{ + ActiveTime += DeltaSeconds; +} + +void UDEPRECATED_GCS_CollisionTraceInstance::OnTraceStateChanged_Implementation(bool bNewState) +{ + bTraceActive = bNewState; + HitActors.Empty(); +} + +void UDEPRECATED_GCS_CollisionTraceInstance::SetTraceMeshInfo(UPrimitiveComponent* NewPrimitiveComponent, TArray PrimitiveComponentSocketNames) +{ + TracePrimitiveComponent = NewPrimitiveComponent; + TracePrimitiveComponentSocketNames = PrimitiveComponentSocketNames; +} + +bool UDEPRECATED_GCS_CollisionTraceInstance::CanHitActor_Implementation(const AActor* ActorToCheck) const +{ + //in active trace can not hit anything. TODO make it checkf? + if (!bTraceActive) + { + return false; + } + + return ActorToCheck != GetTraceSourceActor() && ActorToCheck != TraceOwner && !HitActors.Contains(ActorToCheck); +} + +AActor* UDEPRECATED_GCS_CollisionTraceInstance::GetTraceSourceActor() const +{ + return TracePrimitiveComponent->GetOwner(); +} + +void UDEPRECATED_GCS_CollisionTraceInstance::ToggleTraceState(bool bNewState) +{ + if (TraceOwner && bTraceActive != bNewState) + { + BroadcastStateChanged(bNewState); + } +} + +void UDEPRECATED_GCS_CollisionTraceInstance::OnTraceHit_Implementation(const FHitResult& HitResult) +{ + if (HitResult.GetHitObjectHandle().IsValid()) + { + if (CanHitActor(HitResult.GetActor())) + { + UE_LOG(LogGCS_Collision, VeryVerbose, TEXT("%s's Trace(%s, SourceActor:%s) hit actor(%s,Comp:%s)"), *TraceOwner->GetName(), + *(TraceGameplayTag.IsValid()?TraceGameplayTag.ToString():GetClass()->GetName()), + *GetTraceSourceActor()->GetName(), + *HitResult.GetActor()->GetName(), *(HitResult.Component.IsValid()?HitResult.GetComponent()->GetName():TEXT("Null"))); + HitActors.Add(HitResult.GetActor()); + BroadcastHit(HitResult); + } + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_AsyncAction_CollisionTrace.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_AsyncAction_CollisionTrace.cpp new file mode 100644 index 0000000..b3b18d9 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_AsyncAction_CollisionTrace.cpp @@ -0,0 +1,105 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Collision/GCS_AsyncAction_CollisionTrace.h" +#include "Collision/GCS_TraceSystemComponent.h" +#include "Components/PrimitiveComponent.h" +#include "Engine/Engine.h" + +UGCS_AsyncAction_CollisionTrace* UGCS_AsyncAction_CollisionTrace::SetupAndListenForCollisionTraceHit(UGCS_TraceSystemComponent* TraceSystem, + const TArray& TraceDefinitions, + UPrimitiveComponent* PrimitiveComponent, + UObject* OptionalSourceObject) +{ + if (TraceSystem == nullptr) + { + FFrame::KismetExecutionMessage(TEXT("SetupAndListenForCollisionTraceHit was passed a null TraceSystem"), ELogVerbosity::Error); + return nullptr; + } + + if (PrimitiveComponent == nullptr) + { + FFrame::KismetExecutionMessage(TEXT("SetupAndListenForCollisionTraceHit was passed a null PrimitiveComponent"), ELogVerbosity::Error); + return nullptr; + } + + if (TraceDefinitions.IsEmpty()) + { + FFrame::KismetExecutionMessage(TEXT("SetupAndListenForCollisionTraceHit was passed empty TraceDefinitions"), ELogVerbosity::Error); + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(TraceSystem, EGetWorldErrorMode::LogAndReturnNull); + if (!World) + { + return nullptr; + } + + UGCS_AsyncAction_CollisionTrace* Action = NewObject(); + + Action->TraceDefinitions = TraceDefinitions; + Action->TraceSystem = TraceSystem; + Action->SourceComponent = PrimitiveComponent; + Action->SourceObject = OptionalSourceObject; + Action->RegisterWithGameInstance(World); + + return Action; +} + +void UGCS_AsyncAction_CollisionTrace::Activate() +{ + UGCS_TraceSystemComponent* TSC = TraceSystem.Get(); + UPrimitiveComponent* Primitive = SourceComponent.Get(); + UObject* SourceObj = SourceObject.Get(); + + if (TSC && Primitive) + { + TSC->OnTraceHitEvent.AddDynamic(this, &ThisClass::TraceHitCallback); + + static FHitResult EmptyHitResult; + TraceHandles = TSC->AddTraces(TraceDefinitions, Primitive, SourceObj); + for (auto& Handle : TraceHandles) + { + BeforeActive.Broadcast(Handle, EmptyHitResult); + TSC->StartTrace(Handle); + } + } + else + { + SetReadyToDestroy(); + } +} + +void UGCS_AsyncAction_CollisionTrace::Cancel() +{ + Super::Cancel(); + UGCS_TraceSystemComponent* TSC = TraceSystem.Get(); + UPrimitiveComponent* Primitive = SourceComponent.Get(); + if (TSC && Primitive) + { + TSC->OnTraceHitEvent.RemoveAll(this); + //Deactivate traces. + for (const FGCS_TraceHandle& Handle : TraceHandles) + { + if (Handle.IsValidHandle()) + { + TSC->RemoveTrace(Handle); + } + } + TraceHandles.Empty(); + SourceComponent = nullptr; + TraceSystem = nullptr; + SourceObject = nullptr; + } +} + +void UGCS_AsyncAction_CollisionTrace::TraceHitCallback(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult) +{ + if (ShouldBroadcastDelegates() && TraceHandle.IsValidHandle()) + { + if (TraceHandles.Contains(TraceHandle)) + { + OnHit.Broadcast(TraceHandle, HitResult); + } + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceDelegates.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceDelegates.cpp new file mode 100644 index 0000000..eabc4d3 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceDelegates.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Collision/GCS_TraceDelegates.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCS_TraceDelegates) diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceEnumLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceEnumLibrary.cpp new file mode 100644 index 0000000..4ffa874 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceEnumLibrary.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Collision/GCS_TraceEnumLibrary.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceFunctionLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceFunctionLibrary.cpp new file mode 100644 index 0000000..bcb83b5 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceFunctionLibrary.cpp @@ -0,0 +1,13 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Collision/GCS_TraceFunctionLibrary.h" + + +TArray UGCS_TraceFunctionLibrary::FilterTraceDefinitionsByTag(const TArray& Definitions, const FGameplayTag& TagToMatch) +{ + return Definitions.FilterByPredicate([TagToMatch](const FGCS_TraceDefinition& Definition) + { + return Definition.TraceTag.IsValid() && Definition.CollisionShape.IsValid() && Definition.TraceTag.MatchesTag(TagToMatch); + }); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceStructLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceStructLibrary.cpp new file mode 100644 index 0000000..83ec0b7 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceStructLibrary.cpp @@ -0,0 +1,249 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Collision/GCS_TraceStructLibrary.h" + +#include "GCS_LogChannels.h" +#include "Components/BoxComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/SphereComponent.h" +#include "TargetingSystem/TargetingPreset.h" + + +bool FGCS_CollisionShape::InitializeShape(const UPrimitiveComponent* SourceComponent) +{ + GCS_OWNED_CLOG(SourceComponent, Warning, "Should never use this shape:%s", *FGCS_CollisionShape::StaticStruct()->GetName()); + FDebug::DumpStackTraceToLog(ELogVerbosity::VeryVerbose); + return false; +} + +FTransform FGCS_CollisionShape::GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + GCS_OWNED_CLOG(SourceComponent, Warning, "Should never use this shape:%s", *FGCS_CollisionShape::StaticStruct()->GetName()); + FDebug::DumpStackTraceToLog(ELogVerbosity::VeryVerbose); + return SourceComponent->GetComponentTransform(); +} + +FCollisionShape FGCS_CollisionShape::GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + GCS_OWNED_CLOG(SourceComponent, Warning, "Should never use this shape:%s", *FGCS_CollisionShape::StaticStruct()->GetName()); + FDebug::DumpStackTraceToLog(ELogVerbosity::VeryVerbose); + return FCollisionShape::MakeSphere(30); +} + +bool FGCS_CollisionShape_Static::InitializeShape(const UPrimitiveComponent* SourceComponent) +{ + return true; +} + +FTransform FGCS_CollisionShape_Static::GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + const auto ComponentCurrentTransform = SourceComponent->GetComponentTransform(); + return FTransform(Orientation, Offset) * ComponentCurrentTransform; +} + +FCollisionShape FGCS_CollisionShape_Static::GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + switch (ShapeType) + { + case EGCS_CollisionShapeType::Sphere: + { + return FCollisionShape::MakeSphere(Radius); + } + case EGCS_CollisionShapeType::Box: + { + return FCollisionShape::MakeBox(HalfSize); + } + case EGCS_CollisionShapeType::Capsule: + { + return FCollisionShape::MakeCapsule(Radius, HalfHeight); + } + default: + { + return FCollisionShape::MakeSphere(Radius); + } + } +} + +bool FGCS_CollisionShape_ShapeBased::InitializeShape(const UPrimitiveComponent* SourceComponent) +{ + if (ShapeType == EGCS_CollisionShapeType::Sphere) + { + if (const auto SphereCollision = Cast(SourceComponent)) + { + Radius = SphereCollision->GetScaledSphereRadius(); + return true; + } + GCS_OWNED_CLOG(SourceComponent->GetOwner(), Warning, "No compatible shape component was found! Requires SphereComponent, Got %s(%s)", *GetNameSafe(SourceComponent), + *SourceComponent->GetClass()->GetName()) + } + if (ShapeType == EGCS_CollisionShapeType::Box) + { + if (const auto BoxCollision = Cast(SourceComponent)) + { + HalfSize = BoxCollision->GetScaledBoxExtent(); + return true; + } + GCS_OWNED_CLOG(SourceComponent->GetOwner(), Warning, "No compatible shape component was found! Requires BoxComponent, Got %s(%s)", *GetNameSafe(SourceComponent), + *SourceComponent->GetClass()->GetName()) + } + if (ShapeType == EGCS_CollisionShapeType::Capsule) + { + if (const auto CapsuleCollision = Cast(SourceComponent)) + { + HalfHeight = CapsuleCollision->GetScaledCapsuleHalfHeight(); + Radius = CapsuleCollision->GetScaledCapsuleRadius(); + return true; + } + GCS_OWNED_CLOG(SourceComponent->GetOwner(), Warning, "No compatible shape component was found! Requires CapsuleComponent, Got %s(%s)", *GetNameSafe(SourceComponent), + *SourceComponent->GetClass()->GetName()) + } + + return false; +} + +FTransform FGCS_CollisionShape_ShapeBased::GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + const auto ComponentCurrentTransform = SourceComponent->GetComponentTransform(); + return FTransform(Orientation, Offset) * ComponentCurrentTransform; +} + +FCollisionShape FGCS_CollisionShape_ShapeBased::GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + switch (ShapeType) + { + case EGCS_CollisionShapeType::Sphere: + { + return FCollisionShape::MakeSphere(Radius); + } + case EGCS_CollisionShapeType::Box: + { + return FCollisionShape::MakeBox(HalfSize); + } + case EGCS_CollisionShapeType::Capsule: + { + return FCollisionShape::MakeCapsule(Radius, HalfHeight); + } + default: + { + return FCollisionShape::MakeSphere(Radius); + } + } +} + +bool FGCS_CollisionShape_Attached::InitializeShape(const UPrimitiveComponent* SourceComponent) +{ + if (!IsValid(SourceComponent) || !SourceComponent->DoesSocketExist(SocketOrBoneName)) + { + GCS_OWNED_CLOG(SourceComponent->GetOwner(), Warning, "No SocketOrBone(%s) exists on mesh component! Got %s(%s)", *SocketOrBoneName.ToString(), *GetNameSafe(SourceComponent), + *SourceComponent->GetClass()->GetName()) + FDebug::DumpStackTraceToLog(ELogVerbosity::VeryVerbose); + return false; + } + return true; +} + +FTransform FGCS_CollisionShape_Attached::GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + const auto BoneTransform = SourceComponent->GetSocketTransform(SocketOrBoneName); + return FTransform(Orientation, Offset) * BoneTransform; +} + +FTransform FGCS_CollisionShape_SocketBased::GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + const auto ComponentCurrentTransform = SourceComponent->GetComponentTransform(); + return FTransform(Orientation, Offset) * ComponentCurrentTransform; +} + +bool FGCS_CollisionShape_SocketBased::InitializeShape(const UPrimitiveComponent* SourceComponent) +{ + if (const UMeshComponent* MeshComponent = Cast(SourceComponent)) + { + const auto SocketStartTransform = MeshComponent->GetSocketTransform(MeshSocketStart, RTS_Component); + const auto SocketStartLocation = SocketStartTransform.GetLocation(); + + const auto SocketEndTransform = MeshComponent->GetSocketTransform(MeshSocketEnd, RTS_Component); + const auto SocketEndLocation = SocketEndTransform.GetLocation(); + + const FVector CenterLocation = (SocketStartLocation + SocketEndLocation) / 2.f; + + HalfHeight = FMath::Max((SocketStartLocation - SocketEndLocation).Length() / 2.f + MeshSocketLengthOffset, 1.f); + Offset = CenterLocation; + Orientation = FRotationMatrix::MakeFromZ(SocketStartLocation - SocketEndLocation).Rotator(); + + return true; + } + GCS_OWNED_CLOG(SourceComponent->GetOwner(), Warning, "No compatible mesh component was found! Got %s(%s)", *GetNameSafe(SourceComponent), + *SourceComponent->GetClass()->GetName()) + return false; +} + +FCollisionShape FGCS_CollisionShape_SocketBased::GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const +{ + return FCollisionShape::MakeCapsule(Radius, HalfHeight); +} + +FGCS_TraceDefinition::FGCS_TraceDefinition() +{ + CollisionShape.InitializeAs(FGCS_CollisionShape_Static::StaticStruct()); +} + +bool FGCS_TraceDefinition::IsValidDefinition() const +{ + // Base collision shape is not allowed! + return TraceTag.IsValid() && CollisionShape.IsValid() && CollisionShape.GetScriptStruct() != FGCS_CollisionShape::StaticStruct(); +} + +FString FGCS_TraceDefinition::ToString() const +{ + return FString::Format(TEXT("{0} {1}"), {TraceTag.ToString(), GetNameSafe(CollisionShape.GetScriptStruct())}); +} + +bool FGCS_TraceHandle::IsValidHandle() const +{ + return TraceTag.IsValid() && Guid.IsValid() && SourceObject.IsValid(); +} + +void FGCS_TraceState::ChangeExecutionState(const bool bNewTraceState, const bool bStopImmediate) +{ + if (bNewTraceState) + { + this->ExecutionState = EGCS_TraceExecutionState::InProgress; + this->TimeSinceLastTick = 0; + this->TimeSinceActive = 0; + this->TotalTickNumDuringExecution = 0; + this->HitActors.Reset(); + if (SourceComponent) + { + UpdatePreviousTransform(GetCurrentTransform()); + } + } + else + { + if (this->ExecutionState == EGCS_TraceExecutionState::InProgress && !bStopImmediate) + { + this->ExecutionState = EGCS_TraceExecutionState::PendingStop; + } + else + { + this->ExecutionState = EGCS_TraceExecutionState::Stopped; + } + } +} + +FTransform FGCS_TraceState::GetCurrentTransform() const +{ + check(Shape.IsValid()) + return Shape.Get().GetTransform(SourceComponent.Get(), TimeSinceActive); +} + +void FGCS_TraceState::UpdatePreviousTransform(const FTransform& Transform) +{ + if (TransformsOverTime.Num() == 0) + { + TransformsOverTime.Add(Transform); + } + else + { + TransformsOverTime[0] = Transform; + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSubsystem.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSubsystem.cpp new file mode 100644 index 0000000..7d9d9b6 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSubsystem.cpp @@ -0,0 +1,485 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Collision/GCS_TraceSubsystem.h" + +#include "GCS_LogChannels.h" +#include "KismetTraceUtils.h" +#include "Async/ParallelFor.h" +#include "Components/StaticMeshComponent.h" +#include "Collision/GCS_TraceSystemComponent.h" +#include "Kismet/KismetMathLibrary.h" + +#define LOCTEXT_NAMESPACE "UGCS_CollisionTraceSubsystem" + +// CVars +namespace GenericCombatSystemCVars +{ +#if ENABLE_DRAW_DEBUG + + static bool bEnableTraceDebugging = false; + FAutoConsoleVariableRef CvarForceEnableTraceDebugging( + TEXT("gcs.debug.EnableTraceDebugging"), + bEnableTraceDebugging, + TEXT("Toggles whether enable draw debugs for all traces. (Enabled: true, Disabled: false)")); + + static float OverrideTraceDebuggingLifeTime = 0.f; + FAutoConsoleVariableRef CvarOverrideTraceDebuggingLifeTime( + TEXT("gcs.debug.OverrideTraceDebuggingLifeTime"), + OverrideTraceDebuggingLifeTime, + TEXT("Overrides the draws life time to ease the trace debugging")); +#endif +} + +void UGCS_TraceSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + this->TraceStates.Reserve(4068); +} + +void UGCS_TraceSubsystem::Tick(float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::Tick"), STAT_UGCS_TraceSubsystem_Tick, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + TickIdx++; + // Lock removals so we don't get any modifications to the array while we are iterating + RemovalLock = true; + + PreTraceTick(DeltaTime); + + PrepareSubTicks(DeltaTime); + PerformSubTicks(DeltaTime); + + PostTraceTick(); + + // Reverse iterate, remove pending removals + RemovalLock = false; + for (int Idx = TraceStates.Num() - 1; Idx >= 0; Idx--) + { + const auto& TraceState = TraceStates[Idx]; + if (!TraceStates.IsValidIndex(Idx)) + { + continue; + } + + if (TraceState.IsPendingRemoval) + { + RemoveTraceStateAt(Idx, TraceState.Handle.Guid); + } + } +} + +void UGCS_TraceSubsystem::RemoveTraceState(const int Idx, const FGuid Guid) +{ + if (RemovalLock) + { + if (TraceStates.IsValidIndex(Idx) && TraceStates[Idx].Handle.Guid == Guid) + { + TraceStates[Idx].IsPendingRemoval = true; + } + } + else + { + RemoveTraceStateAt(Idx, Guid); + } +} + +int32 UGCS_TraceSubsystem::AddTraceState() +{ + FScopeLock ScopeLock(&CriticalSection); + return TraceStates.AddDefaulted(); +} + +bool UGCS_TraceSubsystem::IsValidStateIdx(int32 StateIdx) const +{ + return TraceStates.IsValidIndex(StateIdx); +} + +FGCS_TraceState& UGCS_TraceSubsystem::GetTraceStateAt(const int Index) +{ + return TraceStates[Index]; +} + +void UGCS_TraceSubsystem::RemoveTraceStateAt(const int Idx, const FGuid Guid) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::RemoveTraceStateAt"), STAT_UGCS_TraceSubsystem_RemoveTraceStateAt, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + FScopeLock ScopeLock(&CriticalSection); + if (TraceStates.IsValidIndex(Idx) && TraceStates[Idx].Handle.Guid == Guid) + { + // 移除并交换 + TraceStates.RemoveAtSwap(Idx); + + // If a state was moved to Idx, update its corresponding handle in the OwningSystem + if (TraceStates.IsValidIndex(Idx)) + { + FGCS_TraceState& MovedState = TraceStates[Idx]; + if (IsValid(MovedState.OwningSystem)) + { + // Update the Component's HandleToStateIdx map + MovedState.OwningSystem->HandleToStateIdx.Remove(MovedState.Handle); // Remove old mapping + MovedState.OwningSystem->HandleToStateIdx.Add(MovedState.Handle, Idx); // Add new mapping + } + // mark pending removal if owning system become invalid. + else + { + MovedState.IsPendingRemoval = true; + } + } + } +} + +void UGCS_TraceSubsystem::PreTraceTick(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PreTraceTick"), STAT_UGCS_TraceSubsystem_PreTraceTick, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + ParallelFor(TraceStates.Num(), [&](const int32 Idx) + { + if (!TraceStates.IsValidIndex(Idx)) + { + return; + } + auto& TraceState = TraceStates[Idx]; + if (TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped) + { + return; + } + + check(TraceState.bShouldTickThisFrame == false) + + if (!IsValid(TraceState.SourceComponent)) + { + TraceState.ExecutionState = EGCS_TraceExecutionState::Stopped; + TraceState.bShouldTickThisFrame = false; + return; + } + + const FTransform& CurrentTransform = TraceState.GetCurrentTransform(); + + if (TraceState.TransformsOverTime.IsEmpty()) + { + TraceState.TransformsOverTime.Add(CurrentTransform); + } + + if (TraceState.ExecutionState == EGCS_TraceExecutionState::PendingStop) + { + TraceState.bShouldTickThisFrame = true; + TraceState.TransformsOverTime.Add(CurrentTransform); + return; + } + + switch (TraceState.TickPolicy) + { + case EGCS_TraceTickType::Default: + TraceState.TransformsOverTime.Add(CurrentTransform); + TraceState.bShouldTickThisFrame = true; + return; + case EGCS_TraceTickType::DistanceBased: + { + const FVector& PreviousLocation = TraceState.TransformsOverTime[0].GetLocation(); + const FVector& CurrentLocation = CurrentTransform.GetLocation(); + const FRotator& PreviousRotation = TraceState.TransformsOverTime[0].GetRotation().Rotator(); + const FRotator& CurrentRotation = CurrentTransform.GetRotation().Rotator(); + + bool bDistanceThresholdMet = (PreviousLocation - CurrentLocation).Length() >= TraceState.TickInterval; + bool bAngleThresholdMet = FMath::Abs(PreviousRotation.Yaw - CurrentRotation.Yaw) >= TraceState.AngleThreshold || + FMath::Abs(PreviousRotation.Pitch - CurrentRotation.Pitch) >= TraceState.AngleThreshold || + FMath::Abs(PreviousRotation.Roll - CurrentRotation.Roll) >= TraceState.AngleThreshold; + + if (bDistanceThresholdMet || bAngleThresholdMet) + { + TraceState.TransformsOverTime.Add(CurrentTransform); + TraceState.bShouldTickThisFrame = true; + } + return; + } + + case EGCS_TraceTickType::FixedFrameRate: + TraceState.TimeSinceLastTick += DeltaTime; + int32 SubTickCountPreview = FMath::FloorToInt(TraceState.TimeSinceLastTick / TraceState.TickInterval); + if (SubTickCountPreview > 0) + { + TraceState.TransformsOverTime.Add(CurrentTransform); + TraceState.bShouldTickThisFrame = true; + } + } + }); +} + +void UGCS_TraceSubsystem::PostTraceTick() +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PostTraceTick"), STAT_UGCS_TraceSubsystem_PostTraceTick, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + ParallelFor(TraceStates.Num(), [&](const int32 Idx) + { + auto& TraceState = TraceStates[Idx]; + if (!TraceState.bShouldTickThisFrame) + { + return; + } + + // Reset time since last tick. + if (TraceState.TickPolicy == EGCS_TraceTickType::FixedFrameRate) + { + int32 SubTickCount = TraceState.SubTicks.Num(); + TraceState.TimeSinceLastTick -= SubTickCount * TraceState.TickInterval; + + TraceState.TimeSinceLastTick = FMath::Max(TraceState.TimeSinceLastTick, 0.0f); + } + else + { + TraceState.TimeSinceLastTick = 0; + } + + TraceState.bShouldTickThisFrame = false; + + if (TraceState.ExecutionState == EGCS_TraceExecutionState::PendingStop) + { + TraceState.ExecutionState = EGCS_TraceExecutionState::Stopped; + TraceState.TransformsOverTime.Empty(); + TraceState.TimeSinceActive = 0; + } + else + { + if (!TraceState.TransformsOverTime.IsEmpty()) + { + TraceState.TransformsOverTime.RemoveAtSwap(0); + } + } + }); +} + +void UGCS_TraceSubsystem::PrepareSubTicks(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PrepareSubTicks"), STAT_UGCS_TraceSubsystem_PrepareSubTicks, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + ParallelFor(TraceStates.Num(), [&](const int32 Idx) + { + if (!TraceStates.IsValidIndex(Idx)) + { + return; + } + auto& TraceState = TraceStates[Idx]; + + TraceState.SubTicks.Reset(); + if (TraceState.bShouldTickThisFrame) + { + int32 SubTickCount = 1; + if (TraceState.TickPolicy == EGCS_TraceTickType::DistanceBased) + { + SubTickCount = FMath::CeilToInt((TraceState.TransformsOverTime[0].GetLocation() - TraceState.TransformsOverTime[1].GetLocation()).Length() / TraceState.TickInterval); + } + else if (TraceState.TickPolicy == EGCS_TraceTickType::FixedFrameRate) + { + SubTickCount = FMath::FloorToInt(TraceState.TimeSinceLastTick / TraceState.TickInterval); + } + SubTickCount = FMath::Min(10, SubTickCount); + + if (TraceState.TransformsOverTime.Num() > 1) + { + const float SubTickRatio = 1.0 / SubTickCount; + const FTransform CurrentTransform = TraceState.TransformsOverTime.Last(); + const FTransform PreviousTransform = TraceState.TransformsOverTime[0]; + TraceState.CollisionShapeOverTime = TraceState.Shape.Get().GetDynamicCollisionShape(TraceState.SourceComponent, TraceState.TimeSinceActive); + for (int32 i = 0; i < SubTickCount; i++) + { + FGCS_TraceSubTick SubTick{}; + SubTick.StartTransform = UKismetMathLibrary::TLerp(PreviousTransform, CurrentTransform, SubTickRatio * i, ELerpInterpolationMode::DualQuatInterp); + SubTick.EndTransform = UKismetMathLibrary::TLerp(PreviousTransform, CurrentTransform, SubTickRatio * (i + 1), ELerpInterpolationMode::DualQuatInterp); + SubTick.AverageTransform = UKismetMathLibrary::TLerp(SubTick.StartTransform, SubTick.EndTransform, 0.5, ELerpInterpolationMode::DualQuatInterp); + TraceState.SubTicks.Add(SubTick); + } + } + else + { + GCS_LOG(Warning, "Trace [%s] has less than 2 transforms in TransformsOverTime array!", *TraceState.Handle.TraceTag.ToString()) + } + } + + if (TraceState.ExecutionState == EGCS_TraceExecutionState::InProgress) + { + TraceState.TimeSinceActive += DeltaTime; + } + }); +} + +void UGCS_TraceSubsystem::PerformSubTicks(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PerformSubTicks"), STAT_UGCS_TraceSubsystem_PerformSubTicks, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + for (int32 Idx = 0; Idx < TraceStates.Num(); Idx++) + { + if (!TraceStates.IsValidIndex(Idx)) + { + continue; + } + auto& TraceState = TraceStates[Idx]; + + if (TraceState.bShouldTickThisFrame) + { + for (int32 SubTickIdx = 0; SubTickIdx < TraceState.SubTicks.Num(); SubTickIdx++) + { + const FGCS_TraceSubTick& SubTick = TraceState.SubTicks[SubTickIdx]; + + // Respect cancellations by user-defined code immediately. + if (TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped) + { + break; + } + TraceState.TotalTickNumDuringExecution += 1; + + FTraceDelegate Delegate = FTraceDelegate::CreateUObject(this, &UGCS_TraceSubsystem::HandleTraceResults, Idx, TickIdx, TraceState.TimeSinceActive); + + PerformAsyncTrace(SubTick.StartTransform, SubTick.EndTransform, SubTick.AverageTransform, TraceState.World, TraceState.SweepSetting, + TraceState.CollisionShapeOverTime, + TraceState.CollisionParams, + TraceState.ResponseParams, + TraceState.ObjectQueryParams, &Delegate); + } + } + } +} + +void UGCS_TraceSubsystem::HandleTraceResults(const FTraceHandle& InTraceHandle, FTraceDatum& InTraceDatum, int32 InTraceStateIdx, uint32 InTickIdx, float InShapeTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::HandleTraceResults"), STAT_UGCS_TraceSubsystem_HandleTraceResults, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + if (!TraceStates.IsValidIndex(InTraceStateIdx)) + { + return; + } + + auto& TraceState = TraceStates[InTraceStateIdx]; + + if (!IsValid(TraceState.OwningSystem) || TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped) + { + TraceState.TimeSinceActive = 0; + return; + } + + // GCS_LOG(Warning, "Trace [%s] has ticked %d within %f s", *TraceState.Handle.TraceTag.ToString(), TraceState.TotalTickNumDuringExecution, TraceState.TimeSinceActive) + +#if ENABLE_DRAW_DEBUG + if (GenericCombatSystemCVars::bEnableTraceDebugging) + { + const EDrawDebugTrace::Type DrawDebugType = GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime > 0 ? EDrawDebugTrace::ForDuration : EDrawDebugTrace::ForOneFrame; + const float DrawDebugTime = GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime > 0 + ? GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime + : (DrawDebugType == EDrawDebugTrace::ForOneFrame ? 0.0f : 0.5f); + + const bool bHasAuthority = TraceState.SourceComponent->GetOwner()->HasAuthority(); + + + DrawDebug(InTraceDatum.Start, + InTraceDatum.End, + InTraceDatum.Rot, + InTraceDatum.OutHits, TraceState.Shape.Get().GetDynamicCollisionShape(TraceState.SourceComponent, InShapeTime), TraceState.World, + DrawDebugType, + DrawDebugTime, + bHasAuthority ? FLinearColor::Red : FLinearColor::White, + bHasAuthority ? FLinearColor::Green : FLinearColor::Black); + // GCS_OWNED_CLOG(TraceState.SourceComponent, Display, "Did collision trace: start(%s) end(%s)", *InTraceDatum.Start.ToCompactString(), *InTraceDatum.End.ToCompactString()) + } +#endif // ENABLE_DRAW_DEBUG + + TArray FilteredHits; + for (const FHitResult& NewHit : InTraceDatum.OutHits) + { + if (!TraceState.HitActors.Contains(NewHit.GetActor())) + { + FilteredHits.Add(NewHit); + TraceState.HitActors.Add(NewHit.GetActor()); + } + } + if (FilteredHits.Num() > 0) + { + TraceState.OwningSystem->OnTraceHitDetected(TraceState.Handle, FilteredHits, TraceState.TimeSinceLastTick, InTickIdx); + } +} + +void UGCS_TraceSubsystem::PerformAsyncTrace(const FTransform& StartTransform, const FTransform& EndTransform, const FTransform& AverageTransform, UWorld* World, + const FGCS_TraceSweepSetting& TraceSettings, const FCollisionShape& CollisionShape, const FCollisionQueryParams& CollisionParams, + const FCollisionResponseParams& CollisionResponseParams, + const FCollisionObjectQueryParams& ObjectQueryParams, const FTraceDelegate* InDelegate) +{ + switch (TraceSettings.SweepType) + { + case EGCS_TraceSweepType::ByChannel: + World->AsyncSweepByChannel( + EAsyncTraceType::Multi, + StartTransform.GetLocation(), + EndTransform.GetLocation(), + AverageTransform.GetRotation(), + TraceSettings.TraceChannel, + CollisionShape, + CollisionParams, + CollisionResponseParams, InDelegate); + break; + case EGCS_TraceSweepType::ByObject: + World->AsyncSweepByObjectType( + EAsyncTraceType::Multi, + StartTransform.GetLocation(), + EndTransform.GetLocation(), + AverageTransform.GetRotation(), + ObjectQueryParams, + CollisionShape, + CollisionParams, InDelegate); + break; + case EGCS_TraceSweepType::ByProfile: + World->AsyncSweepByProfile( + EAsyncTraceType::Multi, + StartTransform.GetLocation(), + EndTransform.GetLocation(), + AverageTransform.GetRotation(), + TraceSettings.ProfileName, + CollisionShape, + CollisionParams, InDelegate); + break; + } +} + +#if ENABLE_DRAW_DEBUG +void UGCS_TraceSubsystem::DrawDebug(const FVector& StartLocation, const FVector& EndLocation, const FQuat& Orientation, TArray Hits, const FCollisionShape& CollisionShape, + const UWorld* World, + const EDrawDebugTrace::Type DrawDebugType, float DrawDebugTime, const FLinearColor& DrawDebugColor, + const FLinearColor& DrawDebugHitColor) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::DrawDebug"), STAT_UGCS_TraceSubsystem_DrawDebug, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + // We have to manually find if there is a blocking hit. + bool bHasBlockingHit = false; + for (const FHitResult& HitResult : Hits) + { + if (HitResult.bBlockingHit) + { + bHasBlockingHit = true; + break; + } + } + + switch (CollisionShape.ShapeType) + { + case ECollisionShape::Sphere: + DrawDebugSphereTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetSphereRadius(), DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime); + break; + case ECollisionShape::Capsule: + DrawDebugCapsuleTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetCapsuleRadius(), CollisionShape.GetCapsuleHalfHeight(), Orientation.Rotator(), DrawDebugType, + bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime); + break; + case ECollisionShape::Box: + DrawDebugBoxTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetBox(), Orientation.Rotator(), DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, + DrawDebugTime); + break; + default: + case ECollisionShape::Line: + DrawDebugLineTraceMulti(World, StartLocation, EndLocation, DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime); + break; + } +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSystemComponent.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSystemComponent.cpp new file mode 100644 index 0000000..f43e396 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Collision/GCS_TraceSystemComponent.cpp @@ -0,0 +1,489 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Collision/GCS_TraceSystemComponent.h" +#include "GCS_LogChannels.h" +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" +#include "Collision/GCS_TraceSubsystem.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/PrimitiveComponent.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +UGCS_TraceSystemComponent::UGCS_TraceSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(false); +} + +UGCS_TraceSystemComponent* UGCS_TraceSystemComponent::GetTraceSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +bool UGCS_TraceSystemComponent::FindTraceSystemComponent(const AActor* Actor, UGCS_TraceSystemComponent*& Component) +{ + Component = GetTraceSystemComponent(Actor); + return Component != nullptr; +} + +void UGCS_TraceSystemComponent::OnInitialize_Implementation() +{ + if (UMeshComponent* PrimitiveComp = UGCS_CombatFunctionLibrary::GetMainMeshComponent(GetOwner())) + { + AddTraces(TraceDefinitions, PrimitiveComp, GetOwner()); + } + + bInitialized = true; +} + +void UGCS_TraceSystemComponent::OnDeinitialize_Implementation() +{ + if (bInitialized) + { + RemoveAllTraces(); + bInitialized = false; + } +} + +// Called when the game starts +void UGCS_TraceSystemComponent::BeginPlay() +{ + if (bAutoInitialize) + { + OnInitialize(); + } + Super::BeginPlay(); +} + +void UGCS_TraceSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + OnDeinitialize(); +} + +void UGCS_TraceSystemComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + Super::OnComponentDestroyed(bDestroyingHierarchy); + OnDestroyedEvent.Broadcast(); + + OnDeinitialize(); +} + +TArray UGCS_TraceSystemComponent::AddTraces(const TArray& Definitions, UPrimitiveComponent* SourceComponent, UObject* SourceObject) +{ + TArray Handles; + for (const FGCS_TraceDefinition& Def : Definitions) + { + FGCS_TraceHandle Handle = AddTrace(Def, SourceComponent, SourceObject); + if (Handle.IsValidHandle()) + { + Handles.Add(Handle); + } + } + return Handles; +} + +TArray UGCS_TraceSystemComponent::AddTraces(const TArray& DefinitionHandles, UPrimitiveComponent* SourceComponent, UObject* SourceObject) +{ + TArray Handles; + for (const FDataTableRowHandle& DefHandle : DefinitionHandles) + { + FGCS_TraceHandle Handle = AddTrace(DefHandle, SourceComponent, SourceObject); + if (Handle.IsValidHandle()) + { + Handles.Add(Handle); + } + } + return Handles; +} + +FGCS_TraceHandle UGCS_TraceSystemComponent::AddTrace(const FGCS_TraceDefinition& TraceDefinition, UPrimitiveComponent* SourceComponent, UObject* SourceObject) +{ + if (!TraceDefinition.IsValidDefinition()) + { + GCS_CLOG(Warning, "Try adding invalid trace definition!") + return FGCS_TraceHandle(); + } + + if (!IsValid(SourceObject)) + { + GCS_CLOG(Warning, "Missing source object for trace definition:%s", *TraceDefinition.ToString()) + return FGCS_TraceHandle(); + } + + if (!IsValid(SourceComponent)) + { + GCS_CLOG(Warning, "Missing source component for trace definition:%s", *TraceDefinition.ToString()) + return FGCS_TraceHandle(); + } + + FInstancedStruct Shape = TraceDefinition.CollisionShape; + bool bWasInitialized = Shape.GetMutable().InitializeShape(SourceComponent); + + if (!bWasInitialized) + { + return FGCS_TraceHandle(); + } + + UWorld* World = GetWorld(); + if (!IsValid(World)) + { + GCS_CLOG(Error, "Invalid world context!") + return FGCS_TraceHandle(); + } + + UGCS_TraceSubsystem* Subsystem = World->GetSubsystem(); + if (!IsValid(Subsystem)) + { + GCS_CLOG(Error, "Failed to get TraceSubsystem!") + return FGCS_TraceHandle(); + } + int32 StateIdx = Subsystem->AddTraceState(); + + FGCS_TraceHandle Handle = {TraceDefinition.TraceTag, FGuid::NewGuid(), SourceObject}; + + float TickInterval = 0; + float AngleThreshold = TraceDefinition.AngleTickThreshold; + if (TraceDefinition.TraceTickType == EGCS_TraceTickType::DistanceBased) + { + TickInterval = TraceDefinition.DistanceTickThreshold; + } + else if (TraceDefinition.TraceTickType == EGCS_TraceTickType::FixedFrameRate) + { + TickInterval = 1.0f / static_cast(TraceDefinition.FixedTickFrameRate); + } + auto& TraceState = Subsystem->GetTraceStateAt(StateIdx); + TraceState.World = GetWorld(); + TraceState.SourceComponent = SourceComponent; + TraceState.OwningSystem = this; + + TraceState.SweepSetting = TraceDefinition.SweepSetting; + TraceState.Shape = Shape; + + TraceState.Handle = Handle; + TraceState.ExecutionState = EGCS_TraceExecutionState::Stopped; + TraceState.TimeSinceLastTick = 0; + TraceState.TimeSinceActive = 0; + TraceState.TickPolicy = TraceDefinition.TraceTickType; + TraceState.TickInterval = TickInterval; + TraceState.AngleThreshold = AngleThreshold; + TraceState.bShouldTickThisFrame = false; + if (AActor* Owner = GetOwner()) + { + TraceState.CollisionParams.AddIgnoredActor(Owner); + } + TraceState.CollisionParams.bTraceComplex = TraceDefinition.SweepSetting.bTraceComplex; + TraceState.CollisionParams.bReturnPhysicalMaterial = true; + for (const auto& ObjType : TraceDefinition.SweepSetting.ObjectTypes) + { + TraceState.ObjectQueryParams.AddObjectTypesToQuery(ObjType); + } + + HandleToStateIdx.Add(Handle, StateIdx); + TagToHandles.Add(TraceDefinition.TraceTag, Handle); + GCS_CLOG(Verbose, "Added trace(%s) with source component:%s", *Handle.ToDebugString(), *GetNameSafe(SourceComponent)) + return Handle; +} + +FGCS_TraceHandle UGCS_TraceSystemComponent::AddTrace(const FDataTableRowHandle& TraceDefinitionHandle, UPrimitiveComponent* SourceComponent, UObject* SourceObject) +{ + if (!TraceDefinitionHandle.IsNull()) + { + if (FGCS_TraceDefinition* TraceDefinition = TraceDefinitionHandle.GetRow(TEXT("AddTrace"))) + { + AddTrace(*TraceDefinition, SourceComponent, SourceObject); + } + else + { + GCS_CLOG(Warning, "definition handle doesn't point to valid TraceDefinition Table. %s", *TraceDefinitionHandle.ToDebugString()) + } + } + GCS_CLOG(Warning, "Passed in invalid trace definition handle! %s", *TraceDefinitionHandle.ToDebugString()) + return FGCS_TraceHandle(); +} + +void UGCS_TraceSystemComponent::RemoveTraces(const TArray& TraceHandles) +{ + for (const FGCS_TraceHandle& TraceHandle : TraceHandles) + { + RemoveTrace(TraceHandle); + } +} + +void UGCS_TraceSystemComponent::RemoveTrace(const FGCS_TraceHandle& TraceHandle) +{ + if (TraceHandle.IsValidHandle()) + { + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + UGCS_TraceSubsystem* Subsystem = GetWorld()->GetSubsystem(); + if (IsValid(Subsystem) && Subsystem->IsValidStateIdx(*StateIdx)) + { + auto& State = Subsystem->GetTraceStateAt(*StateIdx); + State.ChangeExecutionState(false); + OnTraceStateChanged(TraceHandle, false); + Subsystem->RemoveTraceState(*StateIdx, TraceHandle.Guid); + GCS_CLOG(Verbose, "Removed trace(%s)", *TraceHandle.ToDebugString()) + } + HandleToStateIdx.Remove(TraceHandle); + TagToHandles.RemoveSingle(TraceHandle.TraceTag, TraceHandle); + } + } +} + +TArray UGCS_TraceSystemComponent::GetTraceHandlesBySource(const UObject* SourceObject) const +{ + TArray Handles; + for (const auto& Pair : HandleToStateIdx) + { + if (Pair.Key.SourceObject == SourceObject) + { + Handles.Add(Pair.Key); + } + } + return Handles; +} + +TArray UGCS_TraceSystemComponent::GetTraceHandlesByTagsAndSource(const FGameplayTagContainer& TraceTags, const UObject* SourceObject) const +{ + TArray Handles; + for (const FGameplayTag& Tag : TraceTags) + { + TArray TagHandles; + TagToHandles.MultiFind(Tag, TagHandles); + for (const FGCS_TraceHandle& Handle : TagHandles) + { + if (!IsValid(SourceObject) || Handle.SourceObject == SourceObject) + { + Handles.AddUnique(Handle); + } + } + } + return Handles; +} + +TArray UGCS_TraceSystemComponent::StartTraces(const FGameplayTagContainer& TraceTags, const UObject* SourceObject) +{ + TArray Handles = GetTraceHandlesByTagsAndSource(TraceTags, SourceObject); + for (const FGCS_TraceHandle& Handle : Handles) + { + StartTrace(Handle); + } + return Handles; +} + +void UGCS_TraceSystemComponent::StartTraces(const TArray& TraceHandles) +{ + for (const FGCS_TraceHandle& Handle : TraceHandles) + { + StartTrace(Handle); + } +} + +void UGCS_TraceSystemComponent::StartTrace(const FGCS_TraceHandle& TraceHandle) +{ + if (TraceHandle.IsValidHandle()) + { + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + auto& State = GetWorld()->GetSubsystem()->GetTraceStateAt(*StateIdx); + GCS_CLOG(Verbose, "Started trace(%s).", *TraceHandle.ToDebugString()) + State.ChangeExecutionState(true); + OnTraceStateChanged(TraceHandle, true); + } + } +} + +void UGCS_TraceSystemComponent::StopTraces(const TArray& TraceHandles) +{ + UGCS_TraceSubsystem* Subsystem = GetWorld()->GetSubsystem(); + for (const FGCS_TraceHandle& TraceHandle : TraceHandles) + { + if (TraceHandle.IsValidHandle()) + { + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + auto& State = Subsystem->GetTraceStateAt(*StateIdx); + GCS_CLOG(Verbose, "Stopped trace(%s).", *TraceHandle.ToDebugString()) + State.ChangeExecutionState(false); + OnTraceStateChanged(TraceHandle, false); + } + } + } +} + +void UGCS_TraceSystemComponent::StopTrace(const FGCS_TraceHandle& TraceHandle) +{ + if (TraceHandle.IsValidHandle()) + { + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + auto& State = GetWorld()->GetSubsystem()->GetTraceStateAt(*StateIdx); + State.ChangeExecutionState(false); + OnTraceStateChanged(TraceHandle, false); + } + } +} + +TArray UGCS_TraceSystemComponent::AddTracesByDefinitions(const TArray& Definitions, UPrimitiveComponent* SourceComponent, + UObject* SourceObject) +{ + return AddTraces(Definitions, SourceComponent, SourceObject); +} + +TArray UGCS_TraceSystemComponent::AddTracesByDefinitionHandles(const TArray& DefinitionHandles, UPrimitiveComponent* SourceComponent, + UObject* SourceObject) +{ + return AddTraces(DefinitionHandles, SourceComponent, SourceObject); +} + +FGCS_TraceHandle UGCS_TraceSystemComponent::AddTraceByDefinition(const FGCS_TraceDefinition& Definition, UPrimitiveComponent* SourceComponent, UObject* SourceObject) +{ + return AddTrace(Definition, SourceComponent, SourceObject); +} + +TArray UGCS_TraceSystemComponent::StartTracesByTagsAndSource(const FGameplayTagContainer& TraceTags, const UObject* SourceObject) +{ + return StartTraces(TraceTags, SourceObject); +} + +void UGCS_TraceSystemComponent::StartTracesByHandles(const TArray& TraceHandles) +{ + return StartTraces(TraceHandles); +} + +void UGCS_TraceSystemComponent::StartTraceByHandle(const FGCS_TraceHandle& TraceHandle) +{ + return StartTrace(TraceHandle); +} + +void UGCS_TraceSystemComponent::StopTracesByHandles(const TArray& TraceHandles) +{ + StopTraces(TraceHandles); +} + +void UGCS_TraceSystemComponent::StopTraceByHandle(const FGCS_TraceHandle& TraceHandle) +{ + StopTrace(TraceHandle); +} + +void UGCS_TraceSystemComponent::RemoveTraceByHandle(const FGCS_TraceHandle& TraceHandle) +{ + RemoveTrace(TraceHandle); +} + +TArray UGCS_TraceSystemComponent::GetTraceHandlesByTag(FGameplayTag TraceToFind) const +{ + TArray Handles; + TagToHandles.MultiFind(TraceToFind, Handles); + return Handles; +} + +AActor* UGCS_TraceSystemComponent::GetTraceSourceActor(const FGCS_TraceHandle& TraceHandle) const +{ + if (UPrimitiveComponent* SourceComponent = GetTraceSourceComponent(TraceHandle)) + { + return SourceComponent->GetOwner(); + } + return nullptr; +} + +UPrimitiveComponent* UGCS_TraceSystemComponent::GetTraceSourceComponent(const FGCS_TraceHandle& TraceHandle) const +{ + if (TraceHandle.IsValidHandle()) + { + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + FGCS_TraceState& State = GetWorld()->GetSubsystem()->GetTraceStateAt(*StateIdx); + return State.SourceComponent; + } + } + return nullptr; +} + +void UGCS_TraceSystemComponent::RemoveAllTraces() +{ + UWorld* World = GetWorld(); + if (!IsValid(World)) + { + return; + } + UGCS_TraceSubsystem* Subsystem = World->GetSubsystem(); + if (!IsValid(Subsystem)) + { + return; + } + + // 新增:收集所有要移除的Idx和Guid,避免边遍历边修改 + TArray> ToRemove; + for (const TPair& Pair : HandleToStateIdx) + { + if (Pair.Key.IsValidHandle()) + { + if (Subsystem->IsValidStateIdx(Pair.Value)) // 额外验证 + { + const FGCS_TraceState& State = Subsystem->GetTraceStateAt(Pair.Value); + ToRemove.Add(TPair(Pair.Value, Pair.Key.Guid)); + } + } + } + + // 先停止所有状态 + for (const auto& Pending : ToRemove) + { + if (Subsystem->IsValidStateIdx(Pending.Key)) + { + auto& State = Subsystem->GetTraceStateAt(Pending.Key); + State.ChangeExecutionState(false); + } + } + + // 然后批量移除 + for (const auto& Pending : ToRemove) + { + Subsystem->RemoveTraceState(Pending.Key, Pending.Value); + } + + // 清空映射 + for (const TPair& Pair : HandleToStateIdx) + { + TagToHandles.RemoveSingle(Pair.Key.TraceTag, Pair.Key); + } + HandleToStateIdx.Empty(); +} + +bool UGCS_TraceSystemComponent::IsTraceActive(const FGCS_TraceHandle& TraceHandle) const +{ + if (const int32* StateIdx = HandleToStateIdx.Find(TraceHandle)) + { + const FGCS_TraceState& State = GetWorld()->GetSubsystem()->GetTraceStateAt(*StateIdx); + return State.ExecutionState == EGCS_TraceExecutionState::InProgress; + } + return false; +} + +void UGCS_TraceSystemComponent::OnTraceHitDetected(const FGCS_TraceHandle& TraceHandle, const TArray& HitResults, const float DeltaTime, const uint32 TickIdx) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSystemComponent::OnTraceHitDetected"), UGCS_TraceSystemComponent_OnTraceHitDetected, STATGROUP_GCS) + + FScopeLock ScopedLock(&TraceDoneScopeLock); + + for (const FHitResult& HitResult : HitResults) + { + // GCS_CLOG(Verbose, "Trace(%s) hit:%s at location(%s)", *TraceHandle.ToDebugString(), *GetNameSafe(HitResult.GetActor()), *HitResult.Location.ToString()) + GCS_CLOG(Verbose, "Trace(%s) hit:%s", *TraceHandle.ToDebugString(), *HitResult.ToString()) + OnTraceHitEvent.Broadcast(TraceHandle, HitResult); + } +} + +void UGCS_TraceSystemComponent::OnTraceStateChanged(const FGCS_TraceHandle& TraceHandle, bool bNewState) +{ + if (bNewState) + { + OnTraceStartedEvent.Broadcast(TraceHandle); + } + else + { + OnTraceStoppedEvent.Broadcast(TraceHandle); + } + OnTraceStateChangedEvent.Broadcast(TraceHandle, bNewState); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AbilityActionSetSettings.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AbilityActionSetSettings.cpp new file mode 100644 index 0000000..44cc3fc --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AbilityActionSetSettings.cpp @@ -0,0 +1,72 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_AbilityActionSetSettings.h" + +bool UGCS_AbilityActionSetSettings::SelectBestAbilityActions(const FGameplayTagContainer& SourceTags, const FGameplayTagContainer& TargetTags, const FGameplayTagContainer& AbilityTags, + TArray& Actions) const +{ + FGCS_AbilityActionSet ActionSet; + bool bFound = false; + + for (int32 i = 0; i < ActionSets.Num(); i++) + { + if (ActionSets[i].AbilityTag.IsValid() && ActionSets[i].AbilityTag.MatchesAny(AbilityTags)) + { + ActionSet = ActionSets[i]; + bFound = true; + break; + } + } + + if (!bFound) + { + return false; + } + + // try finds in layers. + + for (int32 i = 0; i < ActionSet.Layered.Num(); i++) + { + bool bMatchingSource = ActionSet.Layered[i].SourceTagQuery.IsEmpty() || ActionSet.Layered[i].SourceTagQuery.Matches(SourceTags); + bool bMatchingTarget = ActionSet.Layered[i].TargetTagQuery.IsEmpty() || ActionSet.Layered[i].TargetTagQuery.Matches(TargetTags); + if (bMatchingSource && bMatchingTarget) + { + Actions = ActionSet.Layered[i].Actions; + return true; + } + } + + // falback to default. + Actions = ActionSet.Actions; + + return true; +} + +#if WITH_EDITORONLY_DATA +#include "UObject/ObjectSaveContext.h" + +void UGCS_AbilityActionSetSettings::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + + for (FGCS_AbilityActionSet& ActionSet : ActionSets) + { + for (FGCS_AbilityAction& Action : ActionSet.Actions) + { + Action.EditorFriendlyName = Action.Animation != nullptr ? Action.Animation.GetName() : TEXT("Empty Action"); + } + for (FGCS_AbilityActionsWithQuery& Layered : ActionSet.Layered) + { + Layered.EditorFriendlyName = FString::Format(TEXT("Source:({0}) Target({1})"), { + Layered.SourceTagQuery.IsEmpty() ? TEXT("Empty") : Layered.SourceTagQuery.GetDescription(), + Layered.TargetTagQuery.IsEmpty() ? TEXT("Empty") : Layered.TargetTagQuery.GetDescription() + }); + for (FGCS_AbilityAction& Action : Layered.Actions) + { + Action.EditorFriendlyName = Action.Animation != nullptr ? Action.Animation.GetName() : TEXT("Empty Action"); + } + } + } +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackDefinition.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackDefinition.cpp new file mode 100644 index 0000000..83777c4 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackDefinition.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_AttackDefinition.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackRequest.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackRequest.cpp new file mode 100644 index 0000000..d8aaf4f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackRequest.cpp @@ -0,0 +1,189 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_AttackRequest.h" +#include "Engine/DataTable.h" +#include "GCS_CombatEntityInterface.h" +#include "GameFramework/Character.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/PrimitiveComponent.h" +#include "GameFramework/PlayerController.h" +#include "Utility/GCS_CombatFunctionLibrary.h" +#include "Weapon/GCS_WeaponInterface.h" + +FDataTableRowHandle UGCS_AttackRequest_Base::GetAttackDefinitionHandle_Implementation() const +{ + return FDataTableRowHandle(); +} + +FGCS_AttackDefinition UGCS_AttackRequest_Base::GetAttackDefinition() const +{ + if (const FGCS_AttackDefinition* Def = GetAttackDefinitionHandle().GetRow(TEXT("Get Attack Definition from AttackRequest"))) + { + return *Def; + } + return FGCS_AttackDefinition(); +} + +FDataTableRowHandle UGCS_AttackRequest_Melee::GetAttackDefinitionHandle_Implementation() const +{ + return AttackDefinitionHandle; +} + +FGCS_BulletDefinition UGCS_AttackRequest_Bullet::GetBulletDefinition() const +{ + if (auto Def = BulletDefinitionHandle.GetRow(TEXT("Get Bullet Definition from AttackRequest_Bullet"))) + { + return *Def; + } + return FGCS_BulletDefinition(); +} + +FDataTableRowHandle UGCS_AttackRequest_Bullet::GetAttackDefinitionHandle_Implementation() const +{ + return GetBulletDefinition().AttackDefinition; +} + +FVector UGCS_AttackRequest_Bullet::GetPawnTargetingSourceLocation_Implementation(APawn* SourcePawn) const +{ + check(SourcePawn); + + FVector RetLocation = SourcePawn->GetActorLocation(); + if (SourceSocketName != NAME_None) + { + if (UMeshComponent* Mesh = UGCS_CombatFunctionLibrary::GetMainMeshComponent(SourcePawn)) + { + if (Mesh->DoesSocketExist(SourceSocketName)) + { + RetLocation = Mesh->GetSocketLocation(SourceSocketName); + } + } + if (ACharacter* Char = Cast(SourcePawn)) + { + if (Char->GetMesh() && Char->GetMesh()->DoesSocketExist(SourceSocketName)) + { + RetLocation = Char->GetMesh()->GetSocketLocation(SourceSocketName); + } + } + } + + FVector LocalOffsetInWorld = SourcePawn->GetActorTransform().TransformVector(LocationOffset); + return RetLocation + LocalOffsetInWorld; +} + + +FVector UGCS_AttackRequest_Bullet::GetWeaponTargetingSourceLocation_Implementation(APawn* SourcePawn) const +{ + check(SourcePawn) + + FVector RetLocation = SourcePawn->GetActorLocation(); + + if (UObject* CombatImplementer = UGCS_CombatFunctionLibrary::GetCombatEntity(SourcePawn)) + { + if (UObject* WeaponImplementer = IGCS_CombatEntityInterface::Execute_GetCurrentWeapon(CombatImplementer, nullptr)) + { + if (UPrimitiveComponent* PrimitiveComponent = IGCS_WeaponInterface::Execute_GetPrimitiveComponent(WeaponImplementer)) + { + if (SourceWeaponSocketName != NAME_None && PrimitiveComponent->DoesSocketExist(SourceWeaponSocketName)) + { + RetLocation = PrimitiveComponent->GetSocketLocation(SourceWeaponSocketName); + } + else + { + RetLocation = PrimitiveComponent->GetOwner()->GetActorLocation(); + } + } + } + } + + FVector LocalOffsetInWorld = SourcePawn->GetActorTransform().TransformVector(LocationOffset); + return RetLocation + LocalOffsetInWorld; +} + +FTransform UGCS_AttackRequest_Bullet::GetTargetingTransform_Implementation(APawn* SourcePawn, EGCS_AbilityTargetingSourceType Source) const +{ + check(SourcePawn); + + // The caller should determine the transform without calling this if the mode is custom! + check(Source != EGCS_AbilityTargetingSourceType::Custom); + + + static double FocalDistance = 1024.0f; + FVector FocalLoc; + + bool bFocalBased = SourcePawn->Controller != nullptr && (Source == EGCS_AbilityTargetingSourceType::CameraTowardsFocus || Source == EGCS_AbilityTargetingSourceType::PawnTowardsFocus || Source == + EGCS_AbilityTargetingSourceType::WeaponTowardsFocus); + + if (bFocalBased) + { + APlayerController* PC = Cast(SourcePawn->Controller); + FVector CamLoc; + FRotator CamRot; + + if (PC != nullptr) + { + PC->GetPlayerViewPoint(CamLoc, CamRot); + } + else + { + CamLoc = SourceSocketName != NAME_None ? GetPawnTargetingSourceLocation(SourcePawn) : SourcePawn->GetActorLocation() + FVector(0, 0, SourcePawn->BaseEyeHeight); + CamRot = SourcePawn->Controller->GetControlRotation(); + } + + // Determine initial focal point to + FVector AimDir = CamRot.Vector().GetSafeNormal(); + FocalLoc = CamLoc + (AimDir * FocalDistance); + + // Move the start and focal point up in front of pawn + // if (PC) + // { + // const FVector WeaponLoc = GetWeaponTargetingSourceLocation(SourcePawn); + // CamLoc = FocalLoc + (((WeaponLoc - FocalLoc) | AimDir) * AimDir); + // FocalLoc = CamLoc + (AimDir * FocalDistance); + // } + + // valid camera and want camera's location + if (Source == EGCS_AbilityTargetingSourceType::CameraTowardsFocus) + { + // If we're camera -> focus then we're done + return FTransform(CamRot, CamLoc); + } + } + + + // valid camera nd want pawn's location. + if (bFocalBased && Source == EGCS_AbilityTargetingSourceType::PawnTowardsFocus) + { + FVector SourceLoc = GetPawnTargetingSourceLocation(SourcePawn); + + // Return a rotator pointing at the focal point from the source + return FTransform((FocalLoc - SourceLoc).Rotation(), SourceLoc); + } + + // valid camera and want weapon's location. + if (bFocalBased && Source == EGCS_AbilityTargetingSourceType::WeaponTowardsFocus) + { + FVector SourceLoc = GetWeaponTargetingSourceLocation(SourcePawn); + + // Return a rotator pointing at the focal point from the source + return FTransform((FocalLoc - SourceLoc).Rotation(), SourceLoc); + } + + // Either we want the weapon's location, or we failed to find a camera + if (Source == EGCS_AbilityTargetingSourceType::WeaponForward || Source == EGCS_AbilityTargetingSourceType::WeaponTowardsFocus) + { + FVector SourceLoc = GetWeaponTargetingSourceLocation(SourcePawn); + return FTransform(SourcePawn->GetActorQuat(), SourceLoc); + } + + // Either we want the pawn's location, or we failed to find a camera + if (Source == EGCS_AbilityTargetingSourceType::PawnForward || Source == EGCS_AbilityTargetingSourceType::PawnTowardsFocus) + { + // Either we want the pawn's location, or we failed to find a camera + FVector SourceLoc = GetPawnTargetingSourceLocation(SourcePawn); + return FTransform(SourcePawn->GetActorQuat(), SourceLoc); + } + + // If we got here, either we don't have a camera or we don't want to use it, either way go forward + return FTransform(SourcePawn->GetActorQuat(), SourcePawn->GetActorLocation()); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResult.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResult.cpp new file mode 100644 index 0000000..b3d0f1e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResult.cpp @@ -0,0 +1,109 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_AttackResult.h" + +#include "GameplayEffect.h" +#include "GCS_LogChannels.h" +#include "GCS_CombatSystemComponent.h" +#include "CombatFlow/GCS_CombatFlow.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + + +void FGCS_AttackResult::PostReplicatedAdd(const struct FGCS_AttackResultContainer& InArray) +{ +} + +FGCS_AttackResultContainer::FGCS_AttackResultContainer(): CombatFlow(nullptr), CombatSystemComponent(nullptr), MaxSize(5) +{ +} + +FGCS_AttackResultContainer::FGCS_AttackResultContainer(UGCS_CombatFlow* InCombatFlow, int32 InMaxSize): CombatFlow(InCombatFlow), CombatSystemComponent(nullptr), MaxSize(InMaxSize) +{ +} + +FGCS_AttackResultContainer::FGCS_AttackResultContainer(UGCS_CombatSystemComponent* InCombatSystemComponent, int32 InMaxSize): CombatFlow(nullptr), CombatSystemComponent(InCombatSystemComponent), + MaxSize(InMaxSize) +{ +} + +void FGCS_AttackResultContainer::AddEntry(FGCS_AttackResult& NewEntry) +{ + if (Results.Num() >= 5) + { + Results.RemoveAtSwap(0); + MarkArrayDirty(); + } + + Results.Add(NewEntry); + check(CombatSystemComponent != nullptr && CombatSystemComponent->GetOwner()); + + if (CombatSystemComponent->GetCombatFlow()) + { + CombatSystemComponent->GetCombatFlow()->HandleAttackResult(NewEntry); + NewEntry.bConsumed = true; + } + else + { + UE_LOG(LogGCS, Error, TEXT("Missing Combat Flow on %s's combat system component."), *CombatSystemComponent->GetOwner()->GetName()); + } + + MarkItemDirty(NewEntry); +} + +void FGCS_AttackResultContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGCS_AttackResult& Entry = Results[Index]; + + FGCS_ContextPayload_Combat* CombatPayload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(Results[Index].EffectContextHandle); + if (CombatPayload != nullptr && CombatPayload->PredictionKey.IsLocalClientKey()) + { + checkf(!CombatPayload->bIsPredictingContext, TEXT("PredictingContext never should hit this!")) + // PredictionKey will only be valid on the client that predicted it. So if this has a valid PredictionKey, we can assume we already predicted it and shouldn't handle attack results. + if (HasPredictedResultWithPredictedKey(CombatPayload->PredictionKey)) + { + GCS_LOG(Verbose, "Found already predicted attack result!") + Entry.bWasPredicated = true; + } + } + Entry.bWasReplicated = true; + + CombatSystemComponent->GetCombatFlow()->HandleAttackResult(Entry); + + Entry.bConsumed = true; + } +} + +void FGCS_AttackResultContainer::PostReplicatedChange(const TArrayView& ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + if (!Results[Index].bConsumed) + { + CombatSystemComponent->GetCombatFlow()->HandleAttackResult(Results[Index]); + Results[Index].bConsumed = true; + } + } +} + +bool FGCS_AttackResultContainer::HasPredictedResultWithPredictedKey(FPredictionKey PredictionKey) const +{ + for (const FGCS_AttackResult& Result : Results) + { + if (Result.bConsumed) + { + continue; + } + if (const FGCS_ContextPayload_Combat* CombatPayload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(Result.EffectContextHandle)) + { + if (CombatPayload->PredictionKey.IsValidKey() && CombatPayload->PredictionKey == PredictionKey && CombatPayload->PredictionKey.WasReceived() == false) + { + return true; + } + } + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResultProcessor.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResultProcessor.cpp new file mode 100644 index 0000000..b14a12b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_AttackResultProcessor.cpp @@ -0,0 +1,242 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_AttackResultProcessor.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" +#include "GameplayCueFunctionLibrary.h" +#include "GameFramework/Pawn.h" +#include "AbilitySystem/GCS_GameplayEffectContext.h" +#include "CombatFlow/GCS_CombatFlow.h" +#include "UObject/ObjectSaveContext.h" +#include "Utilities/GGA_GameplayCueFunctionLibrary.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +bool UGCS_AttackResultProcessor::ProcessIncomingAttackResult(const FGCS_AttackResult& AttackResult) +{ + if (!AttackResult.bConsumed) + { + HandleIncomingAttackResult(AttackResult); + return true; + } + return false; +} + +EGCS_AttackResultProcessorPolicy UGCS_AttackResultProcessor::GetExecutePolicy_Implementation() const +{ + return ExecutePolicy; +} + +class UWorld* UGCS_AttackResultProcessor::GetWorld() const +{ + if (AActor* OwningActor = GetOwningActor()) + { + return OwningActor->GetWorld(); + } + return nullptr; +} + +AActor* UGCS_AttackResultProcessor::GetOwningActor() const +{ + if (UGCS_CombatFlow* Flow = Cast(GetOuter())) + { + return Flow->GetFlowOwner(); + } + return nullptr; +} + +UAbilitySystemComponent* UGCS_AttackResultProcessor::GetOwningAbilitySystemComponent() const +{ + if (AActor* OwningActor = GetOwningActor()) + { + return UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(OwningActor); + } + return nullptr; +} + +FString UGCS_AttackResultProcessor::GetEditorFriendlyName_Implementation() const +{ + return TEXT(""); +} + +void UGCS_AttackResultProcessor::HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const +{ +} + + +#if WITH_EDITORONLY_DATA +void UGCS_AttackResultProcessor::PreSave(FObjectPreSaveContext SaveContext) +{ + EditorFriendlyName = GetEditorFriendlyName(); + Super::PreSave(SaveContext); +} +#endif + +bool UGCS_AttackResultProcessor_WithTagRequirement::ProcessIncomingAttackResult(const FGCS_AttackResult& AttackResult) +{ + if (!AttackResult.bConsumed) + { + bool bMatchSourceQuery = SourceTagQuery.IsEmpty() || SourceTagQuery.Matches(AttackResult.AggregatedSourceTags); + bool bMatchTargetQuery = TargetTagQuery.IsEmpty() || TargetTagQuery.Matches(AttackResult.AggregatedTargetTags); + + if (bMatchSourceQuery && bMatchTargetQuery) + { + HandleIncomingAttackResult(AttackResult); + return true; + } + } + return false; +} + +FString UGCS_AttackResultProcessor_WithTagRequirement::GetSourceTagQueryDesc() const +{ + return SourceTagQuery.GetDescription(); +} + +FString UGCS_AttackResultProcessor_WithTagRequirement::GetTargetTagQueryDesc() const +{ + return TargetTagQuery.GetDescription(); +} + +void UGCS_AttackResultProcessor_Death::HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const +{ + UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent(); + if (ASC) + { + if (true) + { + } + } + Super::HandleIncomingAttackResult_Implementation(AttackResult); +} + +void UGCS_AttackResultProcessor_GameplayEvent::HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const +{ + APawn* OwningPawn = Cast(GetOwningActor()); + if (OwningPawn == nullptr) + { + return; + } + + if (OwningPawn->HasAuthority() || OwningPawn->IsLocallyControlled()) + { + UAbilitySystemComponent* ASC = bSendToAttacker ? AttackResult.EffectContextHandle.GetInstigatorAbilitySystemComponent() : GetOwningAbilitySystemComponent(); + if (IsValid(ASC)) + { + FGameplayEventData Payload = FGameplayEventData(); + + FGameplayTagContainer InstigatorTags = AttackResult.AggregatedSourceTags; + if (const FGCS_ContextPayload_Combat* CombatPayload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(AttackResult.EffectContextHandle)) + { + InstigatorTags.AppendTags(CombatPayload->DynamicTags); + } + + Payload.Instigator = AttackResult.EffectContextHandle.GetInstigator(); + // Payload.OptionalObject = AttackResult.OptionalObject; + Payload.Target = OwningPawn; + Payload.ContextHandle = AttackResult.EffectContextHandle; + Payload.InstigatorTags = InstigatorTags; + Payload.TargetTags = AttackResult.AggregatedTargetTags; + + for (const FGameplayTag& Tag : EventTriggers) + { + // UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwningPawn, Tag, Payload); + Payload.EventTag = Tag; + // FScopedPredictionWindow NewScopedWindow(ASC, true); + ASC->HandleGameplayEvent(Tag, &Payload); + } + } + } +} + +FString UGCS_AttackResultProcessor_GameplayEvent::GetEditorFriendlyName_Implementation() const +{ + FString Result; + + for (FGameplayTag EventTrigger : EventTriggers) + { + if (EventTrigger.IsValid()) + { + TArray Parts; + EventTrigger.GetTagName().ToString().ParseIntoArray(Parts,TEXT(".")); + if (Parts.Num() > 0) + { + Result.Append(FString::Format(TEXT(" ({0}) "), {Parts.Last()})); + } + } + } + + return FString::Format(TEXT("Send Event:{0}"), {Result}); +} + +void UGCS_AttackResultProcessor_GameplayCue::HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const +{ + AActor* OwningActor = GetOwningActor(); + if (!OwningActor) + { + return; + } + + TArray CuesToTrigger = GameplayCues; + + FGCS_AttackDefinition Atk = UGCS_CombatFunctionLibrary::EffectContextGetAttackDefinition(AttackResult.EffectContextHandle); + + if (!Atk.TargetGameplayCues.IsEmpty()) + { + CuesToTrigger = Atk.TargetGameplayCues; + } + + FGameplayCueParameters Params = FGameplayCueParameters(); + if (const FHitResult* HitResult = AttackResult.EffectContextHandle.GetHitResult()) + { + if (HitResult->GetActor() == OwningActor) + { + Params = UGameplayCueFunctionLibrary::MakeGameplayCueParametersFromHitResult(*HitResult); + } + } + + FGameplayTagContainer InstigatorTags = AttackResult.AggregatedSourceTags; + if (const FGCS_ContextPayload_Combat* CombatPayload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(AttackResult.EffectContextHandle)) + { + InstigatorTags.AppendTags(CombatPayload->DynamicTags); + Params.RawMagnitude = CombatPayload->GetTaggedValue(RawMagnitudeTag); + Params.NormalizedMagnitude = CombatPayload->GetTaggedValue(NormalizedMagnitudeTag); + } + + Params.AggregatedSourceTags = InstigatorTags; + Params.AggregatedTargetTags = AttackResult.AggregatedTargetTags; + Params.EffectCauser = AttackResult.EffectContextHandle.GetEffectCauser(); + Params.Instigator = AttackResult.EffectContextHandle.GetInstigator(); + + Params.EffectContext = AttackResult.EffectContextHandle; + + ModifyGameplayCueParametersBeforeExecute(Params); + + for (const FGameplayTag& Cue : CuesToTrigger) + { + if (!Cue.IsValid()) + { + continue; + } + UGGA_GameplayCueFunctionLibrary::ExecuteGameplayCueLocal(OwningActor, Cue, Params); + } +} + +FString UGCS_AttackResultProcessor_GameplayCue::GetEditorFriendlyName_Implementation() const +{ + FString Result; + for (FGameplayTag EventTrigger : GameplayCues) + { + if (EventTrigger.IsValid()) + { + TArray Parts; + EventTrigger.GetTagName().ToString().ParseIntoArray(Parts,TEXT(".")); + if (Parts.Num() > 0) + { + Result.Append(FString::Format(TEXT(" ({0}) "), {Parts.Last()})); + } + } + } + return FString::Format(TEXT("Execute cues:{0}"), {Result}); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_CombatFlow.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_CombatFlow.cpp new file mode 100644 index 0000000..84cdd4b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/CombatFlow/GCS_CombatFlow.cpp @@ -0,0 +1,95 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "CombatFlow/GCS_CombatFlow.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameFramework/Pawn.h" +#include "Utilities/GGA_AbilitySystemFunctionLibrary.h" +#include "GCS_CombatSystemComponent.h" +#include "GCS_LogChannels.h" +#include "CombatFlow/GCS_AttackResultProcessor.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + + +UGCS_CombatFlow::UGCS_CombatFlow() +{ + Owner = nullptr; +} + +void UGCS_CombatFlow::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); +} + +void UGCS_CombatFlow::Initialize(AActor* NewOwner) +{ + Owner = NewOwner; + // ProcessedAttacks.SetCombatFlow(this); + CombatComponent = UGCS_CombatSystemComponent::GetCombatSystemComponent(Owner); +} + +void UGCS_CombatFlow::HandlePreGameplayEffectSpecApply_Implementation(const FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent, + FGameplayTagContainer& OutDynamicTagsAppendToSpec) +{ +} + +void UGCS_CombatFlow::HandleGameplayEffectExecute_Implementation(const FGGA_GameplayEffectModCallbackData& Payload) +{ +} + +void UGCS_CombatFlow::HandleAttackResult_Implementation(const FGCS_AttackResult& InPayload) +{ + FGCS_AttackResult ModifiedPayload = InPayload; + bool bPredictingContext = UGCS_CombatFunctionLibrary::EffectContextGetIsPredictingContext(InPayload.EffectContextHandle); + const FGCS_ContextPayload_Combat* CombatPayload = UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(ModifiedPayload.EffectContextHandle); + + if (CombatPayload) + { + ModifiedPayload.AggregatedSourceTags.AppendTags(CombatPayload->DynamicTags); + } + + CombatComponent->SetLastProcessedAttackResult(ModifiedPayload); + + for (int32 i = 0; i < AttackResultProcessors.Num(); i++) + { + auto& Processor = AttackResultProcessors[i]; + if (!IsValid(Processor)) + { + continue; + } + bool bShouldExecute = Processor->GetExecutePolicy() == EGCS_AttackResultProcessorPolicy::Default && !bPredictingContext; + + if (Processor->GetExecutePolicy() == EGCS_AttackResultProcessorPolicy::LocalPredicted) + { + bShouldExecute = bPredictingContext || !InPayload.bWasPredicated; + if (!bShouldExecute) + { + GCS_OWNED_CLOG(GetFlowOwner(), VeryVerbose, "Skipped local predicted processor at idx(%d) of type(%s)", i, *Processor->GetClass()->GetName()) + } + } + + if (Processor->GetExecutePolicy() == EGCS_AttackResultProcessorPolicy::ServerOnly) + { + bShouldExecute = !bPredictingContext && !InPayload.bWasReplicated; + if (!bShouldExecute) + { + GCS_OWNED_CLOG(GetFlowOwner(), VeryVerbose, "Skipped server only processor at idx(%d) of type(%s)", i, *Processor->GetClass()->GetName()) + } + } + + if (bShouldExecute) + { +#if WITH_EDITOR + // Debugging says not enabled. + if (!Processor->GetEditorEnableState()) + { + continue; + } +#endif + if (Processor->ProcessIncomingAttackResult(ModifiedPayload)) + { + GCS_OWNED_CLOG(GetFlowOwner(), VeryVerbose, "%sExecuted processor at idx(%d) of type(%s)", bPredictingContext?TEXT("Predicate "):TEXT(""), i, *Processor->GetClass()->GetName()) + } + } + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Combo/GCS_ComboDefinition.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Combo/GCS_ComboDefinition.cpp new file mode 100644 index 0000000..350a9b8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Combo/GCS_ComboDefinition.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Combo/GCS_ComboDefinition.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_ActorOwnedObject.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_ActorOwnedObject.cpp new file mode 100644 index 0000000..e07cbf2 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_ActorOwnedObject.cpp @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_ActorOwnedObject.h" +#include "GameFramework/Actor.h" + +UWorld* UGCS_ActorOwnedObject::GetWorld() const +{ + // To Make sure the outer is Valid and can be used + if (!HasAnyFlags(RF_ClassDefaultObject) && !GetOuter()->HasAnyFlags(RF_BeginDestroyed) && !GetOuter()->IsUnreachable()) + { + //Attempt to get the world + AActor* Outer = GetTypedOuter(); + if (Outer != nullptr) + { + return Outer->GetWorld(); + } + } + return nullptr; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEntityInterface.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEntityInterface.cpp new file mode 100644 index 0000000..782d2da --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEntityInterface.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_CombatEntityInterface.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEnumLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEnumLibrary.cpp new file mode 100644 index 0000000..c271e71 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatEnumLibrary.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_CombatEnumLibrary.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatStructLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatStructLibrary.cpp new file mode 100644 index 0000000..449e139 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatStructLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_CombatStructLibrary.h" +#include "Animation/AnimMontage.h" +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemComponent.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemComponent.cpp new file mode 100644 index 0000000..a597f8f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemComponent.cpp @@ -0,0 +1,318 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_CombatSystemComponent.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "TimerManager.h" +#include "GCS_LogChannels.h" +#include "Engine/World.h" +#include "GameFramework/Controller.h" +#include "Components/SkeletalMeshComponent.h" +#include "CombatFlow/GCS_AttackRequest.h" +#include "CombatFlow/GCS_CombatFlow.h" +#include "GameFramework/GameStateBase.h" +#include "Net/UnrealNetwork.h" +#include "Animation/AnimMontage.h" +#include "Animation/AnimInstance.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + + +UGCS_CombatSystemComponent::UGCS_CombatSystemComponent() : AttackResultContainer(this, 10) +{ + SetIsReplicatedByDefault(true); + bWantsInitializeComponent = true; + bReplicateUsingRegisteredSubObjectList = true; +} + +void UGCS_CombatSystemComponent::InitializeComponent() +{ + AttackResultContainer.SetOwningCombatSystem(this); + Super::InitializeComponent(); +} + +void UGCS_CombatSystemComponent::BeginPlay() +{ + if (GetWorld()->IsGameWorld()) + { + if (GetOwner()->HasAuthority() && CombatFlowClass) + { + UGCS_CombatFlow* LocalNewProperty = NewObject(GetOwner(), CombatFlowClass); + LocalNewProperty->Initialize(GetOwner()); + CombatFlow = LocalNewProperty; + AddReplicatedSubObject(CombatFlow); + } + UGGA_AbilitySystemGlobals::RegisterEventReceiver(this); + } + Super::BeginPlay(); +} + +void UGCS_CombatSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + UGGA_AbilitySystemGlobals::UnregisterEventReceiver(this); + if (GetOwner() && GetOwner()->HasAuthority()) + { + if (CombatFlow) + { + RemoveReplicatedSubObject(CombatFlow); + } + } +} + +void UGCS_CombatSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, CombatFlow); + DOREPLIFETIME(ThisClass, AttackResultContainer); + DOREPLIFETIME(ThisClass, ReplicatedMontageInfo); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + Parameters.Condition = COND_SkipOwner; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ComboStep, Parameters) +} + +UGCS_CombatSystemComponent* UGCS_CombatSystemComponent::GetCombatSystemComponent(const AActor* Actor) +{ + return Actor ? Actor->FindComponentByClass() : nullptr; +} + +bool UGCS_CombatSystemComponent::FindCombatSystemComponent(const AActor* Actor, UGCS_CombatSystemComponent*& CombatComponent) +{ + CombatComponent = Actor ? Actor->FindComponentByClass() : nullptr; + return IsValid(CombatComponent); +} + +bool UGCS_CombatSystemComponent::FindTypedCombatSystemComponent(AActor* Actor, TSubclassOf DesiredClass, UGCS_CombatSystemComponent*& Component) +{ + if (UGCS_CombatSystemComponent* Instance = GetCombatSystemComponent(Actor)) + { + if (Instance->GetClass()->IsChildOf(DesiredClass)) + { + Component = Instance; + return true; + } + } + return false; +} + +UGCS_CombatFlow* UGCS_CombatSystemComponent::GetCombatFlow() const +{ + return CombatFlow; +} + +void UGCS_CombatSystemComponent::RegisterAttackResult(FGCS_AttackResult& Payload) +{ + //TODO Should make this server only? + if (GetOwner() && GetOwner()->HasAuthority()) + { + } + AttackResultContainer.AddEntry(Payload); +} + +FGCS_AttackResult UGCS_CombatSystemComponent::GetLastProcessedAttackResult() const +{ + return LastProcessedAttackResult; +} + +void UGCS_CombatSystemComponent::SetLastProcessedAttackResult(const FGCS_AttackResult& Payload) +{ + LastProcessedAttackResult = Payload; +} + +void UGCS_CombatSystemComponent::PlayPredictableMontageForTarget(UGCS_CombatSystemComponent* TargetCSC, FGCS_PlayMontageRequest Request) +{ + if (GetOwnerRole() >= ROLE_Authority) + { + TargetCSC->SetReplicatedMontage(Request); + } + + if (GetOwnerRole() == ROLE_AutonomousProxy) + { + TargetCSC->PlayPredictedMontage(Request); + ServerPlayPredictableMontageForTarget(TargetCSC, Request); + } +} + + +void UGCS_CombatSystemComponent::ServerPlayPredictableMontageForTarget_Implementation(UGCS_CombatSystemComponent* TargetCSC, FGCS_PlayMontageRequest Request) +{ + TargetCSC->SetReplicatedMontage(Request); +} + + +void UGCS_CombatSystemComponent::SetReplicatedMontage(const FGCS_PlayMontageRequest& Request) +{ + TimerHandle.Invalidate(); + ReplicatedMontageInfo.AnimMontage = Request.AnimMontage; + ReplicatedMontageInfo.PlayRate = Request.PlayRate; + ReplicatedMontageInfo.TriggeredTime = GetWorld()->GetGameState()->GetServerWorldTimeSeconds(); + ReplicatedMontageInfo.StartSectionName = Request.StartSectionName; + + GetWorld()->GetTimerManager().SetTimer(TimerHandle, [&]() + { + ReplicatedMontageInfo.AnimMontage = nullptr; + TimerHandle.Invalidate(); + }, Request.AnimMontage->GetPlayLength() * Request.PlayRate, false); + + + OnRep_ReplicatedMontageInfo(); +} + +//server tell me to play montage. +void UGCS_CombatSystemComponent::OnRep_ReplicatedMontageInfo() +{ + if (ReplicatedMontageInfo.AnimMontage == nullptr) + { + PredictedMontageInfo.AnimMontage = nullptr; + return; + } + if (USkeletalMeshComponent* MeshComponent = GetCharacterMeshComponent()) + { + UAnimMontage* MontageToPlay = ReplicatedMontageInfo.AnimMontage; + float TimeDiff = GetWorld()->GetGameState()->GetServerWorldTimeSeconds() - ReplicatedMontageInfo.TriggeredTime; + float StartTime = FMath::Clamp(TimeDiff, 0, ReplicatedMontageInfo.AnimMontage->GetPlayLength() * ReplicatedMontageInfo.PlayRate); + + //If local montage ahead of replicated montage + if (PredictedMontageInfo.AnimMontage != nullptr) + { + //And it's the same. + if (ReplicatedMontageInfo.AnimMontage == PredictedMontageInfo.AnimMontage) + { + PredictedMontageInfo.AnimMontage = nullptr; + return; + } + PredictedMontageInfo.AnimMontage = nullptr; + } + + MeshComponent->GetAnimInstance()->Montage_Play(MontageToPlay, ReplicatedMontageInfo.PlayRate, EMontagePlayReturnType::MontageLength, StartTime); + } +} + +void UGCS_CombatSystemComponent::PlayPredictedMontage(const FGCS_PlayMontageRequest& Request) +{ + PredictedMontageInfo.AnimMontage = Request.AnimMontage; + PredictedMontageInfo.PlayRate = Request.PlayRate; + PredictedMontageInfo.TriggeredTime = GetWorld()->GetGameState()->GetServerWorldTimeSeconds(); + PredictedMontageInfo.StartSectionName = Request.StartSectionName; + if (USkeletalMeshComponent* MeshComponent = GetCharacterMeshComponent()) + { + float Duration = MeshComponent->GetAnimInstance()->Montage_Play(Request.AnimMontage, Request.PlayRate, EMontagePlayReturnType::MontageLength, Request.StartTimeSeconds); + if (Duration > 0) + { + // Start at a given Section. + if (Request.StartSectionName != NAME_None) + { + MeshComponent->GetAnimInstance()->Montage_JumpToSection(Request.StartSectionName, Request.AnimMontage); + } + } + } +} + +USkeletalMeshComponent* UGCS_CombatSystemComponent::GetCharacterMeshComponent() const +{ + static FName CharacterMeshTagName = "CharacterMesh"; + + return Cast(GetOwner()->FindComponentByTag(USkeletalMeshComponent::StaticClass(), CharacterMeshTagName)); +} + +void UGCS_CombatSystemComponent::OnGlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) +{ + if (IsValid(CombatFlow)) + { + FGameplayTagContainer DynamicTags; + CombatFlow->HandlePreGameplayEffectSpecApply(Spec, AbilitySystemComponent, DynamicTags); + if (!DynamicTags.IsEmpty()) + { + Spec.AppendDynamicAssetTags(DynamicTags); + } + } +} + +void UGCS_CombatSystemComponent::OnRep_CombatFlow() +{ + CombatFlow->Initialize(GetOwner()); + UE_LOG(LogGCS, Display, TEXT("Combat flow replicated for %s"), *GetOwner()->GetName()); +} + +int32 UGCS_CombatSystemComponent::GetComboStep() const +{ + return ComboStep; +} + +void UGCS_CombatSystemComponent::UpdateComboStep(int32 NewComboStep) +{ + if (NewComboStep >= 0) + { + UpdateComboStep(NewComboStep, true); + } +} + +void UGCS_CombatSystemComponent::ResetComboState() +{ + if (ComboStep != 0) + { + UpdateComboStep(0,true); + } +} + +void UGCS_CombatSystemComponent::UpdateComboStep(int32 NewComboStep, bool bSendRpc) +{ + if (ComboStep == NewComboStep || GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy || ComboStep == NewComboStep) + { + return; + } + + const auto PrevComboStep{ComboStep}; + + ComboStep = NewComboStep; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ComboStep, this) + + OnComboStepChanged(PrevComboStep); + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientUpdateComboStep(ComboStep); + } + else + { + ServerUpdateComboStep(ComboStep); + } + } +} + +void UGCS_CombatSystemComponent::OnReplicated_ComboStep(int32 PrevComboStep) +{ + OnComboStepChanged(PrevComboStep); +} + +void UGCS_CombatSystemComponent::ClientUpdateComboStep_Implementation(int32 NewComboStep) +{ + UpdateComboStep(NewComboStep, false); +} + +bool UGCS_CombatSystemComponent::ClientUpdateComboStep_Validate(int32 NewComboStep) +{ + return true; +} + +void UGCS_CombatSystemComponent::ServerUpdateComboStep_Implementation(int32 NewComboStep) +{ + UpdateComboStep(NewComboStep, false); +} + +bool UGCS_CombatSystemComponent::ServerUpdateComboStep_Validate(int32 NewComboStep) +{ + return true; +} + +void UGCS_CombatSystemComponent::OnComboStepChanged_Implementation(int32 PrevComboStep) +{ + OnComboStepChangedEvent.Broadcast(PrevComboStep); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemSettings.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemSettings.cpp new file mode 100644 index 0000000..95db202 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_CombatSystemSettings.cpp @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_CombatSystemSettings.h" + +const UGCS_CombatSystemSettings* UGCS_CombatSystemSettings::Get() +{ + return GetDefault(); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_EffectCauserInterface.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_EffectCauserInterface.cpp new file mode 100644 index 0000000..5a00f6c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_EffectCauserInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_EffectCauserInterface.h" + + +// Add default functionality here for any IGCS_EffectCauserInterface functions that are not pure virtual. diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_GameplayTags.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_GameplayTags.cpp new file mode 100644 index 0000000..88fce24 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_GameplayTags.cpp @@ -0,0 +1,11 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_GameplayTags.h" + +namespace GCS_BulletLaunch +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Always, "GGF.Combat.Bullet.LaunchCond.Always", "Always generate bullet"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(DidNotHitPawn, "GGF.Combat.Bullet.LaunchCond.DidNotHitPawn", "Only generate bullet if not hit any pawn"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(HitPawn, "GGF.Combat.Bullet.LaunchCond.HitPawn", "Only generate bullet if hit any pawn"); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_LogChannels.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_LogChannels.cpp new file mode 100644 index 0000000..b7e99fc --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GCS_LogChannels.cpp @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCS_LogChannels.h" +#include "Engine/EngineTypes.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" +#include "GameplayTask.h" +#include "Abilities/GameplayAbility.h" + +DEFINE_LOG_CATEGORY(LogGCS) +DEFINE_LOG_CATEGORY(LogGCS_Targeting) +DEFINE_LOG_CATEGORY(LogGCS_Collision) +DEFINE_LOG_CATEGORY(LogGCS_Trace) + + +FString GetGCSLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + Role = Component->GetOwnerRole(); + Name = Component->GetOwner()->GetName(); + } + else if (const UGameplayTask* Task = Cast(ContextObject)) + { + Role = Task->GetAvatarActor()->GetLocalRole(); + Name = Task->GetAvatarActor()->GetName(); + } + else if (const UGameplayAbility* Ability = Cast(ContextObject)) + { + Role = Ability->GetAvatarActorFromActorInfo()->GetLocalRole(); + Name = Ability->GetAvatarActorFromActorInfo()->GetName(); + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} + + +FString GetClientServerContextString(UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + + if (AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + } + else if (UActorComponent* Component = Cast(ContextObject)) + { + Role = Component->GetOwnerRole(); + } + + if (Role != ROLE_None) + { + return (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } +#if WITH_EDITOR + if (GIsEditor) + { + extern ENGINE_API FString GPlayInEditorContextString; + return GPlayInEditorContextString; + } +#endif + + return TEXT("[]"); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/GenericCombatSystem.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/GenericCombatSystem.cpp new file mode 100644 index 0000000..455c1d8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/GenericCombatSystem.cpp @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericCombatSystem.h" + +#define LOCTEXT_NAMESPACE "FGenericCombatSystemModule" + +void FGenericCombatSystemModule::StartupModule() +{ + +} + +void FGenericCombatSystemModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericCombatSystemModule, GenericCombatSystem) \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_AttackTrace.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_AttackTrace.cpp new file mode 100644 index 0000000..d8c0161 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_AttackTrace.cpp @@ -0,0 +1,28 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Notifies/GCS_ANS_AttackTrace.h" + +#include "CombatFlow/GCS_AttackRequest.h" +#include "UObject/ObjectSaveContext.h" + + +UGCS_ANS_AttackTrace::UGCS_ANS_AttackTrace(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +#if WITH_EDITORONLY_DATA + bShouldFireInEditor = false; +#endif + AttackRequest = ObjectInitializer.CreateDefaultSubobject(this, TEXT("AttackRequest")); +} + +void UGCS_ANS_AttackTrace::PostInitProperties() +{ + Super::PostInitProperties(); +} + +#if WITH_EDITORONLY_DATA +void UGCS_ANS_AttackTrace::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_BulletTrace.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_BulletTrace.cpp new file mode 100644 index 0000000..565e6bc --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_BulletTrace.cpp @@ -0,0 +1,35 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Notifies/GCS_ANS_BulletTrace.h" + +#include "CombatFlow/GCS_AttackRequest.h" +#include "UObject/ObjectSaveContext.h" + + +UGCS_ANS_BulletTrace::UGCS_ANS_BulletTrace(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +#if WITH_EDITORONLY_DATA + bShouldFireInEditor = false; +#endif + AttackRequest = ObjectInitializer.CreateDefaultSubobject(this, TEXT("AttackRequest")); +} + +#if WITH_EDITOR +#include "Misc/DataValidation.h" + +EDataValidationResult UGCS_ANS_BulletTrace::IsDataValid(FDataValidationContext& Context) const +{ + if (AttackRequest && AttackRequest->bRequireTargeting && AttackRequest->TargetingPreset == nullptr) + { + Context.AddError(FText::FromString(TEXT("TargetingPreset is required!"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} + +void UGCS_ANS_BulletTrace::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_MovementCancellation.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_MovementCancellation.cpp new file mode 100644 index 0000000..2760f55 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Notifies/GCS_ANS_MovementCancellation.cpp @@ -0,0 +1,100 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Notifies/GCS_ANS_MovementCancellation.h" +#include "Components/SkeletalMeshComponent.h" +#include "Animation/AnimInstance.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" + +UGCS_ANS_MovementCancellation::UGCS_ANS_MovementCancellation(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ + bIsNativeBranchingPoint = true; +} + +void UGCS_ANS_MovementCancellation::BranchingPointNotifyBegin(FBranchingPointNotifyPayload& BranchingPointPayload) +{ + Super::BranchingPointNotifyBegin(BranchingPointPayload); + + IsRootMotionDisabled = false; + + if (USkeletalMeshComponent* MeshComp = BranchingPointPayload.SkelMeshComponent) + { + if (UAnimInstance* AnimInstance = MeshComp->GetAnimInstance()) + { + if (FAnimMontageInstance* MontageInstance = AnimInstance->GetMontageInstanceForID(BranchingPointPayload.MontageInstanceID)) + { + if (ACharacter* Character = Cast(MeshComp->GetOwner())) + { + if (Character->GetCharacterMovement()->GetCurrentAcceleration().SizeSquared2D() > 10.0) + { + MontageInstance->PushDisableRootMotion(); + + IsRootMotionDisabled = true; + } + } + } + } + } +} + +void UGCS_ANS_MovementCancellation::BranchingPointNotifyTick(FBranchingPointNotifyPayload& BranchingPointPayload, float FrameDeltaTime) +{ + Super::BranchingPointNotifyTick(BranchingPointPayload, FrameDeltaTime); + if (USkeletalMeshComponent* MeshComp = BranchingPointPayload.SkelMeshComponent) + { + if (UAnimInstance* AnimInstance = MeshComp->GetAnimInstance()) + { + if (FAnimMontageInstance* MontageInstance = AnimInstance->GetMontageInstanceForID(BranchingPointPayload.MontageInstanceID)) + { + if (ACharacter* Character = Cast(MeshComp->GetOwner())) { + if (Character->GetCharacterMovement()->GetCurrentAcceleration().SizeSquared2D() > 10.0) { + if (!IsRootMotionDisabled) { + MontageInstance->PushDisableRootMotion(); + + IsRootMotionDisabled = true; + } + } + else if (IsRootMotionDisabled) { + MontageInstance->PopDisableRootMotion(); + + IsRootMotionDisabled = false; + } + } + } + } + } +} + +void UGCS_ANS_MovementCancellation::BranchingPointNotifyEnd(FBranchingPointNotifyPayload& BranchingPointPayload) +{ + Super::BranchingPointNotifyEnd(BranchingPointPayload); + if (USkeletalMeshComponent* MeshComp = BranchingPointPayload.SkelMeshComponent) + { + if (UAnimInstance* AnimInstance = MeshComp->GetAnimInstance()) + { + if (FAnimMontageInstance* MontageInstance = AnimInstance->GetMontageInstanceForID(BranchingPointPayload.MontageInstanceID)) + { + if (IsRootMotionDisabled) { + IsRootMotionDisabled = false; + + MontageInstance->PopDisableRootMotion(); + } + } + } + } +} + +bool UGCS_ANS_MovementCancellation::IsMoving_Implementation(USkeletalMeshComponent* MeshComp) const +{ + return false; +} + +#if WITH_EDITOR + + +bool UGCS_ANS_MovementCancellation::CanBePlaced(UAnimSequenceBase* Animation) const +{ + return (Animation && Animation->IsA(UAnimMontage::StaticClass())); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.cpp new file mode 100644 index 0000000..e601b87 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.cpp @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Filters/GCS_TargetingFilterTask_Affiliation.h" +#include "GCS_CombatSystemSettings.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/Controller.h" +#include "Team/GCS_CombatTeamAgentInterface.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +bool UGCS_TargetingFilterTask_Affiliation::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + if (const UGCS_CombatSystemSettings* Settings = UGCS_CombatSystemSettings::Get()) + { + if (Settings->bDisableAffiliationCheck) + { + return false; + } + } + FGenericTeamId SourceTeamId = GetSourceTeamId(TargetingHandle, TargetData); + FGenericTeamId TargetTeamId = GetTargetTeamId(TargetingHandle, TargetData); + + if (TargetTeamId == FGenericTeamId::NoTeam && bIgnoreTargetWithNoTeam) + { + return true; + } + + return FAISenseAffiliationFilter::ShouldSenseTeam(SourceTeamId, TargetTeamId, DetectionByAffiliation.GetAsFlags()) == false; +} + +FGenericTeamId UGCS_TargetingFilterTask_Affiliation::GetSourceTeamId(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + AActor* Actor = SourceContext->InstigatorActor ? SourceContext->InstigatorActor : SourceContext->SourceActor; + if (IsValid(Actor)) + { + return UGCS_CombatFunctionLibrary::QueryCombatTeamId(Actor, true, true);; + } + } + return FGenericTeamId::NoTeam; +} + +FGenericTeamId UGCS_TargetingFilterTask_Affiliation::GetTargetTeamId(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + if (const AActor* TargetActor = TargetData.HitResult.GetActor()) + { + return UGCS_CombatFunctionLibrary::QueryCombatTeamId(TargetActor, true, true);; + } + return FGenericTeamId::NoTeam; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_IsDead.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_IsDead.cpp new file mode 100644 index 0000000..f8f6b46 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_IsDead.cpp @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Filters/GCS_TargetingFilterTask_IsDead.h" + +#include "GCS_CombatEntityInterface.h" +#include "Utility/GCS_CombatFunctionLibrary.h" + +bool UGCS_TargetingFilterTask_IsDead::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + AActor* TargetActor = TargetData.HitResult.GetActor(); + + if (UObject* Implementer = UGCS_CombatFunctionLibrary::GetCombatEntity(TargetActor)) + { + return IGCS_CombatEntityInterface::Execute_IsDead(Implementer); + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.cpp new file mode 100644 index 0000000..4dacb8e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.cpp @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.h" + +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" + +bool UGCS_TargetingFilterTask_TagsRequirements::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + const AActor* TargetActor = TargetData.HitResult.GetActor(); + + if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(TargetActor)) + { + FGameplayTagContainer ActorTags; + ASC->GetOwnedGameplayTags(ActorTags); + + return bInvert ? !TagQuery.Matches(ActorTags) : TagQuery.Matches(ActorTags); + } + + if (bLookingForTagAssetInterface) + { + const IGameplayTagAssetInterface* TagAssetInterface = Cast(TargetActor); + if (!TagAssetInterface) + { + const TArray Components = TargetActor->GetComponentsByInterface(UGameplayTagAssetInterface::StaticClass()); + TagAssetInterface = Components.IsValidIndex(0) ? Cast(Components[0]) : nullptr; + } + + if (TagAssetInterface) + { + FGameplayTagContainer ActorTags; + TagAssetInterface->GetOwnedGameplayTags(ActorTags); + + return bInvert ? !TagQuery.Matches(ActorTags) : TagQuery.Matches(ActorTags); + } + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.cpp new file mode 100644 index 0000000..ddfb555 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.cpp @@ -0,0 +1,21 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.h" + +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" + +bool UGCS_TargetingFilterTask_TraceInstance::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + const AActor* TargetActor = TargetData.HitResult.GetActor(); + + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (const UDEPRECATED_GCS_CollisionTraceInstance* TraceInstance = Cast(SourceContext->SourceObject)) + { + return !TraceInstance->CanHitActor(TargetActor); + } + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingFunctionLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingFunctionLibrary.cpp new file mode 100644 index 0000000..db0098f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingFunctionLibrary.cpp @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/GCS_TargetingFunctionLibrary.h" +#include "Components/MeshComponent.h" + +FTargetingSourceContext UGCS_TargetingFunctionLibrary::GetTargetingSourceContext(FTargetingRequestHandle TargetingHandle) +{ + if (TargetingHandle.IsValid()) + { + if (FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + return *SourceContext; + } + } + + return FTargetingSourceContext(); +} + +FString UGCS_TargetingFunctionLibrary::GetTargetingSourceContextDebugString(FTargetingRequestHandle TargetingHandle) +{ + if (TargetingHandle.IsValid()) + { + if (FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + return FString::Format(TEXT("ctx(actor:{0},instigator:{1},source:{2})"), { + *GetNameSafe(SourceContext->SourceActor), *GetNameSafe(SourceContext->InstigatorActor), *GetNameSafe(SourceContext->SourceObject) + }); + } + } + return TEXT("None"); +} + +void UGCS_TargetingFunctionLibrary::GetTargetingResultsActors(FTargetingRequestHandle TargetingHandle, TArray& Targets) +{ + if (TargetingHandle.IsValid()) + { + if (FTargetingDefaultResultsSet* Results = FTargetingDefaultResultsSet::Find(TargetingHandle)) + { + for (const FTargetingDefaultResultData& ResultData : Results->TargetResults) + { + if (AActor* Target = ResultData.HitResult.GetActor()) + { + Targets.Add(Target); + } + } + } + } +} + +void UGCS_TargetingFunctionLibrary::GetTargetingResults(FTargetingRequestHandle TargetingHandle, TArray& OutTargets) +{ + if (TargetingHandle.IsValid()) + { + if (FTargetingDefaultResultsSet* Results = FTargetingDefaultResultsSet::Find(TargetingHandle)) + { + for (const FTargetingDefaultResultData& ResultData : Results->TargetResults) + { + OutTargets.Add(ResultData.HitResult); + } + } + } +} + +FTargetingSourceContext UGCS_TargetingFunctionLibrary::ConvertTargetingLocationInfoToSourceContext(FGameplayAbilityTargetingLocationInfo LocationInfo) +{ + FTargetingSourceContext Context = FTargetingSourceContext(); + + //Return or calculate based on LocationType. + switch (LocationInfo.LocationType) + { + case EGameplayAbilityTargetingLocationType::ActorTransform: + if (LocationInfo.SourceActor) + { + Context.SourceActor = LocationInfo.SourceActor; + } + break; + case EGameplayAbilityTargetingLocationType::SocketTransform: + if (LocationInfo.SourceComponent) + { + // Bad socket name will just return component transform anyway, so we're safe + Context.SourceLocation = LocationInfo.SourceComponent->GetSocketTransform(LocationInfo.SourceSocketName).GetLocation(); + } + break; + case EGameplayAbilityTargetingLocationType::LiteralTransform: + Context.SourceLocation = LocationInfo.LiteralTransform.GetLocation(); + default: + check(false); + break; + } + + return Context; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSourceInterface.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSourceInterface.cpp new file mode 100644 index 0000000..4d213a4 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSourceInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/GCS_TargetingSourceInterface.h" + + +// Add default functionality here for any IGCS_TargetingSourceInterface functions that are not pure virtual. diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSystemComponent.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSystemComponent.cpp new file mode 100644 index 0000000..e4ecfbb --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/GCS_TargetingSystemComponent.cpp @@ -0,0 +1,412 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/GCS_TargetingSystemComponent.h" + +#include "GCS_LogChannels.h" +#include "Kismet/KismetMathLibrary.h" +#include "Camera/CameraComponent.h" +#include "GameFramework/Character.h" +#include "Camera/PlayerCameraManager.h" +#include "Net/UnrealNetwork.h" +#include "TargetingSystem/TargetingSubsystem.h" + +UGCS_TargetingSystemComponent::UGCS_TargetingSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + PrimaryComponentTick.bCanEverTick = true; + SetIsReplicatedByDefault(true); +} + +UGCS_TargetingSystemComponent* UGCS_TargetingSystemComponent::GetTargetingSystemComponent(const AActor* Actor) +{ + return Actor ? Actor->FindComponentByClass() : nullptr; +} + +void UGCS_TargetingSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(ThisClass, TargetedActor, COND_OwnerOnly); +} + + +// Called when the game starts +void UGCS_TargetingSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... +} + + +// Called every frame +void UGCS_TargetingSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + RefreshTargeting(DeltaTime); +} + +void UGCS_TargetingSystemComponent::RefreshTargeting(float DeltaTime) +{ + if (GetOwnerRole() >= ROLE_AutonomousProxy) + { + if (bAutoUpdatePotentialTargets || IsValid(TargetedActor)) + { + RefreshPotentialTargets(); + } + + bool LocalBool = PotentialTargets.Contains(TargetedActor); + + // Current TargetedActor no longer consider valid. + if (TargetedActor && !LocalBool) + { + OnLockOff(); + SetTargetedActor(nullptr); + } + } +} + +void UGCS_TargetingSystemComponent::SearchForActorToTarget() +{ + RefreshPotentialTargets(); + SelectFromPotentialTargets(); + //No target found/unlocked. + if (!IsValid(TargetedActor) && !bAutoUpdatePotentialTargets) + { + PotentialTargets.Empty(); + } +} + +AActor* UGCS_TargetingSystemComponent::SelectClosestActorFromPotentialTargets(float Radius) const +{ + // 检查 Owner 是否有效 + if (!GetOwner()) + { + UE_LOG(LogTemp, Warning, TEXT("SelectClosestActorFromPotentialTargets: Owner is null")); + return nullptr; + } + + // 检查 PotentialTargets 是否为空 + if (PotentialTargets.Num() == 0) + { + return nullptr; + } + + // 过滤有效 Actor 并在范围内 + TArray FilteredPotentialTargets; + const FVector OwnerLocation = GetOwner()->GetActorLocation(); + + for (AActor* Actor : PotentialTargets) + { + // 检查 Actor 是否有效且未被销毁 + if (IsValid(Actor)) + { + float Distance = FVector::Dist(OwnerLocation, Actor->GetActorLocation()); + if (Distance <= Radius) + { + FilteredPotentialTargets.Add(Actor); + } + } + } + + // 如果过滤后没有有效目标 + if (FilteredPotentialTargets.Num() == 0) + { + return nullptr; + } + + // 寻找最近的 Actor + AActor* ClosestActor = FilteredPotentialTargets[0]; + float MinDistance = FVector::Dist(OwnerLocation, ClosestActor->GetActorLocation()); + + for (int32 i = 1; i < FilteredPotentialTargets.Num(); ++i) + { + AActor* CurrentActor = FilteredPotentialTargets[i]; + if (IsValid(CurrentActor)) + { + float Distance = FVector::Dist(OwnerLocation, CurrentActor->GetActorLocation()); + if (Distance < MinDistance) + { + MinDistance = Distance; + ClosestActor = CurrentActor; + } + } + } + + return ClosestActor; +} + +bool UGCS_TargetingSystemComponent::FilterActorsWithPreset(UTargetingPreset* InTargetingPreset, const TArray InTargets, TArray& OutActors) +{ + if (InTargetingPreset == nullptr) + { + return false; + } + + if (UTargetingSubsystem* TargetingSubsystem = UTargetingSubsystem::Get(GetWorld())) + { + FTargetingSourceContext SourceContext; + SourceContext.SourceActor = GetOwner(); + + FTargetingRequestHandle TargetingHandle = UTargetingSubsystem::MakeTargetRequestHandle(TargetingPreset, SourceContext); + + if (TargetingHandle.IsValid() && InTargets.Num() > 0) + { + FTargetingDefaultResultsSet& TargetingResults = FTargetingDefaultResultsSet::FindOrAdd(TargetingHandle); + for (AActor* Target : InTargets) + { + if (!Target) + { + continue; + } + + bool bAddResult = !TargetingResults.TargetResults.ContainsByPredicate([Target](const FTargetingDefaultResultData& Data) -> bool + { + return (Data.HitResult.GetActor() == Target); + }); + + if (bAddResult) + { + FTargetingDefaultResultData* ResultData = new(TargetingResults.TargetResults) FTargetingDefaultResultData(); + ResultData->HitResult.HitObjectHandle = FActorInstanceHandle(Target); + ResultData->HitResult.Location = Target->GetActorLocation(); + } + } + } + + + FTargetingRequestDelegate Delegate = FTargetingRequestDelegate::CreateWeakLambda(this, [&](FTargetingRequestHandle InTargetingHandle) + { + TargetingSubsystem->GetTargetingResultsActors(InTargetingHandle, OutActors); + }); + + FTargetingImmediateTaskData& ImmeidateTaskData = FTargetingImmediateTaskData::FindOrAdd(TargetingHandle); + ImmeidateTaskData.bReleaseOnCompletion = true; + + TargetingSubsystem->ExecuteTargetingRequestWithHandle(TargetingHandle, Delegate); + } + + return !OutActors.IsEmpty(); +} + +void UGCS_TargetingSystemComponent::SetTargetedActor(AActor* NewActor) +{ + SetTargetedActor(NewActor, true); +} + +void UGCS_TargetingSystemComponent::SetTargetedActor(AActor* NewActor, bool bSendRpc) +{ + if (GetOwnerRole() < ROLE_AutonomousProxy) + { + return; + } + + if (bSendRpc) + { + if (GetOwnerRole() >= ROLE_Authority) + { + ClientSetTargetedActor(NewActor); + } + else + { + ServerSetTargetedActor(NewActor); + } + } + + TargetedActor = NewActor; +} + +void UGCS_TargetingSystemComponent::ClientSetTargetedActor_Implementation(AActor* NewActor) +{ + SetTargetedActor(NewActor, false); +} + +void UGCS_TargetingSystemComponent::ServerSetTargetedActor_Implementation(AActor* NewActor) +{ + SetTargetedActor(NewActor, false); +} + +void UGCS_TargetingSystemComponent::OnLockOff_Implementation() +{ + OnTargetLockOffEvent.Broadcast(TargetedActor); +} + +void UGCS_TargetingSystemComponent::OnLockOn_Implementation() +{ + OnTargetLockOnEvent.Broadcast(TargetedActor); +} + +void UGCS_TargetingSystemComponent::SelectFromPotentialTargets() +{ + if (!IsValid(TargetedActor)) + { + TMap LocalPotentialTargets; + + for (TObjectPtr& Elem : PotentialTargets) + { + if (CanBeTargeted(Elem)) + { + LocalPotentialTargets.Add(Elem, UKismetMathLibrary::Abs(CalculateViewAngle(Elem))); + } + } + + if (LocalPotentialTargets.Num() > 0) + { + TArray LocalTargets; + TArray LocalAngles; + LocalPotentialTargets.GenerateKeyArray(LocalTargets); + LocalPotentialTargets.GenerateValueArray(LocalAngles); + + int32 MinIndex; + const float MaxValue = FMath::Min(LocalAngles, &MinIndex); + SetTargetedActor(LocalTargets[MinIndex]); + OnLockOn(); + } + } + else + { + OnLockOff(); + SetTargetedActor(nullptr); + } +} + +void UGCS_TargetingSystemComponent::RefreshPotentialTargets() +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TargetingSystemComponent::RefreshPotentialTargets"), UGCS_TargetingSystemComponent_RefreshPotentialTargets, STATGROUP_GCS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + if (TargetingPreset == nullptr) + { + return; + } + + if (UTargetingSubsystem* TargetingSubsystem = UTargetingSubsystem::Get(GetWorld())) + { + FTargetingSourceContext SourceContext; + SourceContext.SourceActor = GetOwner(); + SourceContext.SourceObject = this; + + FTargetingRequestHandle TargetingHandle = UTargetingSubsystem::MakeTargetRequestHandle(TargetingPreset, SourceContext); + FTargetingRequestDelegate Delegate = FTargetingRequestDelegate::CreateWeakLambda(this, [this,TargetingSubsystem](FTargetingRequestHandle InTargetingHandle) + { + TArray Results; + TargetingSubsystem->GetTargetingResultsActors(InTargetingHandle, Results); + PotentialTargets.Empty(); + PotentialTargets = Results; + }); + + if (bUseAsyncTargeting) + { + FTargetingAsyncTaskData& AsyncTaskData = FTargetingAsyncTaskData::FindOrAdd(TargetingHandle); + AsyncTaskData.bReleaseOnCompletion = true; + + TargetingSubsystem->StartAsyncTargetingRequestWithHandle(TargetingHandle, Delegate); + } + else + { + FTargetingImmediateTaskData& ImmeidateTaskData = FTargetingImmediateTaskData::FindOrAdd(TargetingHandle); + ImmeidateTaskData.bReleaseOnCompletion = true; + + TargetingSubsystem->ExecuteTargetingRequestWithHandle(TargetingHandle, Delegate); + } + } +} + +bool UGCS_TargetingSystemComponent::CanBeTargeted_Implementation(AActor* ActorToTarget) +{ + return true; +} + + +void UGCS_TargetingSystemComponent::StaticSwitchToNewTarget(bool RightDirection) +{ + if (TargetedActor) + { + TMap LocalPotentialTargets; + + for (TObjectPtr& Elem : PotentialTargets) + { + if (Elem != TargetedActor) + { + float LocalDistance = UKismetMathLibrary::Vector_Distance(GetOwner()->GetActorLocation(), Elem->GetActorLocation()); + + FRotator RequiredRotation = UKismetMathLibrary::FindLookAtRotation(GetOwner()->GetActorLocation(), Elem->GetActorLocation()); + FRotator DeltaRotation; + + ACharacter* LocalOwnerCharacter = Cast(GetOwner()); + + DeltaRotation = UKismetMathLibrary::NormalizedDeltaRotator(LocalOwnerCharacter->GetControlRotation(), RequiredRotation); + + if (RightDirection == 1) + { + if (DeltaRotation.Yaw < 0.f && DeltaRotation.Yaw > -100.f) + { + LocalPotentialTargets.Add(Elem, DeltaRotation.Yaw); + } + else + { + LocalPotentialTargets.Add(Elem, -10000.f); + } + } + else + { + if (DeltaRotation.Yaw > 0.f && DeltaRotation.Yaw < 100.f) + { + LocalPotentialTargets.Add(Elem, DeltaRotation.Yaw); + } + else + { + LocalPotentialTargets.Add(Elem, 10000.f); + } + } + } + } + + if (LocalPotentialTargets.Num() > 0) + { + TArray LocalTargets; + TArray LocalAngles; + LocalPotentialTargets.GenerateKeyArray(LocalTargets); + LocalPotentialTargets.GenerateValueArray(LocalAngles); + + if (RightDirection == 1) + { + int32 FoundIndex; + const float MaxValue = FMath::Max(LocalAngles, &FoundIndex); + if (MaxValue > -10000.f) + { + OnLockOff(); + SetTargetedActor(LocalTargets[FoundIndex]); + OnLockOn(); + } + } + else + { + int32 FoundIndex; + const float MinValue = FMath::Min(LocalAngles, &FoundIndex); + if (MinValue < 10000.f) + { + OnLockOff(); + SetTargetedActor(LocalTargets[FoundIndex]); + OnLockOn(); + } + } + } + } +} + + +float UGCS_TargetingSystemComponent::CalculateViewAngle(const AActor* TargetActor) +{ + APawn* OwningPawn = GetPawn(); + FRotator FinalRotation = FRotator::ZeroRotator; + if (TargetActor && OwningPawn) + { + FRotator RequiredRotation = UKismetMathLibrary::FindLookAtRotation(OwningPawn->GetPawnViewLocation(), TargetActor->GetActorLocation()); + + FRotator DeltaRotation = UKismetMathLibrary::NormalizedDeltaRotator(RequiredRotation, OwningPawn->GetViewRotation()); + FinalRotation = DeltaRotation; + } + + return UKismetMathLibrary::Abs(UKismetMathLibrary::Abs(FinalRotation.Yaw) + UKismetMathLibrary::Abs(FinalRotation.Pitch)); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.cpp new file mode 100644 index 0000000..0dc770e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.cpp @@ -0,0 +1,369 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.h" + +#include "CollisionQueryParams.h" +#include "KismetTraceUtils.h" +#include "Components/PrimitiveComponent.h" +#include "Engine/EngineTypes.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" +#include "Kismet/KismetSystemLibrary.h" +#include "TargetingSystem/TargetingSubsystem.h" + +#if ENABLE_DRAW_DEBUG +#include "Engine/Canvas.h" +#endif // ENABLE_DRAW_DEBUG + + +UGCS_TargetingSelectionTask_LineTrace::UGCS_TargetingSelectionTask_LineTrace(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + bComplexTrace = false; + bIgnoreSourceActor = false; + bIgnoreInstigatorActor = false; + bGenerateDefaultHitResult = true; +} + +void UGCS_TargetingSelectionTask_LineTrace::Execute(const FTargetingRequestHandle& TargetingHandle) const +{ + Super::Execute(TargetingHandle); + + SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Executing); + + if (IsAsyncTargetingRequest(TargetingHandle)) + { + ExecuteAsyncTrace(TargetingHandle); + } + else + { + ExecuteImmediateTrace(TargetingHandle); + } +} + +FVector UGCS_TargetingSelectionTask_LineTrace::GetSourceLocation_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceActor) + { + return SourceContext->SourceActor->GetActorLocation(); + } + + return SourceContext->SourceLocation; + } + + return FVector::ZeroVector; +} + +FVector UGCS_TargetingSelectionTask_LineTrace::GetSourceOffset_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return DefaultSourceOffset; +} + +FVector UGCS_TargetingSelectionTask_LineTrace::GetTraceDirection_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceActor) + { + if (APawn* Pawn = Cast(SourceContext->SourceActor)) + { + return Pawn->GetControlRotation().Vector(); + } + else + { + return SourceContext->SourceActor->GetActorForwardVector(); + } + } + } + + return FVector::ZeroVector; +} + +float UGCS_TargetingSelectionTask_LineTrace::GetTraceLength_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return DefaultTraceLength.GetValueAtLevel(GetTraceLevel(TargetingHandle)); +} + +void UGCS_TargetingSelectionTask_LineTrace::GetAdditionalActorsToIgnore_Implementation(const FTargetingRequestHandle& TargetingHandle, TArray& OutAdditionalActorsToIgnore) const +{ +} + +float UGCS_TargetingSelectionTask_LineTrace::GetTraceLevel_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return 0.0f; +} + +void UGCS_TargetingSelectionTask_LineTrace::ExecuteImmediateTrace(const FTargetingRequestHandle& TargetingHandle) const +{ + if (UWorld* World = GetSourceContextWorld(TargetingHandle)) + { +#if ENABLE_DRAW_DEBUG + ResetTraceResultsDebugString(TargetingHandle); +#endif // ENABLE_DRAW_DEBUG + + const FVector Direction = GetTraceDirection(TargetingHandle).GetSafeNormal(); + const FVector Start = (GetSourceLocation(TargetingHandle) + GetSourceOffset(TargetingHandle)); + const FVector End = Start + (Direction * GetTraceLength(TargetingHandle)); + + FCollisionQueryParams Params(SCENE_QUERY_STAT(ExecuteImmediateTrace), bComplexTrace); + InitCollisionParams(TargetingHandle, Params); + + bool bHasBlockingHit = false; + TArray Hits; + if (CollisionProfileName.Name != TEXT("NoCollision")) + { + bHasBlockingHit = World->LineTraceMultiByProfile(Hits, Start, End, CollisionProfileName.Name, Params); + } + else + { + const ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(TraceChannel); + bHasBlockingHit = World->LineTraceMultiByChannel(Hits, Start, End, CollisionChannel, Params); + } + +#if ENABLE_DRAW_DEBUG + DrawDebugTrace(TargetingHandle, Start, End, bHasBlockingHit, Hits); +#endif // ENABLE_DRAW_DEBUG + + ProcessHitResults(TargetingHandle, Hits); + } + + SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed); +} + +void UGCS_TargetingSelectionTask_LineTrace::ExecuteAsyncTrace(const FTargetingRequestHandle& TargetingHandle) const +{ + if (UWorld* World = GetSourceContextWorld(TargetingHandle)) + { + AActor* SourceActor = nullptr; + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + SourceActor = SourceContext->SourceActor; + } + const FVector Direction = GetTraceDirection(TargetingHandle).GetSafeNormal(); + const FVector Start = (GetSourceLocation(TargetingHandle) + GetSourceOffset(TargetingHandle)); + const FVector End = Start + (Direction * GetTraceLength(TargetingHandle)); + + FCollisionQueryParams Params(SCENE_QUERY_STAT(ExecuteAsyncTrace), bComplexTrace); + InitCollisionParams(TargetingHandle, Params); + + FTraceDelegate Delegate = FTraceDelegate::CreateUObject(this, &UGCS_TargetingSelectionTask_LineTrace::HandleAsyncTraceComplete, TargetingHandle); + if (CollisionProfileName.Name != TEXT("NoCollision")) + { + World->AsyncLineTraceByProfile(EAsyncTraceType::Multi, Start, End, CollisionProfileName.Name, Params, &Delegate); + } + else + { + const ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(TraceChannel); + World->AsyncLineTraceByChannel(EAsyncTraceType::Multi, Start, End, CollisionChannel, Params, FCollisionResponseParams::DefaultResponseParam, &Delegate); + } + } + else + { + SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed); + } +} + +void UGCS_TargetingSelectionTask_LineTrace::HandleAsyncTraceComplete(const FTraceHandle& InTraceHandle, FTraceDatum& InTraceDatum, FTargetingRequestHandle TargetingHandle) const +{ + if (TargetingHandle.IsValid()) + { +#if ENABLE_DRAW_DEBUG + ResetTraceResultsDebugString(TargetingHandle); + + // We have to manually find if there is a blocking hit. + bool bHasBlockingHit = false; + for (const FHitResult& HitResult : InTraceDatum.OutHits) + { + if (HitResult.bBlockingHit) + { + bHasBlockingHit = true; + break; + } + } + + DrawDebugTrace(TargetingHandle, InTraceDatum.Start, InTraceDatum.End, bHasBlockingHit, InTraceDatum.OutHits); + +#endif // ENABLE_DRAW_DEBUG + + ProcessHitResults(TargetingHandle, InTraceDatum.OutHits); + } + + SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed); +} + +void UGCS_TargetingSelectionTask_LineTrace::ProcessHitResults(const FTargetingRequestHandle& TargetingHandle, const TArray& Hits) const +{ + if (!TargetingHandle.IsValid()) + { + return; + } + FTargetingDefaultResultsSet& TargetingResults = FTargetingDefaultResultsSet::FindOrAdd(TargetingHandle); + + if (Hits.Num() > 0) + { + for (const FHitResult& HitResult : Hits) + { + if (!HitResult.GetActor()) + { + continue; + } + + bool bAddResult = true; + for (const FTargetingDefaultResultData& ResultData : TargetingResults.TargetResults) + { + if (ResultData.HitResult.GetActor() == HitResult.GetActor()) + { + bAddResult = false; + break; + } + } + + if (bAddResult) + { + FTargetingDefaultResultData* ResultData = new(TargetingResults.TargetResults) FTargetingDefaultResultData(); + ResultData->HitResult = HitResult; + } + } + +#if ENABLE_DRAW_DEBUG + BuildTraceResultsDebugString(TargetingHandle, TargetingResults.TargetResults); +#endif // ENABLE_DRAW_DEBUG + } + else if (bGenerateDefaultHitResult) + { + // If there were no hits, add a default HitResult at the end of the trace + FHitResult HitResult; + const FVector Start = (GetSourceLocation(TargetingHandle) + GetSourceOffset(TargetingHandle)); + const FVector End = Start + (GetTraceDirection(TargetingHandle) * GetTraceLength(TargetingHandle)); + // Start param could be player ViewPoint. We want HitResult to always display the StartLocation. + HitResult.TraceStart = Start; + HitResult.TraceEnd = End; + HitResult.Location = End; + HitResult.ImpactPoint = End; + FTargetingDefaultResultData* ResultData = new(TargetingResults.TargetResults) FTargetingDefaultResultData(); + ResultData->HitResult = HitResult; + } +} + +void UGCS_TargetingSelectionTask_LineTrace::InitCollisionParams(const FTargetingRequestHandle& TargetingHandle, FCollisionQueryParams& OutParams) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (bIgnoreSourceActor && SourceContext->SourceActor) + { + OutParams.AddIgnoredActor(SourceContext->SourceActor); + } + + if (bIgnoreInstigatorActor && SourceContext->InstigatorActor) + { + OutParams.AddIgnoredActor(SourceContext->InstigatorActor); + } + + TArray AdditionalActorsToIgnoreArray; + GetAdditionalActorsToIgnore(TargetingHandle, AdditionalActorsToIgnoreArray); + + if (AdditionalActorsToIgnoreArray.Num() > 0) + { + OutParams.AddIgnoredActors(AdditionalActorsToIgnoreArray); + } + } +} + +#if WITH_EDITOR +bool UGCS_TargetingSelectionTask_LineTrace::CanEditChange(const FProperty* InProperty) const +{ + bool bCanEdit = Super::CanEditChange(InProperty); + + if (bCanEdit && InProperty) + { + const FName PropertyName = InProperty->GetFName(); + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UGCS_TargetingSelectionTask_LineTrace, TraceChannel)) + { + return (CollisionProfileName.Name == TEXT("NoCollision")); + } + } + + return true; +} +#endif // WITH_EDITOR + + +#if ENABLE_DRAW_DEBUG +void UGCS_TargetingSelectionTask_LineTrace::DrawDebug(UTargetingSubsystem* TargetingSubsystem, FTargetingDebugInfo& Info, const FTargetingRequestHandle& TargetingHandle, float XOffset, float YOffset, + int32 MinTextRowsToAdvance) const +{ +#if WITH_EDITORONLY_DATA + if (UTargetingSubsystem::IsTargetingDebugEnabled()) + { + FTargetingDebugData& DebugData = FTargetingDebugData::FindOrAdd(TargetingHandle); + FString& ScratchPadString = DebugData.DebugScratchPadStrings.FindOrAdd(GetNameSafe(this)); + if (!ScratchPadString.IsEmpty()) + { + if (Info.Canvas) + { + Info.Canvas->SetDrawColor(FColor::Yellow); + } + + FString TaskString = FString::Printf(TEXT("Results : %s"), *ScratchPadString); + TargetingSubsystem->DebugLine(Info, TaskString, XOffset, YOffset, MinTextRowsToAdvance); + } + } +#endif // WITH_EDITORONLY_DATA +} + +void UGCS_TargetingSelectionTask_LineTrace::DrawDebugTrace(const FTargetingRequestHandle TargetingHandle, const FVector& StartLocation, const FVector& EndLocation, const bool bHit, + const TArray& Hits) const +{ + if (UTargetingSubsystem::IsTargetingDebugEnabled()) + { + if (UWorld* World = GetSourceContextWorld(TargetingHandle)) + { + const float DrawTime = UTargetingSubsystem::GetOverrideTargetingLifeTime(); + const EDrawDebugTrace::Type DrawDebugType = DrawTime <= 0.0f ? EDrawDebugTrace::Type::ForOneFrame : EDrawDebugTrace::Type::ForDuration; + const FLinearColor TraceColor = FLinearColor::Red; + const FLinearColor TraceHitColor = FLinearColor::Green; + DrawDebugLineTraceMulti(World, StartLocation, EndLocation, DrawDebugType, bHit, Hits, TraceColor, TraceHitColor, DrawTime); + } + } +} + +void UGCS_TargetingSelectionTask_LineTrace::BuildTraceResultsDebugString(const FTargetingRequestHandle& TargetingHandle, const TArray& TargetResults) const +{ +#if WITH_EDITORONLY_DATA + if (UTargetingSubsystem::IsTargetingDebugEnabled()) + { + FTargetingDebugData& DebugData = FTargetingDebugData::FindOrAdd(TargetingHandle); + FString& ScratchPadString = DebugData.DebugScratchPadStrings.FindOrAdd(GetNameSafe(this)); + + for (const FTargetingDefaultResultData& TargetData : TargetResults) + { + if (const AActor* Target = TargetData.HitResult.GetActor()) + { + if (ScratchPadString.IsEmpty()) + { + ScratchPadString = FString::Printf(TEXT("%s"), *GetNameSafe(Target)); + } + else + { + ScratchPadString += FString::Printf(TEXT(", %s"), *GetNameSafe(Target)); + } + } + } + } +#endif // WITH_EDITORONLY_DATA +} + +void UGCS_TargetingSelectionTask_LineTrace::ResetTraceResultsDebugString(const FTargetingRequestHandle& TargetingHandle) const +{ +#if WITH_EDITORONLY_DATA + FTargetingDebugData& DebugData = FTargetingDebugData::FindOrAdd(TargetingHandle); + FString& ScratchPadString = DebugData.DebugScratchPadStrings.FindOrAdd(GetNameSafe(this)); + ScratchPadString.Reset(); +#endif // WITH_EDITORONLY_DATA +} + +#endif // ENABLE_DRAW_DEBUG diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.cpp new file mode 100644 index 0000000..0827113 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.cpp @@ -0,0 +1,120 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.h" + +#include "GCS_LogChannels.h" +#include "Collision/DEPRECATED_GCS_CollisionTraceInstance.h" +#include "Targeting/GCS_TargetingSourceInterface.h" + +UDEPRECATED_GCS_CollisionTraceInstance* UGCS_TargetingSelectionTask_TraceExt::GetSourceTraceInstance_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceObject) + { + return Cast(SourceContext->SourceObject); + } + } + UE_LOG(LogGCS, Error, TEXT("No valid CollisionTraceInstance passed in as SourceObject! TargetingPreset:%s"), *GetOuter()->GetName()); + return nullptr; +} + +void UGCS_TargetingSelectionTask_TraceExt::Execute(const FTargetingRequestHandle& TargetingHandle) const +{ + Super::Execute(TargetingHandle); +} + +FVector UGCS_TargetingSelectionTask_TraceExt::GetSourceLocation_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (bUseContextLocationAsSourceLocation) + { + return SourceContext->SourceLocation; + } + if (SourceContext->SourceActor) + { + return SourceContext->SourceActor->GetActorLocation(); + } + + return SourceContext->SourceLocation; + } + + return FVector::ZeroVector; +} + +FVector UGCS_TargetingSelectionTask_TraceExt::GetTraceDirection_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceObject && SourceContext->SourceObject->GetClass()->ImplementsInterface(UGCS_TargetingSourceInterface::StaticClass())) + { + FVector TraceDirection; + if (IGCS_TargetingSourceInterface::Execute_GetTraceDirection(SourceContext->SourceObject, TraceDirection)) + { + return TraceDirection; + } + } + + return SourceContext->SourceLocation; + } + + return Super::GetTraceDirection_Implementation(TargetingHandle); +} + +void UGCS_TargetingSelectionTask_TraceExt::GetAdditionalActorsToIgnore_Implementation(const FTargetingRequestHandle& TargetingHandle, TArray& OutAdditionalActorsToIgnore) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceObject && SourceContext->SourceObject->GetClass()->ImplementsInterface(UGCS_TargetingSourceInterface::StaticClass())) + { + OutAdditionalActorsToIgnore.Append(IGCS_TargetingSourceInterface::Execute_GetAdditionalActorsToIgnore(SourceContext->SourceObject)); + } + } +} + +float UGCS_TargetingSelectionTask_TraceExt::GetTraceLevel_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (!IsValid(SourceContext->SourceObject)) + { + UE_LOG(LogGCS, Error, TEXT("No valid Context Source Object found! TargetingPreset:%s"), *GetOuter()->GetName()); + return 0; + } + if (!SourceContext->SourceObject->GetClass()->ImplementsInterface(UGCS_TargetingSourceInterface::StaticClass())) + { + UE_LOG(LogGCS, Error, TEXT("Source Object(%s) doesn't implements GCS_TargetingSourceInterface.! TargetingPreset:%s"), + *SourceContext->SourceObject->GetName(), *GetOuter()->GetName()); + return 0; + } + } + return 0; +} + +float UGCS_TargetingSelectionTask_TraceExt::GetTraceLength_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return bTraceLengthLevel ? DefaultTraceLength.GetValueAtLevel(GetTraceLevel(TargetingHandle)) : DefaultTraceLength.GetValue(); +} + +float UGCS_TargetingSelectionTask_TraceExt::GetSweptTraceRadius_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return bSweptTraceRadiusLevel ? DefaultSweptTraceRadius.GetValueAtLevel(GetTraceLevel(TargetingHandle)) : DefaultSweptTraceRadius.GetValue(); +} + +float UGCS_TargetingSelectionTask_TraceExt::GetSweptTraceCapsuleHalfHeight_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + return bSweptTraceCapsuleHalfHeightLevel ? DefaultSweptTraceCapsuleHalfHeight.GetValueAtLevel(GetTraceLevel(TargetingHandle)) : DefaultSweptTraceCapsuleHalfHeight.GetValue(); +} + +FVector UGCS_TargetingSelectionTask_TraceExt::GetSweptTraceBoxHalfExtents_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (bSweptTraceBoxHalfExtentLevel) + { + float Level = GetTraceLevel(TargetingHandle); + return FVector(DefaultSweptTraceBoxHalfExtentX.GetValueAtLevel(Level), DefaultSweptTraceBoxHalfExtentY.GetValueAtLevel(Level), DefaultSweptTraceBoxHalfExtentZ.GetValueAtLevel(Level)); + } + + return Super::GetSweptTraceBoxHalfExtents_Implementation(TargetingHandle); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.cpp new file mode 100644 index 0000000..2c3372f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.cpp @@ -0,0 +1,212 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.h" + +#include "GCS_LogChannels.h" +#include "Components/BoxComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/SphereComponent.h" +#include "Components/ShapeComponent.h" +#include "Misc/DataValidation.h" +#include "Targeting/GCS_TargetingFunctionLibrary.h" +#include "Targeting/GCS_TargetingSourceInterface.h" + +void UGCS_TargetingSelectionTask_TraceExt_BindShape::Execute(const FTargetingRequestHandle& TargetingHandle) const +{ + bool bMatchShapeType = true; + UShapeComponent* Shape = GetTraceShape(TargetingHandle); + + if (!IsValid(Shape)) + { + GCS_LOG(Warning, "") + bMatchShapeType = false; + } + if (TraceType == ETargetingTraceType::Box && !Shape->IsA()) + { + GCS_LOG(Warning, "%s: Trace type mismatched! want Box, got %s. %s", + *GetNameSafe(GetOuter()), + *GetNameSafe(Shape->GetClass()), + *UGCS_TargetingFunctionLibrary::GetTargetingSourceContextDebugString(TargetingHandle) + ) + bMatchShapeType = false; + } + if (TraceType == ETargetingTraceType::Capsule && !Shape->IsA()) + { + GCS_LOG(Warning, "%s: Trace type mismatched! want Capsule, got %s. %s", + *GetNameSafe(GetOuter()), + *GetNameSafe(Shape->GetClass()), + *UGCS_TargetingFunctionLibrary::GetTargetingSourceContextDebugString(TargetingHandle) + ) + bMatchShapeType = false; + } + if (TraceType == ETargetingTraceType::Sphere && !Shape->IsA()) + { + GCS_LOG(Warning, "%s: Trace type mismatched! want Capsule, got %s. %s", + *GetNameSafe(GetOuter()), + *GetNameSafe(Shape->GetClass()), + *UGCS_TargetingFunctionLibrary::GetTargetingSourceContextDebugString(TargetingHandle) + ) + bMatchShapeType = false; + } + + if (bMatchShapeType) + { + Super::Execute(TargetingHandle); + } + else + { + SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed); + } +} + +float UGCS_TargetingSelectionTask_TraceExt_BindShape::GetSweptTraceRadius_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + float BaseValue = -1; + + if (USphereComponent* Shape = Cast(GetTraceShape(TargetingHandle))) + { + BaseValue = Shape->GetScaledSphereRadius(); + } + if (const UCapsuleComponent* Shape = Cast(GetTraceShape(TargetingHandle))) + { + BaseValue = Shape->GetScaledCapsuleRadius(); + } + + float Value = Super::GetSweptTraceRadius_Implementation(TargetingHandle); + + if (BaseValue < 0) + { + return Value; + } + + if (SweptTraceRadiusModType == EGCS_TraceDataModifyType::Add) + { + return BaseValue + Value; + } + + if (SweptTraceRadiusModType == EGCS_TraceDataModifyType::Multiply) + { + return BaseValue * Value; + } + return BaseValue; +} + +float UGCS_TargetingSelectionTask_TraceExt_BindShape::GetSweptTraceCapsuleHalfHeight_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + float BaseValue = -1; + + if (const UCapsuleComponent* Capsule = Cast(GetTraceShape(TargetingHandle))) + { + BaseValue = Capsule->GetScaledCapsuleHalfHeight(); + } + + float Value = Super::GetSweptTraceCapsuleHalfHeight_Implementation(TargetingHandle); + + if (BaseValue < 0) + { + return Value; + } + + if (SweptTraceCapsuleHalfHeightModType == EGCS_TraceDataModifyType::Add) + { + return BaseValue + Value; + } + + if (SweptTraceCapsuleHalfHeightModType == EGCS_TraceDataModifyType::Multiply) + { + return BaseValue * Value; + } + return BaseValue; +} + +FVector UGCS_TargetingSelectionTask_TraceExt_BindShape::GetSweptTraceBoxHalfExtents_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + FVector BaseValue = FVector::ZeroVector; + + if (const UBoxComponent* Shape = Cast(GetTraceShape(TargetingHandle))) + { + BaseValue = Shape->GetScaledBoxExtent(); + } + + FVector Value = Super::GetSweptTraceBoxHalfExtents_Implementation(TargetingHandle); + + if (BaseValue == FVector::ZeroVector) + { + return Value; + } + + if (SweptTraceBoxHalfExtentModType == EGCS_TraceDataModifyType::Add) + { + return BaseValue + Value; + } + + if (SweptTraceBoxHalfExtentModType == EGCS_TraceDataModifyType::Multiply) + { + return BaseValue * Value; + } + return BaseValue; +} + +UShapeComponent* UGCS_TargetingSelectionTask_TraceExt_BindShape::GetTraceShape(const FTargetingRequestHandle& TargetingHandle) const +{ + UShapeComponent* ShapeComponent = nullptr; + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceObject) + { + if (SourceContext->SourceObject->GetClass()->ImplementsInterface(UGCS_TargetingSourceInterface::StaticClass())) + { + if (IGCS_TargetingSourceInterface::Execute_GetTraceShape(SourceContext->SourceObject, ShapeComponent)) + { + return ShapeComponent; + } + UE_LOG(LogGCS, VeryVerbose, TEXT("Source Object(%s) doesn't provide valid ShapeComponent! TargetingPreset:%s"), + *SourceContext->SourceObject->GetName(), *GetOuter()->GetName()); + } + else + { + UE_LOG(LogGCS, VeryVerbose, TEXT("Source Object(%s) doesn't implements GCS_TargetingSourceInterface.! TargetingPreset:%s"), + *SourceContext->SourceObject->GetName(), *GetOuter()->GetName()); + } + } + else + { + UE_LOG(LogGCS, Error, TEXT("No valid Context Source Object found! TargetingPreset:%s"), *GetOuter()->GetName()); + } + } + return nullptr; +} + +FRotator UGCS_TargetingSelectionTask_TraceExt_BindShape::GetSweptTraceRotation_Implementation(const FTargetingRequestHandle& TargetingHandle) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (SourceContext->SourceObject && SourceContext->SourceObject->GetClass()->ImplementsInterface(UGCS_TargetingSourceInterface::StaticClass())) + { + FRotator TraceRotation; + if (IGCS_TargetingSourceInterface::Execute_GetSweptTraceRotation(SourceContext->SourceObject, TraceRotation)) + { + return TraceRotation; + } + } + } + return Super::GetSweptTraceRotation_Implementation(TargetingHandle); +} + +#if WITH_EDITORONLY_DATA +EDataValidationResult UGCS_TargetingSelectionTask_TraceExt_BindShape::IsDataValid(class FDataValidationContext& Context) const +{ + if (TraceType == ETargetingTraceType::Line) + { + FString Txt = FString::Format(TEXT("TraceType == Line is not allowed in this type of task:{0} "), {GetClass()->GetName()}); + Context.AddError(FText::FromString(Txt)); + } + if (bUseContextLocationAsSourceLocation) + { + FString Txt = FString::Format(TEXT("bUseContextLocationAsSourceLocation is not allowed in this type of task:{0} "), {GetClass()->GetName()}); + Context.AddError(FText::FromString(Txt)); + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentComponent.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentComponent.cpp new file mode 100644 index 0000000..b198c1d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentComponent.cpp @@ -0,0 +1,87 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Team/GCS_CombatTeamAgentComponent.h" +#include "GCS_LogChannels.h" +#include "GameFramework/Controller.h" +#include "GameFramework/Pawn.h" +#include "GenericTeamAgentInterface.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" + +// Sets default values for this component's properties +UGCS_CombatTeamAgentComponent::UGCS_CombatTeamAgentComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); + // ... +} + +void UGCS_CombatTeamAgentComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams SharedParams; + SharedParams.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, CombatTeamId, SharedParams); +} + +FGCS_CombatTeamIdChangedSignature* UGCS_CombatTeamAgentComponent::GetOnTeamIdChangedDelegate() +{ + return &OnTeamIdChangedEvent; +} + +FGenericTeamId UGCS_CombatTeamAgentComponent::GetCombatTeamId_Implementation() const +{ + return CombatTeamId; +} + +void UGCS_CombatTeamAgentComponent::SetCombatTeamId_Implementation(FGenericTeamId NewTeamId) +{ + if (GetOwner()->HasAuthority()) + { + const FGenericTeamId OldTeamID = CombatTeamId; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, CombatTeamId, this); + CombatTeamId = NewTeamId; + if (bAssignTeamIdToController) + { + if (APawn* Pawn = Cast(GetOwner())) + { + if (IGenericTeamAgentInterface* AgentInterface = Cast(Pawn->GetController())) + { + AgentInterface->SetGenericTeamId(NewTeamId); + } + } + } + + ConditionalBroadcastTeamChanged(this, OldTeamID, NewTeamId); + } + else + { + UE_LOG(LogGCS, Error, TEXT("Cannot set team for %s on non-authority"), *GetPathName(this)); + } +} + +void UGCS_CombatTeamAgentComponent::OnRep_CombatTeamId(FGenericTeamId OldTeamID) +{ + ConditionalBroadcastTeamChanged(this, OldTeamID, CombatTeamId); +} + +// Called when the game starts +void UGCS_CombatTeamAgentComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... +} + +// Called every frame +void UGCS_CombatTeamAgentComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // ... +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentInterface.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentInterface.cpp new file mode 100644 index 0000000..08290fa --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Team/GCS_CombatTeamAgentInterface.cpp @@ -0,0 +1,36 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Team/GCS_CombatTeamAgentInterface.h" + +#include "GCS_LogChannels.h" + + +// Add default functionality here for any IGCS_CombatTeamAgentInterface functions that are not pure virtual. +void IGCS_CombatTeamAgentInterface::SetCombatTeamId_Implementation(FGenericTeamId NewTeamId) +{ + if (IGenericTeamAgentInterface* TeamAgentInterface = Cast(_getUObject())) + { + TeamAgentInterface->SetGenericTeamId(NewTeamId); + } +} + +FGenericTeamId IGCS_CombatTeamAgentInterface::GetCombatTeamId_Implementation() const +{ + if (IGenericTeamAgentInterface* TeamAgentInterface = Cast(_getUObject())) + { + return TeamAgentInterface->GetGenericTeamId(); + } + + return FGenericTeamId::NoTeam; +} + +void IGCS_CombatTeamAgentInterface::ConditionalBroadcastTeamChanged(TScriptInterface This, FGenericTeamId OldTeamID, FGenericTeamId NewTeamID) +{ + if (OldTeamID != NewTeamID) + { + UObject* ThisObj = This.GetObject(); + UE_LOG(LogGCS, Verbose, TEXT("[%s] %s assigned team %d"), *GetClientServerContextString(ThisObj), *GetPathNameSafe(ThisObj), NewTeamID.GetId()); + This.GetInterface()->GetTeamChangedDelegateChecked().Broadcast(ThisObj, OldTeamID, NewTeamID); + } +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_AttackDefinitionFunctionLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_AttackDefinitionFunctionLibrary.cpp new file mode 100644 index 0000000..27b1d28 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_AttackDefinitionFunctionLibrary.cpp @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utility/GCS_AttackDefinitionFunctionLibrary.h" + +#include "CombatFlow/GCS_AttackDefinition.h" + +#if WITH_EDITOR +void UGCS_AttackDefinitionFunctionLibrary::MigrateAttackDefinitionTable(UDataTable* InTable) +{ + if (InTable && InTable->GetRowStruct()->IsChildOf(FGCS_AttackDefinition::StaticStruct())) + { + TArray Rows; + InTable->GetAllRows(TEXT("Migration"),Rows); + + InTable->MarkPackageDirty(); + } +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_CombatFunctionLibrary.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_CombatFunctionLibrary.cpp new file mode 100644 index 0000000..9f9a28a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Utility/GCS_CombatFunctionLibrary.cpp @@ -0,0 +1,473 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utility/GCS_CombatFunctionLibrary.h" +#include "Components/SkeletalMeshComponent.h" +#include "GCS_CombatEntityInterface.h" +#include "GCS_CombatSystemSettings.h" +#include "Team/GCS_CombatTeamAgentInterface.h" +#include "GCS_LogChannels.h" +#include "GGA_GameplayEffectContext.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/Controller.h" +#include "AbilitySystem/GCS_GameplayEffectContext.h" +#include "CombatFlow/GCS_AttackRequest.h" +#include "GameFramework/Character.h" +#include "Kismet/KismetMathLibrary.h" +#include "Utilities/GGA_GameplayEffectFunctionLibrary.h" +#include "Weapon/GCS_WeaponInterface.h" + +TScriptInterface UGCS_CombatFunctionLibrary::GetCombatTeamAgentInterface(AActor* Actor) +{ + if (IsValid(Actor)) + { + if (Actor->GetClass()->ImplementsInterface(UGCS_CombatTeamAgentInterface::StaticClass())) + { + return Actor; + } + TArray Components = Actor->GetComponentsByInterface(UGCS_CombatTeamAgentInterface::StaticClass()); + return Components.IsValidIndex(0) ? Components[0] : nullptr; + } + return nullptr; +} + +bool UGCS_CombatFunctionLibrary::FindCombatTeamAgentInterface(AActor* Actor, TScriptInterface& OutInterface) +{ + OutInterface = GetCombatTeamAgentInterface(Actor); + return OutInterface != nullptr; +} + +TScriptInterface UGCS_CombatFunctionLibrary::GetCombatEntityInterface(AActor* Actor) +{ + if (IsValid(Actor)) + { + if (Actor->GetClass()->ImplementsInterface(UGCS_CombatEntityInterface::StaticClass())) + { + return Actor; + } + TArray Components = Actor->GetComponentsByInterface(UGCS_CombatEntityInterface::StaticClass()); + return Components.IsValidIndex(0) ? Components[0] : nullptr; + } + return nullptr; +} + +UObject* UGCS_CombatFunctionLibrary::GetCombatEntity(AActor* Actor) +{ + if (IsValid(Actor)) + { + if (Actor->GetClass()->ImplementsInterface(UGCS_CombatEntityInterface::StaticClass())) + { + return Actor; + } + TArray Components = Actor->GetComponentsByInterface(UGCS_CombatEntityInterface::StaticClass()); + return Components.IsValidIndex(0) ? Components[0] : nullptr; + } + return nullptr; +} + +TScriptInterface UGCS_CombatFunctionLibrary::GetWeaponInterface(AActor* Actor) +{ + if (IsValid(Actor)) + { + if (Actor->GetClass()->ImplementsInterface(UGCS_WeaponInterface::StaticClass())) + { + return Actor; + } + TArray Components = Actor->GetComponentsByInterface(UGCS_WeaponInterface::StaticClass()); + return Components.IsValidIndex(0) ? Components[0] : nullptr; + } + return nullptr; +} + +USkeletalMeshComponent* UGCS_CombatFunctionLibrary::GetMainCharacterMeshComponent(AActor* Actor, FName OverrideMeshLookupTag) +{ + if (IsValid(Actor)) + { + if (OverrideMeshLookupTag != NAME_None) + { + TArray Components = Actor->GetComponentsByTag(USkeletalMeshComponent::StaticClass(), OverrideMeshLookupTag); + if (Components.IsValidIndex(0)) + { + return Cast(Components[0]); + } + } + else if (const UGCS_CombatSystemSettings* Settings = UGCS_CombatSystemSettings::Get()) + { + TArray Components = Actor->GetComponentsByTag(USkeletalMeshComponent::StaticClass(), Settings->CharacterMeshLookupTag); + if (Components.IsValidIndex(0)) + { + return Cast(Components[0]); + } + } + + if (ACharacter* Char = Cast(Actor)) + { + return Char->GetMesh(); + } + + if (USkeletalMeshComponent* Component = Cast(Actor->GetComponentByClass(USkeletalMeshComponent::StaticClass()))) + { + return Component; + } + + UE_LOG(LogGCS, Warning, TEXT("Failed to find main character mesh component on actor class:%s"), *Actor->GetClass()->GetName()); + } + + return nullptr; +} + +UMeshComponent* UGCS_CombatFunctionLibrary::GetMainMeshComponent(AActor* Actor, FName OverrideMeshLookupTag) +{ + if (IsValid(Actor)) + { + if (OverrideMeshLookupTag != NAME_None) + { + if (UActorComponent* Component = Actor->FindComponentByTag(UMeshComponent::StaticClass(), OverrideMeshLookupTag)) + { + return Cast(Component); + } + } + else if (const UGCS_CombatSystemSettings* Settings = UGCS_CombatSystemSettings::Get()) + { + TArray Components = Actor->GetComponentsByTag(UMeshComponent::StaticClass(), Settings->CharacterMeshLookupTag); + if (Components.IsValidIndex(0)) + { + return Cast(Components[0]); + } + } + + if (UMeshComponent* Component = Cast(Actor->GetComponentByClass(UMeshComponent::StaticClass()))) + { + return Component; + } + + UE_LOG(LogGCS, Warning, TEXT("Failed to find main mesh component on actor class:%s"), *Actor->GetClass()->GetName()); + } + + return nullptr; +} + +TArray UGCS_CombatFunctionLibrary::GetSocketNamesWithPrefix(const USceneComponent* Component, FString Prefix, ESearchCase::Type SearchCase) +{ + if (IsValid(Component)) + { + return Component->GetAllSocketNames().FilterByPredicate([&](const FName& SocketName) + { + return SocketName.ToString().StartsWith(Prefix, SearchCase); + }); + } + return {}; +} + +bool UGCS_CombatFunctionLibrary::FindCombatInterface(AActor* Actor, TScriptInterface& OutInterface) +{ + OutInterface = GetCombatEntityInterface(Actor); + return OutInterface.GetObject() != nullptr; +} + +bool UGCS_CombatFunctionLibrary::FindWeaponInterface(AActor* Actor, TScriptInterface& OutInterface) +{ + OutInterface = GetWeaponInterface(Actor); + return OutInterface.GetObject() != nullptr; +} + +FRotator UGCS_CombatFunctionLibrary::CalculateAngleBetweenActors(const AActor* From, const AActor* To) +{ + if (IsValid(From) && IsValid(To)) + { + return UKismetMathLibrary::NormalizedDeltaRotator(UKismetMathLibrary::FindLookAtRotation(From->GetActorLocation(), To->GetActorLocation()), From->GetActorRotation()); + } + + // TODO Warning. + return FRotator::ZeroRotator; +} + +bool UGCS_CombatFunctionLibrary::IsSameCombatTeam(const AActor* A, const AActor* B) +{ + return GetCombatTeamId(A) == GetCombatTeamId(B); +} + +FGenericTeamId UGCS_CombatFunctionLibrary::GetCombatTeamId(const AActor* Actor) +{ + return QueryCombatTeamId(Actor, true, true); +} + +FGenericTeamId UGCS_CombatFunctionLibrary::QueryCombatTeamId(const AActor* Actor, bool bCombatAgent, bool bGenericAgent) +{ + if (!IsValid(Actor)) + { + return FGenericTeamId::NoTeam; + } + + if (bCombatAgent) + { + if (Actor->GetClass()->ImplementsInterface(UGCS_CombatTeamAgentInterface::StaticClass())) + { + return IGCS_CombatTeamAgentInterface::Execute_GetCombatTeamId(Actor); + } + + TArray Components = Actor->GetComponentsByInterface(UGCS_CombatTeamAgentInterface::StaticClass()); + if (Components.IsValidIndex(0)) + { + return IGCS_CombatTeamAgentInterface::Execute_GetCombatTeamId(Components[0]); + } + + if (const APawn* Pawn = Cast(Actor)) + { + if (Pawn->GetController() && Pawn->GetController()->GetClass()->ImplementsInterface(UGCS_CombatTeamAgentInterface::StaticClass())) + { + return IGCS_CombatTeamAgentInterface::Execute_GetCombatTeamId(Pawn->GetController()); + } + } + } + + if (bGenericAgent) + { + if (const IGenericTeamAgentInterface* GenericTeamAgentInterface = Cast(Actor)) + { + return GenericTeamAgentInterface->GetGenericTeamId(); + } + + if (const APawn* Pawn = Cast(Actor)) + { + if (const IGenericTeamAgentInterface* GenericTeamAgentInterface = Cast(Pawn->GetController())) + { + return GenericTeamAgentInterface->GetGenericTeamId(); + } + } + } + + + return FGenericTeamId::NoTeam; +} + +EGCS_Direction UGCS_CombatFunctionLibrary::CalculateDirectionFromAngle(const float Angle) +{ + if (UKismetMathLibrary::InRange_FloatFloat(Angle, -45.0f, 45.0f)) + { + return EGCS_Direction::Forward; + } + + if (UKismetMathLibrary::InRange_FloatFloat(Angle, 45.0f, 135.f)) + { + return EGCS_Direction::Right; + } + + if (UKismetMathLibrary::InRange_FloatFloat(Angle, -135.f, -45.f)) + { + return EGCS_Direction::Left; + } + return EGCS_Direction::Backward; +} + +TSoftObjectPtr UGCS_CombatFunctionLibrary::SelectMontageByDirection(EGCS_Direction Direction, TArray> Montages) +{ + switch (Direction) + { + case EGCS_Direction::Forward: + return Montages.IsValidIndex(0) ? Montages[0] : nullptr; + case EGCS_Direction::Backward: + return Montages.IsValidIndex(1) ? Montages[1] : nullptr; + case EGCS_Direction::Left: + return Montages.IsValidIndex(2) ? Montages[2] : nullptr; + case EGCS_Direction::Right: + return Montages.IsValidIndex(3) ? Montages[3] : nullptr; + } + return nullptr; +} + +void UGCS_CombatFunctionLibrary::AddTaggedValue(TArray& TaggedValues, FGameplayTag Tag, float ValueToAdd) +{ + bool bFound = false; + for (FGCS_TaggedValue& TaggedValue : TaggedValues) + { + if (TaggedValue.Attribute == Tag) + { + TaggedValue.Value += ValueToAdd; + bFound = true; + break; + } + } + + if (!bFound) + { + FGCS_TaggedValue Temp; + Temp.Attribute = Tag; + Temp.Value = ValueToAdd; + TaggedValues.Add(Temp); + } +} + +float UGCS_CombatFunctionLibrary::GetTaggedValue(const TArray TaggedValues, FGameplayTag Tag) +{ + for (const FGCS_TaggedValue& TaggedValue : TaggedValues) + { + if (TaggedValue.Attribute == Tag) + { + return TaggedValue.Value; + } + } + + return 0; +} + +FGameplayTagContainer UGCS_CombatFunctionLibrary::FilterGameplayTagContainer(const FGameplayTagContainer& TagContainer, FGameplayTagContainer OtherContainer) +{ + return TagContainer.Filter(OtherContainer); +} + +FGameplayEffectSpecHandle UGCS_CombatFunctionLibrary::AddAttackHandleToGameplayEffectSpec(FGameplayEffectSpecHandle SpecHandle, FDataTableRowHandle AttackHandle) +{ + if (SpecHandle.IsValid() && !AttackHandle.IsNull()) + { + if (FGCS_AttackDefinition* AtkDef = AttackHandle.GetRow(TEXT("AddAttackHandleToGameplayEffectSpec"))) + { + AddAttackDefinitionToGameplayEffectSpec(SpecHandle, *AtkDef); + } + + FGameplayEffectContextHandle ContextHandle = SpecHandle.Data->GetEffectContext(); + EffectContextSetAttackDefinitionHandle(ContextHandle, AttackHandle); + } + return SpecHandle; +} + +FGameplayEffectSpecHandle UGCS_CombatFunctionLibrary::AddAttackDefinitionToGameplayEffectSpec(FGameplayEffectSpecHandle SpecHandle, const FGCS_AttackDefinition& AtkDefinition) +{ + if (SpecHandle.IsValid()) + { + SpecHandle.Data->AppendDynamicAssetTags(AtkDefinition.AttackTags); + + // apply set by callers from atk definition. + + for (const TTuple& ByCallerMagnitude : AtkDefinition.SetByCallerMagnitudes) + { + if (ByCallerMagnitude.Key.IsValid() && ByCallerMagnitude.Value > 0) + { + SpecHandle.Data->SetSetByCallerMagnitude(ByCallerMagnitude.Key, ByCallerMagnitude.Value); + } + } + } + + return SpecHandle; +} + +void UGCS_CombatFunctionLibrary::AddAttackHandleToGameplayEffectContainerSpec(FGGA_GameplayEffectContainerSpec ContainerSpec, FDataTableRowHandle AttackHandle) +{ + if (!AttackHandle.IsNull()) + { + if (FGCS_AttackDefinition* AtkDef = AttackHandle.GetRow(TEXT("AddAttackHandleToGameplayEffectSpec"))) + { + for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs) + { + AddAttackDefinitionToGameplayEffectSpec(SpecHandle, *AtkDef); + FGameplayEffectContextHandle ContextHandle = SpecHandle.Data->GetEffectContext(); + EffectContextSetAttackDefinitionHandle(ContextHandle, AttackHandle); + } + } + } +} + +void UGCS_CombatFunctionLibrary::EffectContextSetAttackDefinitionHandle(FGameplayEffectContextHandle EffectContext, FDataTableRowHandle Handle) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + Payload->AtkDataTable = Handle.DataTable; + Payload->AtkRowName = Handle.RowName; + } + else + { + UE_LOG(LogGCS, Error, TEXT("Can't access GCS_GameplayEffectContext! You need to setup GCS_AbilitySystemGlobals as AbilitySystemGlobalsClassName.")) + } +} + +FGCS_ContextPayload_Combat UGCS_CombatFunctionLibrary::EffectContextGetCombatPayload(FGameplayEffectContextHandle EffectContext) +{ + if (FGGA_GameplayEffectContext* Context = UGGA_GameplayEffectFunctionLibrary::GetEffectContextPtr(EffectContext)) + { + if (FGCS_ContextPayload_Combat* CombatPayload = Context->FindMutablePayloadByType()) + { + return *CombatPayload; + } + } + return FGCS_ContextPayload_Combat(); +} + +FGCS_ContextPayload_Combat* UGCS_CombatFunctionLibrary::EffectContextGetMutableCombatPayload(const FGameplayEffectContextHandle& EffectContext) +{ + if (FGGA_GameplayEffectContext* Context = UGGA_GameplayEffectFunctionLibrary::GetEffectContextPtr(EffectContext)) + { + if (FGCS_ContextPayload_Combat* CombatPayload = Context->FindOrAddMutablePayloadPtr()) + { + return CombatPayload; + } + } + return nullptr; +} + +void UGCS_CombatFunctionLibrary::EffectContextAddTagToCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag TagToAdd) +{ + if (TagToAdd.IsValid()) + { + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + Payload->DynamicTags.AddTagFast(TagToAdd); + } + } +} + +void UGCS_CombatFunctionLibrary::EffectContextSetTaggedValueToCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag Tag, float NewValue) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + Payload->SetTaggedValue(Tag, NewValue); + } +} + +float UGCS_CombatFunctionLibrary::EffectContextGetTaggedValueFromCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag Tag) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + return Payload->GetTaggedValue(Tag); + } + return 0; +} + +void UGCS_CombatFunctionLibrary::EffectContextGetDynamicTagsFromCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTagContainer& OutTags) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + OutTags.Reset(); + OutTags = Payload->DynamicTags; + } +} + +FDataTableRowHandle UGCS_CombatFunctionLibrary::EffectContextGetAttackDefinitionHandle(FGameplayEffectContextHandle EffectContext) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + FDataTableRowHandle Handle; + Handle.DataTable = Payload->AtkDataTable; + Handle.RowName = Payload->AtkRowName; + return Handle; + } + return FDataTableRowHandle(); +} + +bool UGCS_CombatFunctionLibrary::EffectContextGetIsPredictingContext(FGameplayEffectContextHandle EffectContext) +{ + if (FGCS_ContextPayload_Combat* Payload = EffectContextGetMutableCombatPayload(EffectContext)) + { + return Payload->bIsPredictingContext; + } + return false; +} + +FGCS_AttackDefinition UGCS_CombatFunctionLibrary::EffectContextGetAttackDefinition(FGameplayEffectContextHandle EffectContext) +{ + FDataTableRowHandle Handle = EffectContextGetAttackDefinitionHandle(EffectContext); + if (FGCS_AttackDefinition* Def = Handle.GetRow(TEXT("EffectContextGetAttackDefinition"))) + { + return *Def; + } + return FGCS_AttackDefinition(); +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_AttachmentRelationshipMapping.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_AttachmentRelationshipMapping.cpp new file mode 100644 index 0000000..6076704 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_AttachmentRelationshipMapping.cpp @@ -0,0 +1,112 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Weapon/GCS_AttachmentRelationshipMapping.h" +#include "Animation/Skeleton.h" +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "Components/SkeletalMeshComponent.h" +#include "UObject/ObjectSaveContext.h" + +bool UGCS_AttachmentRelationshipMapping::FindRelationshipForMesh(const USkeletalMeshComponent* InSkeletalMeshComponent, const UStaticMesh* InStaticMesh, const USkeletalMesh* InSkeletalMesh, + FName InSocketName, FGCS_AttachmentRelationship& OutRelationship) const +{ + bool bFoundMatchingSkeleton = false; + if (bUseNameMatching) + { + if (!CompatibleSkeletonNames.IsEmpty() && InSkeletalMeshComponent && InSkeletalMeshComponent->GetSkeletalMeshAsset()) + { + for (const FString& SkeletonName : CompatibleSkeletonNames) + { + if (SkeletonName.IsEmpty()) + continue; + + if (USkeleton* Skeleton = InSkeletalMeshComponent->GetSkeletalMeshAsset()->GetSkeleton()) + { + if (Skeleton->GetName() == SkeletonName) + { + bFoundMatchingSkeleton = true; + break; + } + } + } + } + } + else + { + if (!CompatibleSkeletons.IsEmpty() && InSkeletalMeshComponent && InSkeletalMeshComponent->GetSkeletalMeshAsset()) + { + for (TSoftObjectPtr CompatibleSkeleton : CompatibleSkeletons) + { + if (CompatibleSkeleton.IsNull()) + continue; + + if (!CompatibleSkeleton.IsValid()) + { + CompatibleSkeleton.LoadSynchronous(); + } + if (CompatibleSkeleton == InSkeletalMeshComponent->GetSkeletalMeshAsset()->GetSkeleton()) + { + bFoundMatchingSkeleton = true; + break; + } + } + } + } + + + if (!bFoundMatchingSkeleton) + { + return false; + } + + for (const FGCS_AttachmentRelationship& Rel : Relationships) + { + if (Rel.SocketName != InSocketName) + { + continue; + } + + if (IsValid(InStaticMesh) && Rel.StaticMesh.IsValid() && InStaticMesh == Rel.StaticMesh.Get()) + { + OutRelationship = Rel; + return true; + } + + if (IsValid(InSkeletalMesh) && Rel.SkeletalMesh.IsValid() && InSkeletalMesh == Rel.SkeletalMesh.Get()) + { + OutRelationship = Rel; + return true; + } + } + return false; +} + +#if WITH_EDITOR + +void UGCS_AttachmentRelationshipMapping::PreSave(FObjectPreSaveContext SaveContext) +{ + for (FGCS_AttachmentRelationship& Relationship : Relationships) + { + if (Relationship.SocketName == NAME_None) + { + Relationship.EditorFriendlyName = "Invalid setup"; + } + else + { + Relationship.EditorFriendlyName = FString::Format(TEXT("Adjust SM({0})/SKM({1}) for Socket({2})"), + { + Relationship.StaticMesh.IsNull() ? TEXT("None") : Relationship.StaticMesh.LoadSynchronous()->GetName(), + Relationship.SkeletalMesh.IsNull() ? TEXT("None") : Relationship.SkeletalMesh.LoadSynchronous()->GetName(), + Relationship.SocketName.ToString() + }); + } + } + Super::PreSave(SaveContext); +} + +EDataValidationResult UGCS_AttachmentRelationshipMapping::IsDataValid(class FDataValidationContext& Context) const +{ + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponActor.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponActor.cpp new file mode 100644 index 0000000..df4111d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponActor.cpp @@ -0,0 +1,188 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Weapon/GCS_WeaponActor.h" +#include "Components/PrimitiveComponent.h" +#include "GameFramework/Pawn.h" +#include "GCS_LogChannels.h" +#include "Collision/GCS_TraceSystemComponent.h" +#include "Misc/DataValidation.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" + +AGCS_WeaponActor::AGCS_WeaponActor(const FObjectInitializer& ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + bReplicates = true; + bWeaponActive = false; +} + +APawn* AGCS_WeaponActor::GetWeaponOwner_Implementation() const +{ + return Cast(GetOwner()); +} + +const FGameplayTagContainer AGCS_WeaponActor::GetWeaponTags_Implementation() const +{ + return WeaponTags; +} + +void AGCS_WeaponActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, bWeaponActive, Parameters) +} + +void AGCS_WeaponActor::SetWeaponActive_Implementation(bool bNewActive) +{ + if (GetOwner()->HasAuthority()) + { + const bool prev = bWeaponActive; + bWeaponActive = bNewActive; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, bWeaponActive, this); + OnWeaponActiveStateChanged(prev); + } +} + +bool AGCS_WeaponActor::IsWeaponActive_Implementation() const +{ + return bWeaponActive; +} + +UPrimitiveComponent* AGCS_WeaponActor::GetPrimitiveComponent_Implementation() const +{ + if (WeaponMeshTagName.IsValid()) + { + UPrimitiveComponent* Primitive = Cast(GetOwner()->FindComponentByTag(UPrimitiveComponent::StaticClass(), WeaponMeshTagName)); + if (!IsValid(Primitive)) + { + GCS_CLOG(Warning, "failed to find weapon mesh via tag (%s) as weapon primitive component. weapon owner:%s", + *WeaponMeshTagName.ToString(), + *GetOwner()->GetName()); + return nullptr; + } + return Primitive; + } + GCS_CLOG(Warning, "no weapon primitive component provided. weapon owner:%s", *GetOwner()->GetName()); + return nullptr; +} + +void AGCS_WeaponActor::GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const +{ + TagContainer = Execute_GetWeaponTags(this); +} + +void AGCS_WeaponActor::OnWeaponActiveStateChanged_Implementation(bool Prev) +{ + OnWeaponActiveStateChangedEvent.Broadcast(bWeaponActive); +} + +void AGCS_WeaponActor::BeginPlay() +{ + Super::BeginPlay(); +} + +void AGCS_WeaponActor::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (Execute_IsWeaponActive(this)) + { + bWeaponActive = false; + RefreshTraceInstance(); + } + Super::EndPlay(EndPlayReason); +} + +void AGCS_WeaponActor::RefreshTraceInstance_Implementation() +{ + APawn* Pawn = Execute_GetWeaponOwner(this); + if (!IsValid(Pawn)) + { + GCS_CLOG(Warning, "mising weapon owner!") + return; + } + UGCS_TraceSystemComponent* TSC = UGCS_TraceSystemComponent::GetTraceSystemComponent(Pawn); + if (!IsValid(TSC)) + { + GCS_CLOG(Warning, "missing trace system on weapon owner(%s)!", *GetNameSafe(Pawn)) + return; + } + if (bWeaponActive) + { + TraceHandles.Reset(); + for (const FGCS_TraceDefinition& TraceDefinition : TraceDefinitions) + { + UPrimitiveComponent* Primitive = GetSourceComponentForTrace(TraceDefinition.TraceTag); + if (Primitive == nullptr) + { + GCS_CLOG(Error, "No SourceComponent provided for trace:%s,check your GetSourceComponentForTrace implementation.", *TraceDefinition.TraceTag.ToString()); + continue; + } + FGCS_TraceHandle Handle = TSC->AddTrace(TraceDefinition, Primitive, GetSourceObjectForTrace()); + if (Handle.IsValidHandle()) + { + TraceHandles.Add(Handle); + } + } + TSC->OnTraceHitEvent.AddDynamic(this, &ThisClass::OnAnyTraceHit); + TSC->OnTraceStateChangedEvent.AddDynamic(this, &ThisClass::OnAnyTraceStateChanged); + } + else + { + TSC->OnTraceHitEvent.RemoveDynamic(this, &ThisClass::OnAnyTraceHit); + TSC->OnTraceStateChangedEvent.RemoveDynamic(this, &ThisClass::OnAnyTraceStateChanged); + for (const FGCS_TraceHandle& TraceHandle : TraceHandles) + { + TSC->RemoveTrace(TraceHandle); + } + } +} + +UObject* AGCS_WeaponActor::GetSourceObjectForTrace_Implementation() +{ + return this; +} + +UPrimitiveComponent* AGCS_WeaponActor::GetSourceComponentForTrace_Implementation(const FGameplayTag& TraceTag) const +{ + // Default Using weapon primitive as source component for trace + if (UPrimitiveComponent* PrimitiveComponent = Execute_GetPrimitiveComponent(this)) + { + return PrimitiveComponent; + } + + GCS_CLOG(Error, "Weapon(%s) didn't return return valid primitive component to be used as SourceComponent for traces, try implement this function to get properly SourceComponent For Trace.", + *GetNameSafe(this)) + return nullptr; +} + +void AGCS_WeaponActor::OnAnyTraceHit_Implementation(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult) +{ +} + +void AGCS_WeaponActor::OnAnyTraceStateChanged_Implementation(const FGCS_TraceHandle& TraceHandle, bool NewState) +{ +} + +void AGCS_WeaponActor::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); +} + +#if WITH_EDITOR +EDataValidationResult AGCS_WeaponActor::IsDataValid(class FDataValidationContext& Context) const +{ + for (int32 i = 0; i < TraceDefinitions.Num(); i++) + { + if (!TraceDefinitions[i].IsValidDefinition()) + { + Context.AddWarning(FText::FromString(FString::Format(TEXT("Found invalid trace definition at index({0})"), {i}))); + return EDataValidationResult::Invalid; + } + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponInterface.cpp b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponInterface.cpp new file mode 100644 index 0000000..d1b4ae3 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Private/Weapon/GCS_WeaponInterface.cpp @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Weapon/GCS_WeaponInterface.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" + +// Add default functionality here for any IGCS_WeaponInterface functions that are not pure virtual. +UPrimitiveComponent* IGCS_WeaponInterface::GetPrimitiveComponent_Implementation() const +{ + return nullptr; +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_CombatAbility.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_CombatAbility.h new file mode 100644 index 0000000..28e4c44 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_CombatAbility.h @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_GameplayAbility.h" +#include "GCS_CombatAbility.generated.h" + +class IGCS_CombatEntityInterface; +class UGCS_CombatSystemComponent; + +/** + * Base combat ability. + * 基础战斗能力。 + */ +UCLASS(Abstract) +class GENERICCOMBATSYSTEM_API UGCS_CombatAbility : public UGGA_GameplayAbility +{ + GENERATED_BODY() + +protected: + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Ability") + UGCS_CombatSystemComponent* GetCombatSystemFromActorInfo() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Ability") + UObject* GetCombatEntityFromActorInfo() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Ability") + TScriptInterface GetCombatEntityInterfaceFromActorInfo() const; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_ComboAbility.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_ComboAbility.h new file mode 100644 index 0000000..56a88f7 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Abilities/GCS_ComboAbility.h @@ -0,0 +1,105 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatAbility.h" +#include "Combo/GCS_ComboDefinition.h" +#include "GCS_ComboAbility.generated.h" + +class UGCS_CombatSystemComponent; +class UAS_Combat; + +/** + * This combo ability acted as manager of sub abilities. + */ +UCLASS(Abstract, HideCategories=(Cooldowns,Input,GampelayEffects)) +class GENERICCOMBATSYSTEM_API UGCS_ComboAbility : public UGCS_CombatAbility +{ + GENERATED_BODY() + +public: + UGCS_ComboAbility(); + + virtual void PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData = nullptr) override; + virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) override; + virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags = nullptr, + const FGameplayTagContainer* TargetTags = nullptr, FGameplayTagContainer* OptionalRelevantTags = nullptr) const override; + virtual void OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override; + virtual void OnRemoveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override; + + virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, + bool bWasCancelled) override; + +protected: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Combat Ability") + bool AllowAdvanceCombo() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Combat Ability") + void StartCombo(const FGameplayEventData& ComboEvent); + virtual void StartCombo_Implementation(const FGameplayEventData& ComboEvent); + + /** + * Advance combo with ComboEventData as context. + * @param ComboEventData The data used as combo context. 游戏事件数据作为连击上下文。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Combat Ability") + void AdvanceCombo(const FGameplayEventData& ComboEventData); + virtual void AdvanceCombo_Implementation(const FGameplayEventData& ComboEventData); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Combat Ability") + void ResetCombo(); + + UFUNCTION() + virtual void HandleAbilityEnd(const FAbilityEndedData& AbilityEndedData); + + virtual bool SelectComboDefinition(const FGameplayEventData& ComboEventData, int32 CurrentStep, FGCS_ComboDefinition& OutDefinition); + + /** + * This is where you can use the extension filed within your combo definition to apply additional rules. + * 这里你可以使用连击定义中的自定义字段来添加额外的选择规则。 + * @param ComboEvent The combo event data to provide as context.连击事件数据,用作上下文参考。 + * @param CurrentStep The current combo step of combat system. 当前的连击步骤。 + * @param ComboDefinition The combo definition you are checking. 正在检查的连击定义。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Combat Ability") + bool CanSelectedComboDefinition(const FGameplayEventData& ComboEvent, int32 CurrentStep, const FGCS_ComboDefinition& ComboDefinition) const; + + virtual void HandleComboExecution(const FGameplayEventData& ComboEventData); + + // virtual void GiveSubAbilities(const FGameplayAbilitySpec& CurrentSpec); + // virtual void RemoveSubAbilities(); + +protected: + bool bCurrentAbilityEnded = false; + + //The ability current combo step was executing. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combo") + FGameplayAbilitySpecHandle CurrentAbility; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combo") + TSubclassOf CurrentAbilityClass; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combo") + FGameplayAbilitySpecHandle NextAbility; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combo") + TSubclassOf NextAbilityAbilityClass; + + int32 DesiredComboStep{INDEX_NONE}; + + /** + * Granted potential combo abilities. + * 赋予的潜在ComboAbilities. + */ + // UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combo") + // TArray AvailableAbilities; + + FDelegateHandle AbilityEndedDelegateHandle; + +#if WITH_EDITOR + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Attributes/AS_Poise.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Attributes/AS_Poise.h new file mode 100644 index 0000000..2ea4376 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Attributes/AS_Poise.h @@ -0,0 +1,124 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" + +#include "AS_Poise.generated.h" + +namespace AS_Poise +{ + + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Poise) + + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MaxPoise) + + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(PoiseRecover) + + +} + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class GENERICCOMBATSYSTEM_API UAS_Poise : public UAttributeSet +{ + GENERATED_BODY() + + +public: + + UAS_Poise(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + + virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; + + virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override; + + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + // Current Poise value of an actor.(actor的当前抗打击值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Poise, Category = "Attribute|PoiseSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData Poise{ 3 }; + ATTRIBUTE_ACCESSORS(ThisClass, Poise) + + // Max Poise value of an actor.(actor的最大抗打击值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxPoise, Category = "Attribute|PoiseSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData MaxPoise{ 3 }; + ATTRIBUTE_ACCESSORS(ThisClass, MaxPoise) + + // How many Poise to recover per second.(每秒恢复抗打击值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_PoiseRecover, Category = "Attribute|PoiseSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData PoiseRecover{ 1 }; + ATTRIBUTE_ACCESSORS(ThisClass, PoiseRecover) + + + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetPoiseAttribute"), Category = "Attribute|PoiseSet") + static FGameplayAttribute Bp_GetPoiseAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetPoise"), Category = "Attribute|PoiseSet") + float Bp_GetPoise() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetPoise"), Category = "Attribute|PoiseSet") + void Bp_SetPoise(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitPoise"), Category = "Attribute|PoiseSet") + void Bp_InitPoise(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetMaxPoiseAttribute"), Category = "Attribute|PoiseSet") + static FGameplayAttribute Bp_GetMaxPoiseAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetMaxPoise"), Category = "Attribute|PoiseSet") + float Bp_GetMaxPoise() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetMaxPoise"), Category = "Attribute|PoiseSet") + void Bp_SetMaxPoise(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitMaxPoise"), Category = "Attribute|PoiseSet") + void Bp_InitMaxPoise(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetPoiseRecoverAttribute"), Category = "Attribute|PoiseSet") + static FGameplayAttribute Bp_GetPoiseRecoverAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetPoiseRecover"), Category = "Attribute|PoiseSet") + float Bp_GetPoiseRecover() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetPoiseRecover"), Category = "Attribute|PoiseSet") + void Bp_SetPoiseRecover(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitPoiseRecover"), Category = "Attribute|PoiseSet") + void Bp_InitPoiseRecover(float NewValue); + + + + +protected: + + /** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */ + virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty); + + UFUNCTION() + virtual void OnRep_Poise(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_MaxPoise(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_PoiseRecover(const FGameplayAttributeData& OldValue); + +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.h new file mode 100644 index 0000000..c973e8a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/DEPRECATED_GCS_AbilitySystemGlobals.h @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemGlobals.h" +#include "DEPRECATED_GCS_AbilitySystemGlobals.generated.h" + +UCLASS(Deprecated, meta=(DeprecationMessage="GCS_AbilitySystemGlobals is deprecated. Please use GGA_AbilitySystemGlobals instead.")) +class GENERICCOMBATSYSTEM_API UDEPRECATED_GCS_AbilitySystemGlobals : public UGGA_AbilitySystemGlobals +{ + GENERATED_BODY() + +public: + virtual FGameplayEffectContext* AllocGameplayEffectContext() const override; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.h new file mode 100644 index 0000000..be53b08 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Effects/GCS_GEComponent_PredictivelyExecute.h @@ -0,0 +1,24 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectComponent.h" +#include "GCS_GEComponent_PredictivelyExecute.generated.h" + +/** + * This component will predictively execute instant GE which treated as infinite one. + * @attention Internally will mark this effect context as local predicting context, so you can conditional apply logic in subsequent codes. + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_GEComponent_PredictivelyExecute : public UGameplayEffectComponent +{ + GENERATED_BODY() + +public: + virtual void OnGameplayEffectApplied(FActiveGameplayEffectsContainer& ActiveGEContainer, FGameplayEffectSpec& GESpec, FPredictionKey& PredictionKey) const override; + +private: + UPROPERTY(EditDefaultsOnly, Category = GCS) + bool bPredictGameplayCues{false}; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/GCS_GameplayEffectContext.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/GCS_GameplayEffectContext.h new file mode 100644 index 0000000..7b2a1cf --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/GCS_GameplayEffectContext.h @@ -0,0 +1,74 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatStructLibrary.h" +#include "UObject/Object.h" +#include "GCS_GameplayEffectContext.generated.h" + +/** + * The Combat related data which was carried and pass around within gameplay effect context, as one of the gameplay effect context payload. + * 战斗相关数据,在游戏效果上下文中被携带和传递,作为游戏效果上下文数据之一。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_ContextPayload_Combat +{ + GENERATED_BODY() + + + void SetTaggedValue(const FGameplayTag& Tag, float NewValue); + + float GetTaggedValue(const FGameplayTag& Tag) const; + + /** + * Indicate the effect spec owning this context was Predictively executed. + * 表示拥有此上下文的效果实例是以客户端预测方式执行的。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + bool bIsPredictingContext{false}; + + UPROPERTY() + FPredictionKey PredictionKey; + + /** + * Attack definition data table. + * 攻击定义数据表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS", meta=(RequiredAssetDataTags = "RowStructure=/Script/GenericCombatSystem.GCS_AttackDefinition")) + TObjectPtr AtkDataTable{nullptr}; + + /** + * Row name in the attack definition table. + * 攻击定义表中的行名。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FName AtkRowName{NAME_None}; + + /** + * Bullet definition data table. + * 子弹定义数据表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS", meta=(RequiredAssetDataTags = "RowStructure=/Script/GenericCombatSystem.GCS_BulletDefinition")) + TObjectPtr BulletDataTable{nullptr}; + + /** + * Row name in the bullet definition table. + * 子弹定义表中的行名。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FName BulletRowName{NAME_None}; + + /** + * The tags added during gameplay effect apply(coming from MMC,or Execution). + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayTagContainer DynamicTags; + + /** + * Array of tagged values associated with the combat process. + * 与战斗过程关联的标记值数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + TArray TaggedValues; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.h b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.h new file mode 100644 index 0000000..827d09c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/AbilitySystem/Tasks/GCS_AbilityTask_CollisionTrace.h @@ -0,0 +1,116 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "Collision/GCS_TraceStructLibrary.h" +#include "Components/SkeletalMeshComponent.h" +#include "GCS_AbilityTask_CollisionTrace.generated.h" + +class UGCS_AttackRequest_Melee; + +/** + * Ability task for handling collision traces in combat. + * 处理战斗中碰撞检测的能力任务。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_AbilityTask_CollisionTrace : public UAbilityTask +{ + GENERATED_BODY() + +public: + UGCS_AbilityTask_CollisionTrace(); + /** + * Creates and activates a collision trace task. + * 创建并激活碰撞检测任务。 + * @param OwningAbility The owning gameplay ability. 所属游戏能力。 + * @param TaskInstanceName The name of the task instance. 任务实例名称。 + * @param bAdjustVisibilityBasedAnimTickOption Whether to adjust visibility-based animation ticking. 是否调整基于可见性的动画tick。 + * @return The created task. 创建的任务。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|AbilityTasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGCS_AbilityTask_CollisionTrace* HandleCollisionTraces(UGameplayAbility* OwningAbility, FName TaskInstanceName, bool bAdjustVisibilityBasedAnimTickOption = false); + + /** + * Activates the task. + * 激活任务。 + */ + virtual void Activate() override; + + /** + * Called when the task is destroyed. + * 任务销毁时调用。 + * @param bInOwnerFinished Whether the owner finished the task. 拥有者是否完成了任务。 + */ + virtual void OnDestroy(bool bInOwnerFinished) override; + + /** + * Adds a melee attack request to the task. + * 向任务添加近战攻击请求。 + * @param Request The melee attack request. 近战攻击请求。 + * @param SourceObject Optional source object. 可选的源对象。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|AbilityTasks") + void AddMeleeRequest(const UGCS_AttackRequest_Melee* Request, UObject* SourceObject); + + /** + * Removes a melee attack request from the task. + * 从任务移除近战攻击请求。 + * @param Request The melee attack request. 近战攻击请求。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|AbilityTasks") + void RemoveMeleeRequest(const UGCS_AttackRequest_Melee* Request); + + /** + * Delegate for trace instance hit events. + * 碰撞检测实例命中事件的委托。 + */ + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGCS_OnTraceInstanceHitSignature, const UGCS_AttackRequest_Melee*, MeleeRequest, const FGCS_TraceHandle&, TraceHandle, + const FHitResult&, + HitResult); + + /** + * Fired when a trace instance detects targets. + * 当碰撞检测实例检测到目标时触发。 + */ + UPROPERTY(BlueprintAssignable) + FGCS_OnTraceInstanceHitSignature OnTargetsFound; + +protected: + /** + * Handles trace instance hit events. + * 处理碰撞检测实例命中事件。 + * @param TraceHandle The trace instance. 碰撞检测实例。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION() + void TraceHitCallback(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult); + + /** + * Map of melee requests to their associated trace instances. + * 近战请求及其关联碰撞检测实例的映射。 + */ + TMap, TArray> MeleeRequests; + + /** + * Whether to adjust visibility-based animation ticking. + * 是否调整基于可见性的动画tick。 + */ + UPROPERTY() + bool bAdjustAnimTickOption{false}; + + /** + * Whether the animation tick option was adjusted. + * 是否已调整动画tick选项。 + */ + UPROPERTY() + bool bAdjustedAnimTickOption{false}; + + /** + * The previous animation tick option. + * 之前的动画tick选项。 + */ + UPROPERTY() + EVisibilityBasedAnimTickOption PrevAnimTickOption{EVisibilityBasedAnimTickOption::AlwaysTickPose}; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletContainer.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletContainer.h new file mode 100644 index 0000000..44b8493 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletContainer.h @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GCS_BulletStructLibrary.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Object.h" +#include "GCS_BulletContainer.generated.h" + + +struct FGCS_BulletContainer; +class UGCS_BulletSystemComponent; + +/** + * Structure representing an equipment entry in the container. + * 表示容器中装备条目的结构体。 + * @note WIP + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_BulletEntry : public FFastArraySerializerItem +{ + GENERATED_BODY() + + friend FGCS_BulletContainer; + + //The request id of this entry. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGuid Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGCS_BulletSpawnParameters SpawnParameters; +}; + +/** + * Container for a list of applied equipment. + * 存储已应用装备列表的容器。 + */ +USTRUCT() +struct GENERICCOMBATSYSTEM_API FGCS_BulletContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + FGCS_BulletContainer() + : OwningComponent(nullptr) + { + } + + FGCS_BulletContainer(UGCS_BulletSystemComponent* InComponent) + : OwningComponent(InComponent) + { + } + + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Entries, DeltaParms, *this); + } + + + int32 IndexOfById(const FGuid& Id) const; + + UPROPERTY(VisibleAnywhere, Category="BulletSystem", meta=(ShowOnlyInnerProperties, DisplayName="Bullets")) + TArray Entries; + + UPROPERTY() + TObjectPtr OwningComponent; +}; + +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum { WithNetDeltaSerializer = true }; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletInstance.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletInstance.h new file mode 100644 index 0000000..d7745b8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletInstance.h @@ -0,0 +1,406 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_BulletStructLibrary.h" +#include "GCS_EffectCauserInterface.h" +#include "GameFramework/Actor.h" +#include "GCS_BulletInstance.generated.h" + +class UGCS_AttackRequest_Bullet; +class UGCS_BulletSubsystem; +class UProjectileMovementComponent; + +/** + * Base class for bullet instances. + * 子弹实例的基类。 + */ +UCLASS(Abstract, BlueprintType, NotBlueprintable) +class GENERICCOMBATSYSTEM_API AGCS_BulletInstance : public AActor, public IGCS_EffectCauserInterface +{ + GENERATED_BODY() + + friend UGCS_BulletSubsystem; + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + AGCS_BulletInstance(); + + /** + * Gets lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the projectile movement component. + * 获取子弹的运动组件。 + * @return The projectile movement component. 运动组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + UProjectileMovementComponent* GetProjectileMovementComponent() const; + + /** + * Sets the bullet definition handle. + * 设置子弹定义句柄。 + * @param NewHandle The new definition handle. 新定义句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet") + void SetDefinitionHandle(UPARAM(meta=(RowType="/Script/GenericCombatSystem.GCS_BulletDefinition")) FDataTableRowHandle NewHandle); + + /** + * Sets the bullet's unique ID. + * 设置子弹的唯一ID。 + * @param NewId The new ID. 新ID。 + */ + void SetBulletId(const FGuid& NewId); + + /** + * Gets the bullet's unique ID. + * 获取子弹的唯一ID。 + * @return The bullet ID. 子弹ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + FGuid GetBulletId() const; + + /** + * Sets the parent bullet ID for bullet chains. + * 设置子弹链的父子弹ID。 + * @param NewParentId The parent bullet ID. 父子弹ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void SetParentBulletId(FGuid NewParentId); + + /** + * Gets the parent bullet ID. + * 获取父子弹ID。 + * @return The parent bullet ID. 父子弹ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + FGuid GetParentBulletId() const; + + /** + * Launches the bullet. + * 发射子弹。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void LaunchBullet(); + + /** + * Sets the hit result for the bullet. + * 设置子弹的命中结果。 + * @param NewHitResult The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet", meta=(DisplayName="Set Bullet HitResult")) + void SetHitResult(const FHitResult& NewHitResult); + + /** + * Gets the latest hit result. + * 获取最新的命中结果。 + * @return The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet", meta=(DisplayName="Get Bullet HitResult")) + const FHitResult& GetHitResult() const; + + /** + * Checks if the bullet has gameplay authority. + * 检查子弹是否具有游戏权限。 + * @return True if the bullet has authority. 如果子弹具有权限返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + bool HasGameplayAuthority() const; + + /** + * Called after network initialization. + * 网络初始化后调用。 + */ + virtual void PostNetInit() override; + + /** + * Called after receiving network data. + * 接收网络数据后调用。 + */ + virtual void PostNetReceive() override; + + /** + * Handles locally predicted bullet instances. + * 处理本地预测的子弹实例。 + * @param PredictedBullet The predicted bullet instance. 预测的子弹实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void FoundLocalPredictedBullet(AGCS_BulletInstance* PredictedBullet); + + // Effect causer interface + /** + * Gets the gameplay effect spec handle. + * 获取游戏效果规格句柄。 + * @param OutHandle The effect spec handle (output). 效果规格句柄(输出)。 + * @return True if successful. 如果成功返回true。 + */ + virtual bool GetEffectSpecHandle_Implementation(FGameplayEffectSpecHandle& OutHandle) override; + + /** + * Gets the gameplay effect container. + * 获取游戏效果容器。 + * @return The effect container. 效果容器。 + */ + virtual FGGA_GameplayEffectContainer GetEffectContainer_Implementation() const override; + + /** + * Gets the effect container level override. + * 获取效果容器级别覆盖。 + * @return The level override. 级别覆盖。 + */ + virtual int32 GetEffectContainerLevelOverride_Implementation() const override; + + /** + * Sets the effect container spec. + * 设置效果容器规格。 + * @param InEffectContainerSpec The effect container spec. 效果容器规格。 + */ + virtual void SetEffectContainerSpec_Implementation(const FGGA_GameplayEffectContainerSpec& InEffectContainerSpec) override; + + /** + * Gets the effect container spec. + * 获取效果容器规格。 + * @return The effect container spec. 效果容器规格。 + */ + virtual FGGA_GameplayEffectContainerSpec GetEffectContainerSpec_Implementation() const override; + + /** + * Gets the effect class. + * 获取效果类。 + * @return The effect class. 效果类。 + */ + virtual TSubclassOf GetEffectClass_Implementation() const override; + + /** + * Gets the effect level. + * 获取效果级别。 + * @return The effect level. 效果级别。 + */ + virtual int32 GetEffectLevel_Implementation() const override; + + /** + * Sets the effect spec. + * 设置效果规格。 + * @param InEffectSpec The effect spec. 效果规格。 + */ + virtual void SetEffectSpec_Implementation(FGameplayEffectSpecHandle& InEffectSpec) override; + + /** + * Gets the bullet's shape component. + * 获取子弹的形状组件。 + * @return The shape component. 形状组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GCS|Bullet") + UShapeComponent* GetBulletShape() const; + virtual UShapeComponent* GetBulletShape_Implementation() const; + +protected: + /** + * Called when the game starts or when spawned. + * 游戏开始或生成时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends or the bullet is destroyed. + * 游戏结束或子弹销毁时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Called when the bullet is taken from the pool with a valid definition. + * 子弹从池中取出且具有有效定义时调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void OnBulletBeginPlay(); + + /** + * Called when the bullet is returned to the pool and deactivated. + * 子弹返回池中并停用时调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void OnBulletEndPlay(); + + /** + * Records the initial location and rotation of the bullet. + * 记录子弹的初始位置和旋转。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet") + void SetupInitialLocationAndRotation(); + + /** + * Refreshes the bullet's travel states. + * 刷新子弹的移动状态。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet") + virtual void RefreshTravelStates(); + + /** + * Checks if the hit result should be penetrated. + * 检查命中结果是否应穿透。 + * @param InHitResult The hit result. 命中结果。 + * @return True if the hit should be penetrated. 如果命中应穿透返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + virtual bool ShouldPenetrateHitResult(const FHitResult& InHitResult) const; + + /** + * Checks if a bullet chain should be generated. + * 检查是否应生成子弹链。 + * @return True if a bullet chain should be generated. 如果应生成子弹链返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GCS|Bullet") + bool ShouldGenerateBullet(); + + /** + * Handles bullet chain logic on hit. + * 处理命中时的子弹链逻辑。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void HandleBulletHitChains(); + + /** + * Applies gameplay effects to the hit result. + * 对命中结果应用游戏效果。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Bullet") + void ApplyGameplayEffects(FHitResult HitResult); + + /** + * Called when the bullet ID is replicated. + * 子弹ID复制时调用。 + * @param Prev The previous bullet ID. 之前的子弹ID。 + */ + UFUNCTION() + virtual void OnRep_BulletId(FGuid Prev); + + /** + * Called when the bullet definition is replicated. + * 子弹定义复制时调用。 + */ + UFUNCTION() + void OnRep_BulletDefinition(); + + /** + * Unique ID of the bullet. + * 子弹的唯一ID。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnRep_BulletId, Category="GCS|BulletState") + FGuid BulletId; + + /** + * Whether the bullet is locally predicted. + * 子弹是否为本地预测。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS|BulletState") + bool bIsLocalPredicting{false}; + + /** + * Whether the bullet was spawned on the server. + * 子弹是否在服务器上生成。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS|BulletState") + bool bServerInitiated{false}; + + /** + * Parent bullet ID for bullet chains. + * 子弹链的父子弹ID。 + */ + UPROPERTY(VisibleAnywhere, Category="GCS|BulletState") + FGuid ParentBulletId; + + /** + * The associated attack request. + * 关联的攻击请求。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS|BulletState") + TObjectPtr Request; + + /** + * Handle to the bullet definition. + * 子弹定义的句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS|Bullet", ReplicatedUsing=OnRep_BulletDefinition, meta=(ExposeOnSpawn)) + FDataTableRowHandle DefinitionHandle; + + /** + * Loaded bullet definition. + * 加载的子弹定义。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS|BulletState") + FGCS_BulletDefinition Definition; + + /** + * The projectile movement component. + * 子弹的运动组件。 + */ + UPROPERTY(VisibleAnywhere, Category="GCS|Bullet") + TObjectPtr ProjectileMovement; + + /** + * The gameplay effect spec carried by the bullet. + * 子弹携带的游戏效果规格。 + */ + UPROPERTY(VisibleAnywhere, Category="GCS|BulletState") + FGameplayEffectSpecHandle EffectSpecHandle; + + /** + * The gameplay effect container spec carried by the bullet. + * 子弹携带的游戏效果容器规格。 + */ + UPROPERTY(VisibleAnywhere, Category = "GCS|BulletState") + FGGA_GameplayEffectContainerSpec EffectContainerSpec; + + /** + * Index for bullet chains. + * 子弹链的索引。 + */ + UPROPERTY() + int32 ChainIndex{0}; + + /** + * Initial location of the bullet. + * 子弹的初始位置。 + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category="GCS|BulletState") + FVector InitialActorLocation; + + /** + * Initial rotation of the bullet. + * 子弹的初始旋转。 + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category="GCS|BulletState") + FRotator InitialActorRotation; + + /** + * The latest hit result. + * 最新的命中结果。 + */ + UPROPERTY(VisibleAnywhere, Category="GCS|BulletState") + FHitResult LastHitResult; + + /** + * Distance traveled by the bullet. + * 子弹的移动距离。 + */ + UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category="GCS|BulletState") + double TraveledDistance{0.0f}; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void Tick(float DeltaTime) override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletStructLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletStructLibrary.h new file mode 100644 index 0000000..05aae8b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletStructLibrary.h @@ -0,0 +1,351 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatStructLibrary.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "Collision/GCS_TraceStructLibrary.h" +#include "UObject/Object.h" +#include "GCS_BulletStructLibrary.generated.h" + +class UGCS_AttackRequest_Bullet; +class UNiagaraSystem; +class AGCS_BulletInstance; + + +/** + * Base struct allow you to extend the bullet definition's fields using C++. + * 基础结构体,允许你通过C++拓展子弹定义的字段。 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICCOMBATSYSTEM_API FGCS_BulletDefinitionExtension +{ + GENERATED_BODY() +}; + + +/** + * Data structure defining bullet properties and behavior. + * 定义子弹属性和行为的数据结构。 + */ +USTRUCT(BlueprintType, meta=(DisplayName="GCS Bullet Definition")) +struct GENERICCOMBATSYSTEM_API FGCS_BulletDefinition : public FTableRowBase +{ + GENERATED_BODY() + + /** + * The bullet actor class. + * 子弹Actor类。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", meta=(AllowAbstract="false")) + TSoftClassPtr BulletActorClass; + + /** + * Duration for which the bullet exists (-1 for infinite). + * 子弹存在的持续时间(-1表示无限)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common") + float Duration{3.0}; + + /** + * Number of bullets fired at once. + * 一次性发射的子弹数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Launch Configuration", meta=(ClampMin=1, UIMin=1)) + int32 BulletCount{1}; + + /** + * Yaw angle for bullet launch. + * 子弹发射的水平角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Launch Configuration") + float LaunchAngle{0.0f}; + + /** + * Yaw angle interval between bullets. + * 子弹之间的水平角间隔。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Launch Configuration") + float LaunchAngleInterval{10.0f}; + + /** + * Pitch angle for bullet launch. + * 子弹发射的仰角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Launch Configuration") + float LaunchElevationAngle{0.0f}; + + /** + * Distance at which bullet attenuation begins. + * 子弹开始衰减的距离。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float AttenuationRange{800.0f}; + + /** + * Gravity scale within the attenuation range. + * 衰减范围内的重力系数。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement") + float GravityScaleInRange{1.0f}; + + /** + * Gravity scale outside the attenuation range. + * 衰减范围外的重力系数。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement") + float GravityScaleOutRage{1.0f}; + + /** + * Initial hit radius for the bullet. + * 子弹的初始命中半径。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float InitialHitRadius{20.0f}; + + /** + * Final hit radius for the bullet (-1 to use initial radius). + * 子弹的最终命中半径(-1使用初始半径)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float FinalHitRadius{-1.0f}; + + /** + * Initial speed of the bullet. + * 子弹的初始速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float InitialSpeed{1500.0f}; + + /** + * Minimum speed of the bullet. + * 子弹的最小速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float MinSpeed{1500.0f}; + + /** + * Maximum speed of the bullet. + * 子弹的最大速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement", meta=(Units="cm")) + float MaxSpeed{1500.0f}; + + /** + * Handle to the attack definition for the bullet. + * 子弹的攻击定义句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Attack", meta=(RowType="/Script/GenericCombatSystem.GCS_AttackDefinition")) + FDataTableRowHandle AttackDefinition; + + /** + * Trace definitions for hit detection. + * 用于命中检测的碰撞检测定义。 + * @note Overrides trace definitions in the bullet instance class. + * @注意 覆盖子弹实例类中的碰撞检测定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Trace") + TArray TraceDefinitions; + + /** + * Visual effect for the bullet projectile (Niagara). + * 子弹的视觉效果(Niagara)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VFX") + TSoftObjectPtr ProjectileFX; + + /** + * Visual effect for the bullet projectile (Cascade). + * 子弹的视觉效果(Cascade)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VFX") + TSoftObjectPtr ProjectileFX_Cascade; + + /** + * Visual effect for bullet impact (Niagara). + * 子弹命中的视觉效果(Niagara)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VFX") + TSoftObjectPtr ImpactFX; + + /** + * Visual effect for bullet impact (Cascade). + * 子弹命中的视觉效果(Cascade)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VFX") + TSoftObjectPtr ImpactFX_Cascade; + + /** + * Sound effect for bullet impact. + * 子弹命中的音效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="SFX") + TSoftObjectPtr ImpactSFX; + + /** + * Sound effect attached to the bullet projectile. + * 附着在子弹上的音效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="SFX") + TSoftObjectPtr ProjectileSFX; + + /** + * Sound effect played once when the bullet spawns. + * 子弹生成时播放一次的音效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="SFX") + TSoftObjectPtr SpawnSFX; + + /** + * Whether the bullet penetrates characters/pawns. + * 子弹是否穿透角色/Pawn。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Penetration") + bool bPenetrateCharacter{false}; + + /** + * Whether the bullet penetrates map geometry. + * 子弹是否穿透地图几何体。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Penetration") + bool bPenetrateMap{false}; + + /** + * Handle to the bullet definition to spawn on hit/expiration. + * 命中或失效时生成的子弹定义句柄。 + * @note Cannot be the same as this bullet. + * @注意 不能与此子弹相同。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Hit Configuration", meta=(RowType="/Script/GenericCombatSystem.GCS_BulletDefinition")) + FDataTableRowHandle HitBulletDefinition; + + /** + * Condition for launching bullet chains. + * 子弹链的发射条件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Hit Configuration", meta=(Categories="GGF.Combat.Bullet.LaunchCond")) + FGameplayTag LaunchCondition{FGameplayTag::EmptyTag}; + + /** + * Native Instanced struct for extending the bullet definition. + * 实例化结构体用于扩充子弹定义的字段。 + * @attention For C++ users only. 仅针对C++用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + TInstancedStruct NativeExtension; + + /** + * Blueprint Instanced struct for extending the bullet definition. + * 实例化结构体用于扩充子弹定义的字段。 + * @attention For blueprint users only. 仅针对蓝图用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + FInstancedStruct Extension; + + /** + * Custom user settings for extending the bullet definition. + * 扩展子弹定义的自定义用户设置。 + */ + UE_DEPRECATED(1.5, "Using extension field to add custom fields!") + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Deprecated", meta=(ForceInlineRow, BaseStruct = "/Script/GenericCombatSystem.GCS_UserSetting")) + TMap UserSettings; + + /** + * Shares the hit history to sub bullet.(prevent repeat hit for whole bullet chains.) + */ + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Hit Configuration",meta=(Categories="GGF.Combat.Bullet.LaunchCond")) + // bool bUseSharedHitList{true}; + + /** + * The amount of time between a bullet hits something and when it explodes. + */ + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Hit Configuration", meta=(Units="s", ClampMin=0)) + // float ExplosionDelay{0.0}; + + // Emitter will be added in next version. + + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Emitter", meta=(RowType="/Script/GenericCombatSystem.GCS_BulletDefinition")) + // FDataTableRowHandle EmitterBulletDefinition; + // + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Emitter") + // float EmitterInitialWaitTime{0}; + // + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Emitter") + // float EmitterMinShootInterval{0}; + // + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Emitter") + // float EmitterMaxShootInterval{0}; +}; + + +/** + * Parameters for spawning bullets. + * 子弹生成参数。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_BulletSpawnParameters +{ + GENERATED_BODY() + + /** + * The owner of the bullet. + * 子弹的拥有者。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + TObjectPtr Owner{nullptr}; + + /** + * Handle to the bullet definition. + * 子弹定义的句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS", meta=(RowType="/Script/GenericCombatSystem.GCS_BulletDefinition")) + FDataTableRowHandle DefinitionHandle; + + /** + * Transform for spawning the bullet. + * 子弹生成时的变换。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FTransform SpawnTransform{FTransform::Identity}; + + /** + * The associated attack request. + * 关联的攻击请求。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + TObjectPtr Request{nullptr}; + + /** + * Whether the bullet is locally predicted. + * 子弹是否为本地预测。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + bool bIsLocalPredicting{false}; + + /** + * IDs for locally predicted bullets. + * 本地预测子弹的ID。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS", meta=(DisplayName="Local Predicting Bullet Ids")) + TArray OverrideBulletIds; + + /** + * ID of the parent bullet (for bullet chains). + * 父子弹的ID(用于子弹链)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGuid ParentId; + + /** + * Returns a debug string representation. + * 返回调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString ToDebugString() const; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSubsystem.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSubsystem.h new file mode 100644 index 0000000..4f1eefc --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSubsystem.h @@ -0,0 +1,101 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_BulletStructLibrary.h" +#include "Subsystems/WorldSubsystem.h" +#include "GCS_BulletSubsystem.generated.h" + +class UGCS_AttackRequest_Bullet; +class AGCS_BulletInstance; + + +/** + * Subsystem for managing bullet spawning and lifecycle. + * 管理子弹生成和生命周期的子系统。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_BulletSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + static UGCS_BulletSubsystem* Get(const UWorld* World); + + /** + * Spawns bullets based on the provided parameters. + * 根据提供的参数生成子弹。 + * @param SpawnParameters The spawn parameters. 生成参数。 + * @return The spawned bullet instances. 生成的子弹实例。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet") + virtual TArray SpawnBullets(const FGCS_BulletSpawnParameters& SpawnParameters); + + /** + * Gets the IDs of the provided bullet instances. + * 获取提供的子弹实例的ID。 + * @param Instances The bullet instances. 子弹实例。 + * @return The IDs of the bullets. 子弹的ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Bullet") + virtual TArray GetIdsFromBullets(TArray Instances); + + /** + * Gets or creates bullet instances based on parameters and definition. + * 根据参数和定义获取或创建子弹实例。 + * @param SpawnParameters The spawn parameters. 生成参数。 + * @param Definition The bullet definition. 子弹定义。 + * @return The bullet instances. 子弹实例。 + */ + TArray GetOrCreateBulletInstances(const FGCS_BulletSpawnParameters& SpawnParameters, const FGCS_BulletDefinition& Definition); + + /** + * Retrieves a bullet instance from the pool. + * 从池中获取子弹实例。 + * @param BulletClass The class of the bullet. 子弹的类。 + * @return The bullet instance. 子弹实例。 + */ + AGCS_BulletInstance* TakeBulletFromPool(TSubclassOf BulletClass); + + /** + * Destroys a bullet by its ID. + * 根据ID销毁子弹。 + * @param BulletId The bullet ID. 子弹ID。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Bullet") + virtual void DestroyBullet(FGuid BulletId); + + /** + * Creates a single bullet instance. + * 创建单个子弹实例。 + * @param SpawnParameters The spawn parameters. 生成参数。 + * @param Definition The bullet definition. 子弹定义。 + * @return The created bullet instance. 创建的子弹实例。 + */ + AGCS_BulletInstance* CreateBulletInstance(const FGCS_BulletSpawnParameters& SpawnParameters, const FGCS_BulletDefinition& Definition); + + /** + * Loads a bullet definition from a handle. + * 从句柄加载子弹定义。 + * @param Handle The bullet definition handle. 子弹定义句柄。 + * @param OutDefinition The loaded definition (output). 加载的定义(输出)。 + * @return True if the definition was loaded successfully. 如果定义加载成功返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS|Bullet", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool LoadBulletDefinition(const FDataTableRowHandle& Handle, FGCS_BulletDefinition& OutDefinition); + + /** + * Active bullet instances mapped by their IDs. + * 按ID映射的激活子弹实例。 + */ + UPROPERTY() + TMap> BulletInstances; + + /** + * Pool of bullet instances for reuse. + * 用于重用的子弹实例池。 + */ + UPROPERTY() + TArray> BulletPools; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSystemComponent.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSystemComponent.h new file mode 100644 index 0000000..82bb478 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_BulletSystemComponent.h @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_BulletStructLibrary.h" +#include "Components/ActorComponent.h" +#include "GCS_BulletSystemComponent.generated.h" + + +/** + * @note WIP + */ +UCLASS(Abstract, ClassGroup=(GCS), meta=(BlueprintSpawnableComponent)) +class GENERICCOMBATSYSTEM_API UGCS_BulletSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UGCS_BulletSystemComponent(); + +protected: + // Called when the game starts + virtual void BeginPlay() override; + +public: + /** + * Gets the bullet system component from an actor. + * 从Actor获取子弹系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The bullet system component. 子弹系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|BulletSystem", Meta = (DefaultToSelf="Actor")) + static UGCS_BulletSystemComponent* GetBulletSystemComponent(const AActor* Actor); + + /** + * Finds the bullet system component on an actor. + * 在Actor上查找子弹系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param Component The found component (output). 找到的组件(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|BulletSystem", Meta = (DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindBulletSystemComponent(const AActor* Actor, UGCS_BulletSystemComponent*& Component); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|BulletSystem") + void SpawnBullet(const FGCS_BulletSpawnParameters& SpawnParameters); + + // virtual TArray SpawnBulletInternal(const FGCS_BulletSpawnParameters& SpawnParameters); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_SphereBulletInstance.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_SphereBulletInstance.h new file mode 100644 index 0000000..d15029a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Bullet/GCS_SphereBulletInstance.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_BulletInstance.h" +#include "GCS_SphereBulletInstance.generated.h" + +class USphereComponent; + +/** + * Bullet instance with a spherical collision shape. + * 具有球形碰撞形状的子弹实例。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICCOMBATSYSTEM_API AGCS_SphereBulletInstance : public AGCS_BulletInstance +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + AGCS_SphereBulletInstance(); + + /** + * Gets the bullet's shape component. + * 获取子弹的形状组件。 + * @return The sphere component. 球形组件。 + */ + virtual UShapeComponent* GetBulletShape_Implementation() const override; + +protected: + /** + * Called when the game starts or when spawned. + * 游戏开始或生成时调用。 + */ + virtual void BeginPlay() override; + + /** + * The sphere component for collision detection. + * 用于碰撞检测的球形组件。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS") + TObjectPtr Sphere; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void Tick(float DeltaTime) override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/DEPRECATED_GCS_CollisionTraceInstance.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/DEPRECATED_GCS_CollisionTraceInstance.h new file mode 100644 index 0000000..3aca782 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/DEPRECATED_GCS_CollisionTraceInstance.h @@ -0,0 +1,205 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GCS_TraceDelegates.h" +#include "GCS_ActorOwnedObject.h" +#include "GCS_TraceSystemComponent.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "DEPRECATED_GCS_CollisionTraceInstance.generated.h" + +class UPrimitiveComponent; +class UGCS_AttackRequest_Base; +class UTargetingPreset; +class UGCS_AttackRequest_Melee; +class UGCS_TraceSystemComponent; + +/** + * Object for managing collision trace instances. + * 管理碰撞检测实例的对象。 + */ +UCLASS(Blueprintable, AutoExpandCategories = ("GCS"), Deprecated, meta=(DeprecationMessage="CollisionTraceInstance is nolonger required since GCS 1.5!")) +class GENERICCOMBATSYSTEM_API UDEPRECATED_GCS_CollisionTraceInstance : public UGCS_ActorOwnedObject +{ + GENERATED_BODY() + + friend UGCS_TraceSystemComponent; + +public: + /** + * Gameplay tag for the trace instance. + * 碰撞检测实例的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS|Trace Settings", meta=(ExposeOnSpawn)) + FGameplayTag TraceGameplayTag; + + /** + * The primitive component used for tracing. + * 用于追踪的原始组件。 + */ + UPROPERTY(BlueprintReadWrite, Category = "GCS|Trace Settings", meta=(ExposeOnSpawn)) + TObjectPtr TracePrimitiveComponent; + + /** + * Socket names on the primitive component for tracing. + * 原始组件上用于追踪的插槽名称。 + */ + UPROPERTY(BlueprintReadWrite, Category = "GCS|Trace Settings", meta=(ExposeOnSpawn)) + TArray TracePrimitiveComponentSocketNames; + + /** + * Targeting preset for fetching target actors. + * 用于获取目标Actor的目标预设。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS|Trace Settings", meta=(ExposeOnSpawn)) + TObjectPtr TargetingPreset; + + /** + * The actor that created this trace instance. + * 创建此碰撞检测实例的Actor。 + */ + UPROPERTY(BlueprintReadOnly, Category = "GCS|Trace Settings") + TObjectPtr TraceOwner = nullptr; + + /** + * Associated information for this trace. + * 此碰撞检测的关联信息。 + */ + UPROPERTY(VisibleAnywhere, Transient, Category = "GCS|Trace State") + FGameplayEventData TraceInformation; + + /** + * Delegate for trace hit events. + * 碰撞检测命中事件的委托。 + */ + UPROPERTY(BlueprintAssignable, BlueprintCallable) + FGCS_OnTraceHitSignature OnHit; + + /** + * Delegate for trace state change events. + * 碰撞检测状态更改事件的委托。 + */ + UPROPERTY(BlueprintAssignable, BlueprintCallable) + FGCS_OnTraceStateChangedSignature OnTraceStateChangedEvent; + + /** + * The active duration of the trace instance. + * 碰撞检测实例的激活时间。 + */ + UPROPERTY(BlueprintReadOnly, Category="GCS|Trace State") + float ActiveTime{0.0f}; + + /** + * Broadcasts a hit event. + * 广播命中事件。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Trace") + void BroadcastHit(const FHitResult& HitResult); + + /** + * Broadcasts a state change event. + * 广播状态更改事件。 + * @param bNewState The new state. 新状态。 + */ + void BroadcastStateChanged(bool bNewState); + +protected: + /** + * Initializes the trace instance. + * 初始化碰撞检测实例。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GCS|Trace", meta=(BlueprintProtected)) + void OnTraceBeginPlay(); + virtual void OnTraceBeginPlay_Implementation(); + + /** + * Cleans up the trace instance. + * 清理碰撞检测实例。 + * @note The instance is returned to the cache pool instead of being destroyed. + * @注意 实例被返回到缓存池而不是销毁。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GCS|Trace", meta=(BlueprintProtected)) + void OnTraceEndPlay(); + virtual void OnTraceEndPlay_Implementation(); + + /** + * Handles trace ticking. + * 处理碰撞检测的tick。 + * @param DeltaSeconds Time since last frame. 上一帧以来的时间。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GCS|Trace", meta=(BlueprintProtected)) + void OnTraceTick(float DeltaSeconds); + virtual void OnTraceTick_Implementation(float DeltaSeconds); + + /** + * Handles trace state changes. + * 处理碰撞检测状态更改。 + * @param bNewState The new state. 新状态。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GCS|Trace", meta=(BlueprintProtected)) + void OnTraceStateChanged(bool bNewState); + virtual void OnTraceStateChanged_Implementation(bool bNewState); + + /** + * Actors hit during the active duration. + * 激活期间命中的Actor。 + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category="GCS|Trace State", AdvancedDisplay) + TArray> HitActors; + +public: + /** + * Sets the trace mesh information. + * 设置碰撞检测网格信息。 + * @param NewPrimitiveComponent The new primitive component. 新原始组件。 + * @param PrimitiveComponentSocketNames The socket names. 插槽名称。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Trace") + void SetTraceMeshInfo(UPrimitiveComponent* NewPrimitiveComponent, TArray PrimitiveComponentSocketNames); + + /** + * Checks if an actor can be hit. + * 检查是否可以命中Actor。 + * @param ActorToCheck The actor to check. 要检查的Actor。 + * @return True if the actor can be hit. 如果可以命中返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GCS|Trace") + bool CanHitActor(const AActor* ActorToCheck) const; + bool CanHitActor_Implementation(const AActor* ActorToCheck) const; + + /** + * Gets the source actor for the trace. + * 获取碰撞检测的源Actor。 + * @return The source actor (e.g., weapon, bullet, or trace owner). 源Actor(例如武器、子弹或TraceOwner)。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Trace") + AActor* GetTraceSourceActor() const; + + /** + * Toggles the trace state. + * 切换碰撞检测状态。 + * @param bNewState The new state (active traces tick and attempt to hit). 新状态(激活的追踪会tick并尝试命中)。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Trace") + void ToggleTraceState(bool bNewState); + + /** + * Indicates if the trace is active. + * 表示碰撞检测是否激活。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GCS|Trace") + bool bTraceActive{false}; + +protected: + /** + * Handles trace hit events. + * 处理碰撞检测命中事件。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Trace", meta=(BlueprintProtected)) + void OnTraceHit(const FHitResult& HitResult); + virtual void OnTraceHit_Implementation(const FHitResult& HitResult); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_AsyncAction_CollisionTrace.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_AsyncAction_CollisionTrace.h new file mode 100644 index 0000000..b2c4a6e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_AsyncAction_CollisionTrace.h @@ -0,0 +1,115 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_TraceStructLibrary.h" +#include "Engine/CancellableAsyncAction.h" +#include "GCS_AsyncAction_CollisionTrace.generated.h" + +class UGCS_TraceSystemComponent; + +/** + * Async action for setting up and listening to collision trace hits. + * 设置并监听碰撞检测命中的异步动作。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_AsyncAction_CollisionTrace : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Creates and activates trace instances from definitions and listens for hits. + * 从定义创建并激活碰撞检测实例并监听命中。 + * @param TraceSystem The collision trace system component. 碰撞检测系统组件。 + * @param TraceDefinitions The traces definitions will be created and added to collision trace system. 要新建的碰撞实例的定义。 + * @param PrimitiveComponent The primitive component for tracing. 用于追踪的原始组件。 + * @param OptionalSourceObject The optional source object. 可选的源对象. + * @return The async action instance. 异步动作实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) + static UGCS_AsyncAction_CollisionTrace* SetupAndListenForCollisionTraceHit(UGCS_TraceSystemComponent* TraceSystem, const TArray& TraceDefinitions, + UPrimitiveComponent* PrimitiveComponent, UObject* OptionalSourceObject = nullptr); + + /** + * Activates the async action. + * 激活异步动作。 + */ + virtual void Activate() override; + + /** + * Cancels the async action. + * 取消异步动作。 + */ + virtual void Cancel() override; + + /** + * Delegate for collision trace hit events. + * 碰撞检测命中事件的委托。 + */ + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FCollisionTraceSignature, const FGCS_TraceHandle, Handle, const FHitResult&, HitResult); + + /** + * Called before trace instances are activated. + * 在激活碰撞检测实例前调用。 + */ + /** + * Called before trace instances are activated. + * 在激活碰撞检测实例前调用。 + */ + UPROPERTY(BlueprintAssignable) + FCollisionTraceSignature BeforeActive; + + /** + * Fired when a trace instance hits something. + * 当碰撞检测实例命中某物时触发。 + */ + UPROPERTY(BlueprintAssignable) + FCollisionTraceSignature OnHit; + +protected: + /** + * Handles trace instance hit events. + * 处理碰撞检测实例命中事件。 + * @param TraceHandle The trace instance. 碰撞检测实例。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION() + void TraceHitCallback(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult); + + /** + * The collision system component. + * 碰撞系统组件。 + */ + UPROPERTY() + TWeakObjectPtr TraceSystem; + + /** + * The primitive component for tracing. + * 用于追踪的原始组件。 + */ + UPROPERTY() + TWeakObjectPtr SourceComponent; + + /** + * The optional source object. + * 可选的源对象。 + */ + UPROPERTY() + TWeakObjectPtr SourceObject; + + /** + * The trace definitions to create. + * 要创建的碰撞检测定义。 + */ + UPROPERTY() + TArray TraceDefinitions; + + /** + * The monitored trace instances. + * 监听的碰撞检测实例。 + */ + UPROPERTY() + TArray TraceHandles; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceDelegates.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceDelegates.h new file mode 100644 index 0000000..45e968d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceDelegates.h @@ -0,0 +1,24 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_TraceStructLibrary.h" +#include "UObject/Object.h" +#include "GCS_TraceDelegates.generated.h" + +/** + * Delegate for trace instance hit events. + * 碰撞检测实例命中事件的委托。 + */ +UDELEGATE() +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGCS_OnTraceHitSignature, const FGCS_TraceHandle&, TraceHandle, const FHitResult&, HitResult); + +UDELEGATE() +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGCS_OnTraceStateChangedSignature, const FGCS_TraceHandle&, TraceHandle, bool, bNewState); + +UDELEGATE() +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_OnTraceStartedSignature, const FGCS_TraceHandle&, TraceHandle); + +UDELEGATE() +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_OnTraceStoppedSignature, const FGCS_TraceHandle&, TraceHandle); diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceEnumLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceEnumLibrary.h new file mode 100644 index 0000000..6b2f694 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceEnumLibrary.h @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GCS_TraceEnumLibrary.generated.h" + + +UENUM() +enum class EGCS_CollisionShapeType : uint8 +{ + Sphere, + Box, + Capsule +}; + +UENUM() +enum class EGCS_TraceSweepType : uint8 +{ + ByChannel, + ByObject, + ByProfile +}; + +UENUM() +enum class EGCS_TraceTickType : uint8 +{ + Default, + FixedFrameRate, + DistanceBased +}; + +UENUM() +enum class EGCS_TraceExecutionState : uint8 +{ + InProgress, + Stopped, + PendingStop +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceFunctionLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceFunctionLibrary.h new file mode 100644 index 0000000..2e83bd2 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceFunctionLibrary.h @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_TraceStructLibrary.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GCS_TraceFunctionLibrary.generated.h" + +/** + * Blueprint function library for trace system utilities. + * 碰撞检测系统工具的蓝图函数库。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_TraceFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Filter definitions with trace tag matches TagToMatch. + * 筛选与指定标签匹配的碰撞检测定义。 + * @param Definitions The definitions to filter. 要筛选的定义。 + * @param TagToMatch The tag to check. 要检查的标签。 + * @return Matching definitions. 匹配的定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem|Utilities", Meta = (DefaultToSelf="Actor")) + static TArray FilterTraceDefinitionsByTag(const TArray& Definitions, const FGameplayTag& TagToMatch); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceStructLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceStructLibrary.h new file mode 100644 index 0000000..2810c5b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceStructLibrary.h @@ -0,0 +1,469 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "CollisionShape.h" +#include "CollisionQueryParams.h" +#include "Collision/GCS_TraceEnumLibrary.h" +#include "Engine/DataTable.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/Object.h" +#include "GCS_TraceStructLibrary.generated.h" + +class UTargetingPreset; +class UGCS_TraceSystemComponent; + +USTRUCT(BlueprintType) +struct FGCS_TraceSweepSetting +{ + GENERATED_BODY() + + /** + * The type of sweep to perform for collision detection. + * 执行碰撞检测的扫描类型。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GCS") + EGCS_TraceSweepType SweepType = EGCS_TraceSweepType::ByChannel; + + /** + * The collision channel to use for channel-based sweeping. + * 用于基于通道扫描的碰撞通道。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GCS", + meta = (EditCondition = "SweepType == EGCS_TraceSweepType::ByChannel", EditConditionHides)) + TEnumAsByte TraceChannel = ECC_Visibility; + + /** + * The object types to use for object-based sweeping. + * 用于基于对象扫描的对象类型。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GCS", + meta = (EditCondition = "SweepType == EGCS_TraceSweepType::ByObject", EditConditionHides)) + TArray> ObjectTypes; + + /** + * The collision profile name to use for profile-based sweeping. + * 用于基于配置扫描的碰撞配置名称。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GCS", + meta = (EditCondition = "SweepType == EGCS_TraceSweepType::ByProfile", EditConditionHides)) + FName ProfileName = NAME_None; + + /** + * Whether to trace against complex collision. + * 是否对复杂碰撞进行追踪。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GCS") + bool bTraceComplex = false; +}; + +/** + * Build collision shape by static parameters + * 通过静态参数构建碰撞形状。 + * 在SourceComponent空间内的静态形状。 + */ +USTRUCT(meta = (Hidden, DisplayName = "Collision Shape")) +struct GENERICCOMBATSYSTEM_API FGCS_CollisionShape +{ + GENERATED_BODY() + + virtual ~FGCS_CollisionShape() = default; + + virtual bool InitializeShape(const UPrimitiveComponent* SourceComponent); + virtual FTransform GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const; + virtual FCollisionShape GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const; +}; + + +/** + * Build collision shape by static parameters + * 通过静态参数构建碰撞形状。 + * 在SourceComponent空间内的静态形状。 + */ +USTRUCT(meta = (DisplayName = "Collision Shape (Static)")) +struct GENERICCOMBATSYSTEM_API FGCS_CollisionShape_Static : public FGCS_CollisionShape +{ + GENERATED_BODY() + + /** + * The type of collision shape to use. + * 要使用的碰撞形状类型。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + EGCS_CollisionShapeType ShapeType = EGCS_CollisionShapeType::Sphere; + + /** + * The orientation/rotation of the collision shape. + * 碰撞形状的方向/旋转。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", + meta = (EditCondition = "ShapeType != EGCS_CollisionShapeType::Sphere", EditConditionHides)) + FRotator Orientation = FRotator::ZeroRotator; + + /** + * The position offset of the collision shape. + * 碰撞形状的位置偏移。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + FVector Offset = FVector::ZeroVector; + + /** + * The radius of the sphere or capsule. + * 球体或胶囊体的半径。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", + meta = (EditCondition = "ShapeType != EGCS_CollisionShapeType::Box", EditConditionHides)) + float Radius = 10.f; + + /** + * The half height of the capsule. + * 胶囊体的半高。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", + meta = (EditCondition = "ShapeType == EGCS_CollisionShapeType::Capsule", EditConditionHides)) + float HalfHeight = 10.f; + + /** + * The half size of the box. + * 盒子的半尺寸。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", + meta = (EditCondition = "ShapeType == EGCS_CollisionShapeType::Box", EditConditionHides)) + FVector HalfSize = FVector(10.f, 10.f, 10.f); + + virtual bool InitializeShape(const UPrimitiveComponent* SourceComponent) override; + virtual FTransform GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const override; + virtual FCollisionShape GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const override; +}; + +/** + * Build dynamic collision shape by binding to shape component(Box/Sphere/Capsule). + * 通过绑定Shape组件(Box/Sphere/Capsule)构建动态碰撞形状。 + * 在SourceComponent空间内的,与SourceComponent进行匹配的形状。 + */ +USTRUCT(meta = (DisplayName = "Collision Shape (Shape Based)")) +struct GENERICCOMBATSYSTEM_API FGCS_CollisionShape_ShapeBased : public FGCS_CollisionShape +{ + GENERATED_BODY() + + /** + * The type of collision shape to use. + * 要使用的碰撞形状类型。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + EGCS_CollisionShapeType ShapeType = EGCS_CollisionShapeType::Sphere; + + /** + * The orientation/rotation of the collision shape. + * 碰撞形状的方向/旋转。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", + meta = (EditCondition = "ShapeType != EGCS_CollisionShapeType::Sphere", EditConditionHides)) + FRotator Orientation = FRotator::ZeroRotator; + + UPROPERTY(EditAnywhere, Category = "GCS") + FVector Offset = FVector::ZeroVector; + + UPROPERTY() + float Radius = 10.f; + + UPROPERTY() + float HalfHeight = 10.f; + + UPROPERTY() + FVector HalfSize = FVector(10.f, 10.f, 10.f); + + virtual bool InitializeShape(const UPrimitiveComponent* SourceComponent) override; + virtual FTransform GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const override; + virtual FCollisionShape GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const override; +}; + +/** + * 在SourceComponent空间内,跟随SourceComponent的某Socket/Bone运动的形状。 + */ +USTRUCT(meta = (DisplayName = "Collision Shape (Attached)")) +struct GENERICCOMBATSYSTEM_API FGCS_CollisionShape_Attached : public FGCS_CollisionShape_Static +{ + GENERATED_BODY() + + /** + * Name of the socket or bone the shape will be attached to. + * 形状将附加到的插槽或骨骼的名称。 + */ + UPROPERTY(EditAnywhere, Category="GCS") + FName SocketOrBoneName; + + virtual bool InitializeShape(const UPrimitiveComponent* SourceComponent) override; + virtual FTransform GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const override; +}; + +/** + * Build dynamic capsule collision shape by mesh's sockets. + * 通过网格的指定Sockets构建动态碰撞形状。 + */ +USTRUCT(meta = (DisplayName = "Collision Shape (Socket Based)")) +struct GENERICCOMBATSYSTEM_API FGCS_CollisionShape_SocketBased : public FGCS_CollisionShape +{ + GENERATED_BODY() + + /** + * The name of the starting socket on the mesh. + * 网格上起始插槽的名称。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + FName MeshSocketStart = TEXT("TrailStart"); + + /** + * The name of the ending socket on the mesh. + * 网格上结束插槽的名称。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + FName MeshSocketEnd = TEXT("TrailEnd"); + + /** + * Additional length offset for the socket-based shape. + * 基于插槽的形状的额外长度偏移。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + float MeshSocketLengthOffset = 0; + + /** + * The radius of the capsule. + * 胶囊体的半径。 + */ + UPROPERTY(EditAnywhere, Category = "GCS") + float Radius = 10.f; + + /** + * The orientation/rotation of the collision shape. + * 碰撞形状的方向/旋转。 + */ + UPROPERTY() + FRotator Orientation = FRotator::ZeroRotator; + + /** + * The position offset of the collision shape. + * 碰撞形状的位置偏移。 + */ + UPROPERTY() + FVector Offset = FVector::ZeroVector; + + /** + * The half height of the capsule. + * 胶囊体的半高。 + */ + UPROPERTY() + float HalfHeight = 10.f; + + virtual FTransform GetTransform(const UPrimitiveComponent* SourceComponent, const float& Time) const override; + virtual bool InitializeShape(const UPrimitiveComponent* SourceComponent) override; + virtual FCollisionShape GetDynamicCollisionShape(const UPrimitiveComponent* SourceComponent, const float& Time) const override; +}; + +/** + * Structure for defining collision trace instances. + * 定义碰撞检测实例的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_TraceDefinition : public FTableRowBase +{ + GENERATED_BODY() + + FGCS_TraceDefinition(); + + /** + * Tag used as Trace identifier. + * 此Trace的Tag标识。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings", meta=(Categories="GGF.Combat.Trace")) + FGameplayTag TraceTag; + + /** + * Defines the shape used for detecting hit results. + * 定义用于获取命中结果的形状。 + */ + UPROPERTY(EditAnywhere, Category = "Trace Settings", meta=(ExcludeBaseStruct, BaseStruct = "/Script/GenericCombatSystem.GCS_CollisionShape")) + FInstancedStruct CollisionShape{FInstancedStruct::Make(FGCS_CollisionShape_Static())}; + + /** + * Settings for the sweep operation. + * 扫描操作的设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings") + FGCS_TraceSweepSetting SweepSetting; + + /** + * The type of tick policy to use for this trace. + * 此Trace使用的tick策略类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings") + EGCS_TraceTickType TraceTickType{EGCS_TraceTickType::FixedFrameRate}; + + /** + * The fixed frame rate for ticking when using FixedFrameRate policy. + * 使用固定帧率策略时的固定帧率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings", + meta = (EditCondition = "TraceTickType == EGCS_TraceTickType::FixedFrameRate", EditConditionHides)) + int32 FixedTickFrameRate = 30; + + /** + * The distance threshold for ticking when using DistanceBased policy. + * 使用基于距离策略时的距离阈值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings", + meta = (EditCondition = "TraceTickType == EGCS_TraceTickType::DistanceBased", EditConditionHides)) + int32 DistanceTickThreshold = 30; + + /** + * The angle threshold for ticking when using DistanceBased policy (in degrees). + * 使用基于距离策略时的角度阈值(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trace Settings", + meta = (EditCondition = "TraceTickType == EGCS_TraceTickType::DistanceBased", EditConditionHides)) + float AngleTickThreshold = 15.0f; // Degrees + + bool IsValidDefinition() const; + + FString ToString() const; +}; + +/** + * Simple struct used for trace query. + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_TraceHandle +{ + GENERATED_BODY() + + /** + * The gameplay tag identifying this trace. + * 标识此Trace的游戏标签。 + */ + UPROPERTY(BlueprintReadOnly, Category = "GCS") + FGameplayTag TraceTag; + + /** + * The unique GUID for this trace instance. + * 此Trace实例的唯一GUID。 + */ + UPROPERTY(BlueprintReadOnly, Category = "GCS") + FGuid Guid; + + /** + * The source object that created this trace. + * 创建此Trace的源对象。 + */ + UPROPERTY(BlueprintReadOnly, Category = "GCS") + TWeakObjectPtr SourceObject; + + bool IsValidHandle() const; + + // Equality operator + friend bool operator==(const FGCS_TraceHandle& A, const FGCS_TraceHandle& B) + { + return A.TraceTag == B.TraceTag && + A.Guid == B.Guid && + A.SourceObject == B.SourceObject; + } + + // Inequality operator + friend bool operator!=(const FGCS_TraceHandle& A, const FGCS_TraceHandle& B) + { + return !(A == B); + } + + // Hash function for TMap/TSet + friend uint32 GetTypeHash(const FGCS_TraceHandle& Handle) + { + uint32 Hash = GetTypeHash(Handle.TraceTag); + Hash = HashCombine(Hash, GetTypeHash(Handle.Guid)); + Hash = HashCombine(Hash, GetTypeHash(Handle.SourceObject)); + return Hash; + } + + FString ToDebugString() const + { + FString TagName = TraceTag.ToString(); + if (TraceTag.IsValid()) + { + TArray TagNames; + TraceTag.ToString().ParseIntoArray(TagNames,TEXT(".")); + if (TagNames.Num() > 0) + { + TagName = TagNames.Last(); + } + } + + return FString::Printf(TEXT("%s;%s"), *TagName, SourceObject.IsValid() ? *GetNameSafe(SourceObject.Get()) : TEXT("None")); + } +}; + +// 补帧数据. +struct FGCS_TraceSubTick +{ + FTransform StartTransform; + FTransform EndTransform; + FTransform AverageTransform; +}; + +USTRUCT(BlueprintType) +struct FGCS_TraceState +{ + GENERATED_BODY() + + void ChangeExecutionState(bool bNewTraceState, bool bStopImmediate = true); + void UpdatePreviousTransform(const FTransform& Transform); + + // Get the world space transform of this trace. + FTransform GetCurrentTransform() const; + + // Begin Runtime references + UPROPERTY() + UWorld* World{nullptr}; + UPROPERTY() + TObjectPtr SourceComponent{nullptr}; + UPROPERTY() + TObjectPtr OwningSystem{nullptr}; + // End Runtime references + + // Begin Static Data + FGCS_TraceSweepSetting SweepSetting; + // End Static Data + + // Begin runtime data + FGCS_TraceHandle Handle; + + bool IsPendingRemoval = false; + + // The modified dynamic shape. + FInstancedStruct Shape; + + TArray> TransformsOverTime; + + EGCS_TraceExecutionState ExecutionState = EGCS_TraceExecutionState::Stopped; + + FCollisionShape CollisionShapeOverTime; + TArray SubTicks; + + EGCS_TraceTickType TickPolicy; + float TickInterval = 1 / 30; + float AngleThreshold = 15.0f; // Degrees + bool bShouldTickThisFrame = false; + + float TimeSinceLastTick = 0; + // how long this state active? + + // + float TimeSinceActive = 0; + int32 TotalTickNumDuringExecution = 0; + TArray> HitActors; + + FCollisionQueryParams CollisionParams; + FCollisionResponseParams ResponseParams; + FCollisionObjectQueryParams ObjectQueryParams; + + // End runtime data +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSubsystem.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSubsystem.h new file mode 100644 index 0000000..3d8a92a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSubsystem.h @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_TraceStructLibrary.h" +#include "DrawDebugHelpers.h" +#include "WorldCollision.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Subsystems/WorldSubsystem.h" +#include "GCS_TraceSubsystem.generated.h" + +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_TraceSubsystem : public UTickableWorldSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + virtual void Tick(float DeltaTime) override; + virtual bool IsTickable() const override { return true; } + virtual TStatId GetStatId() const override { return TStatId(); } + + FCriticalSection CriticalSection; + bool IsValidStateIdx(int32 StateIdx) const; + FGCS_TraceState& GetTraceStateAt(int Index); + + int32 AddTraceState(); + void RemoveTraceState(int Idx, FGuid Guid); + +protected: + TArray TraceStates; + bool RemovalLock = false; + uint32 TickIdx = 0; + void RemoveTraceStateAt(int Idx, FGuid Guid); + + void PreTraceTick(const float DeltaTime); + void PostTraceTick(); + + // 计算所有用于碰撞检测的必须数据。 + virtual void PrepareSubTicks(const float DeltaTime); + virtual void PerformSubTicks(const float DeltaTime); + + static void PerformAsyncTrace(const FTransform& StartTransform, const FTransform& EndTransform, const FTransform& AverageTransform, UWorld* World, + const FGCS_TraceSweepSetting& TraceSettings, + const FCollisionShape& CollisionShape, const FCollisionQueryParams& CollisionParams, + const FCollisionResponseParams& CollisionResponseParams, + const FCollisionObjectQueryParams& ObjectQueryParams, const FTraceDelegate* InDelegate = nullptr); + + virtual void HandleTraceResults(const FTraceHandle& InTraceHandle, FTraceDatum& InTraceDatum, int32 TraceStateIdx, uint32 InTickIdx, float InShapeTime); + +#if ENABLE_DRAW_DEBUG + static void DrawDebug(const FVector& Start, const FVector& End, const FQuat& Rot, TArray Hits, const FCollisionShape& CollisionShape, const UWorld* World, + const EDrawDebugTrace::Type DrawDebugType, float DrawDebugTime, const FLinearColor& DrawDebugColor, const FLinearColor& DrawDebugHitColor); +#endif + + // 新增:用于批量移除的Pending列表,避免边遍历边修改 + TArray> PendingRemovals; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSystemComponent.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSystemComponent.h new file mode 100644 index 0000000..7a1b9c0 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Collision/GCS_TraceSystemComponent.h @@ -0,0 +1,312 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameplayTagContainer.h" +#include "GCS_TraceDelegates.h" +#include "GCS_TraceStructLibrary.h" +#include "GCS_TraceSystemComponent.generated.h" + +class UGCS_TraceSubsystem; +class UPrimitiveComponent; + + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGCS_OnTraceSystemDestroyedSignature); + + +/** + * Component for managing collision trace instances. + * 管理碰撞检测实例的组件。 + */ +UCLASS(ClassGroup=(GCS), meta=(BlueprintSpawnableComponent, PrioritizeCategories="GCS"), Blueprintable, HideCategories=(Sockets, Navigation, Tags, ComponentTick, ComponentReplication, + Cooking, AssetUserData, Replication)) +class GENERICCOMBATSYSTEM_API UGCS_TraceSystemComponent : public UActorComponent +{ + GENERATED_BODY() + + friend UGCS_TraceSubsystem; + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_TraceSystemComponent(const FObjectInitializer& ObjectInitializer); + + /** + * Gets the collision trace system component from an actor. + * 从Actor获取碰撞检测系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The collision trace system component. 碰撞检测系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem", Meta = (DefaultToSelf="Actor")) + static UGCS_TraceSystemComponent* GetTraceSystemComponent(const AActor* Actor); + + /** + * Finds the collision trace system component on an actor. + * 在Actor上查找碰撞检测系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param Component The found component (output). 找到的组件(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem", Meta = (DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindTraceSystemComponent(const AActor* Actor, UGCS_TraceSystemComponent*& Component); + + /** + * Initializes the collision trace system. + * 初始化碰撞检测系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|TraceSystem", meta=(DisplayName="Initialize Trace System")) + void OnInitialize(); + virtual void OnInitialize_Implementation(); + + /** + * Deinitializes the collision trace system. + * 取消初始化碰撞检测系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|TraceSystem", meta=(DisplayName="Deinitialize Trace System")) + void OnDeinitialize(); + virtual void OnDeinitialize_Implementation(); + + /** + * Creates trace instances from definitions. + * 从定义创建碰撞检测实例。 + * @param Definitions The trace definitions. 碰撞检测定义。 + * @param SourceComponent The primitive component for tracing. 用于追踪的原始组件。 + * @param SourceObject The source object. 源对象。 + * @return The created trace instances. 创建的碰撞检测实例。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + TArray AddTracesByDefinitions(const TArray& Definitions, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + + /** + * Creates trace instances from data table row handles. + * 从数据表行句柄创建碰撞检测实例。 + * @param DefinitionHandles The data table row handles for trace definitions. 碰撞检测定义的数据表行句柄。 + * @param SourceComponent The primitive component for tracing. 用于追踪的原始组件。 + * @param SourceObject The source object. 源对象。 + * @return The created trace instances. 创建的碰撞检测实例。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + TArray AddTracesByDefinitionHandles(UPARAM(meta=(RowType="/Script/GenericCombatSystem.GCS_CollisionTraceDefinition")) + const TArray& DefinitionHandles, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + + /** + * Creates a single trace instance from a definition. + * 从定义创建单个碰撞检测实例。 + * @param Definition The trace definition. 碰撞检测定义。 + * @param SourceComponent The primitive component for tracing. 用于追踪的原始组件。 + * @param SourceObject The source object. 源对象。 + * @return The created trace instance. 创建的碰撞检测实例。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + FGCS_TraceHandle AddTraceByDefinition(const FGCS_TraceDefinition& Definition, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + + /** + * Starts traces by tags and source object. + * 通过标签和源对象启动碰撞检测。 + * @param TraceTags The gameplay tags to match. 要匹配的游戏标签。 + * @param SourceObject The source object. 源对象。 + * @return The started trace instances. 启动的碰撞检测实例。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + TArray StartTracesByTagsAndSource(const FGameplayTagContainer& TraceTags, const UObject* SourceObject); + + /** + * Starts multiple traces by their handles. + * 通过句柄启动多个碰撞检测。 + * @param TraceHandles The trace handles to start. 要启动的碰撞检测句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void StartTracesByHandles(const TArray& TraceHandles); + + /** + * Starts a single trace by its handle. + * 通过句柄启动单个碰撞检测。 + * @param TraceHandle The trace handle to start. 要启动的碰撞检测句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void StartTraceByHandle(const FGCS_TraceHandle& TraceHandle); + + /** + * Stops multiple traces by their handles. + * 通过句柄停止多个碰撞检测。 + * @param TraceHandles The trace handles to stop. 要停止的碰撞检测句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void StopTracesByHandles(const TArray& TraceHandles); + + /** + * Stops a single trace by its handle. + * 通过句柄停止单个碰撞检测。 + * @param TraceHandle The trace handle to stop. 要停止的碰撞检测句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void StopTraceByHandle(const FGCS_TraceHandle& TraceHandle); + + /** + * Removes a trace by its handle. + * 通过句柄移除碰撞检测。 + * @param TraceHandle The trace handle to remove. 要移除的碰撞检测句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void RemoveTraceByHandle(const FGCS_TraceHandle& TraceHandle); + + /** + * Clears all active and cached trace instances. + * 清除所有激活和缓存的碰撞检测实例。 + * @note OnTraceEndPlay event is not fired. OnTraceEndPlay事件不会触发。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|TraceSystem") + void RemoveAllTraces(); + + /** + * Gets all trace handles with the specified tag. + * 获取具有指定标签的所有碰撞检测句柄。 + * @param TraceToFind The gameplay tag to search for. 要搜索的游戏标签。 + * @return Array of trace handles with the specified tag. 具有指定标签的碰撞检测句柄数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + TArray GetTraceHandlesByTag(FGameplayTag TraceToFind) const; + + /** + * Gets all trace handles created by the specified source object. + * 获取由指定源对象创建的所有碰撞检测句柄。 + * @param SourceObject The source object to search for. 要搜索的源对象。 + * @return Array of trace handles from the specified source. 来自指定源的碰撞检测句柄数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + TArray GetTraceHandlesBySource(const UObject* SourceObject) const; + + /** + * Gets all trace handles with the specified tags and source object. + * 获取具有指定标签和源对象的所有碰撞检测句柄。 + * @param TraceTags The gameplay tags to match. 要匹配的游戏标签。 + * @param SourceObject The source object to match. 要匹配的源对象。 + * @return Array of trace handles matching the criteria. 匹配条件的碰撞检测句柄数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + TArray GetTraceHandlesByTagsAndSource(const FGameplayTagContainer& TraceTags, const UObject* SourceObject) const; + + /** + * Gets the source actor for the specified trace handle. + * 获取指定碰撞检测句柄的源Actor。 + * @param TraceHandle The trace handle to query. 要查询的碰撞检测句柄。 + * @return The source actor, or nullptr if not found. 源Actor,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + AActor* GetTraceSourceActor(const FGCS_TraceHandle& TraceHandle) const; + + /** + * Gets the source component for the specified trace handle. + * 获取指定碰撞检测句柄的源组件。 + * @param TraceHandle The trace handle to query. 要查询的碰撞检测句柄。 + * @return The source component, or nullptr if not found. 源组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + UPrimitiveComponent* GetTraceSourceComponent(const FGCS_TraceHandle& TraceHandle) const; + + /** + * Checks if the specified trace is currently active. + * 检查指定的碰撞检测当前是否激活。 + * @param TraceHandle The trace handle to check. 要检查的碰撞检测句柄。 + * @return True if the trace is active. 如果碰撞检测激活则返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|TraceSystem") + bool IsTraceActive(const FGCS_TraceHandle& TraceHandle) const; + + virtual TArray StartTraces(const FGameplayTagContainer& TraceTags, const UObject* SourceObject); + + virtual TArray AddTraces(const TArray& Definitions, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + virtual TArray AddTraces(const TArray& DefinitionHandles, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + virtual FGCS_TraceHandle AddTrace(const FGCS_TraceDefinition& TraceDefinition, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + virtual FGCS_TraceHandle AddTrace(const FDataTableRowHandle& TraceDefinitionHandle, UPrimitiveComponent* SourceComponent, UObject* SourceObject); + + virtual void StartTraces(const TArray& TraceHandles); + + virtual void StartTrace(const FGCS_TraceHandle& TraceHandle); + + virtual void StopTraces(const TArray& TraceHandles); + + virtual void StopTrace(const FGCS_TraceHandle& TraceHandle); + + virtual void RemoveTraces(const TArray& TraceHandles); + virtual void RemoveTrace(const FGCS_TraceHandle& TraceHandle); + + /** + * Event fired when a trace hits something. + * 当碰撞检测命中某物时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category = "GCS|TraceSystem") + FGCS_OnTraceHitSignature OnTraceHitEvent; + + /** + * Event fired when a trace state changes. + * 当碰撞检测状态改变时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category = "GCS|TraceSystem") + FGCS_OnTraceStateChangedSignature OnTraceStateChangedEvent; + + /** + * Event fired when a trace starts. + * 当碰撞检测开始时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category = "GCS|TraceSystem") + FGCS_OnTraceStartedSignature OnTraceStartedEvent; + + /** + * Event fired when a trace stops. + * 当碰撞检测停止时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category = "GCS|TraceSystem") + FGCS_OnTraceStoppedSignature OnTraceStoppedEvent; + + /** + * Event fired when the trace system is destroyed. + * 当碰撞检测系统被销毁时触发的事件。 + */ + FGCS_OnTraceSystemDestroyedSignature OnDestroyedEvent; + +protected: + virtual void OnTraceHitDetected(const FGCS_TraceHandle& TraceHandle, const TArray& HitResults, const float DeltaTime, const uint32 TickIdx); + + virtual void OnTraceStateChanged(const FGCS_TraceHandle& TraceHandle, bool bNewState); + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; + + /** + * Whether to auto-initialize on BeginPlay and deinitialize on EndPlay. + * 是否在BeginPlay时自动初始化,在EndPlay时自动取消初始化。 + */ + UPROPERTY(EditAnywhere, Category = "GCS|TraceSystem") + bool bAutoInitialize{true}; + + /** + * Default trace definitions created on BeginPlay. + * 在BeginPlay时创建的默认碰撞检测定义。 + * @note SourceComponent will be the actor's main mesh. 源组件会是Actor的主要mesh。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GCS|TraceSystem", meta=(TitleProperty="TraceTag")) + TArray TraceDefinitions; + + FCriticalSection TraceDoneScopeLock; + + TMap HandleToStateIdx; + TMultiMap TagToHandles; + bool bInitialized = false; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AbilityActionSetSettings.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AbilityActionSetSettings.h new file mode 100644 index 0000000..8351ca3 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AbilityActionSetSettings.h @@ -0,0 +1,49 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatStructLibrary.h" +#include "Engine/DataAsset.h" +#include "GCS_AbilityActionSetSettings.generated.h" + +class UGCS_LayeredMontageSelectionSet; + +/** + * Data asset for defining ability action sets. + * 定义能力动作集的数据资产。 + */ +UCLASS(BlueprintType, Const, meta=(DisplayName="GCS Ability Action Set")) +class GENERICCOMBATSYSTEM_API UGCS_AbilityActionSetSettings : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Selects the best ability actions based on tags. + * 根据标签选择最佳能力动作。 + * @param SourceTags Tags for the source. 来源标签。 + * @param TargetTags Tags for the target. 目标标签。 + * @param AbilityTags Tags for the ability. 能力标签。 + * @param Actions The matched ability actions (output). 匹配的能力动作(输出)。 + * @return True if selection is successful. 如果选择成功返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GCS", meta=(AutoCreateRefTerm="TargetTags")) + bool SelectBestAbilityActions(const FGameplayTagContainer& SourceTags, const FGameplayTagContainer& TargetTags, const FGameplayTagContainer& AbilityTags, TArray& Actions) const; + + /** + * Array of ability action sets. + * 能力动作集数组。 + */ + UPROPERTY(EditAnywhere, Category="GCS", meta=(TitleProperty="AbilityTag")) + TArray ActionSets; + +#if WITH_EDITORONLY_DATA + /** + * Called before saving in the editor. + * 编辑器中保存前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackDefinition.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackDefinition.h new file mode 100644 index 0000000..60c3f27 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackDefinition.h @@ -0,0 +1,142 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffect.h" +#include "Engine/DataTable.h" +#include "GameplayTagContainer.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GCS_AttackDefinition.generated.h" + + +/** + * Base struct allow you to extend the attack definition's fields using C++. + * 基础结构体,允许你通过C++拓展攻击定义的字段。 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICCOMBATSYSTEM_API FGCS_AttackDefinitionExtension +{ + GENERATED_BODY() +}; + + +/** + * Structure defining an attack's properties. + * 定义攻击属性的结构。 + */ +USTRUCT(BlueprintType, meta=(DisplayName="GCS Attack Definition")) +struct GENERICCOMBATSYSTEM_API FGCS_AttackDefinition : public FTableRowBase +{ + GENERATED_BODY() + + /** + * Tags describing the attack (e.g., Melee/Ranged, Slash/Strike). + * 描述攻击的标签(例如近战/远程、劈砍/打击)。 + * @note Added as dynamic AssetTags to the gameplay effect spec. + * @注意 作为动态AssetTags添加到游戏效果规格。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Common") + FGameplayTagContainer AttackTags; + + /** + * SetByCaller tag-to-float mappings for gameplay effect specs. + * 用于游戏效果规格的SetByCaller标签到浮点映射。 + * @note Usage is flexible (e.g., damage correction factors). + * @注意 使用灵活(例如伤害修正系数)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Common", meta=(ForceInlineRow)) + TMap SetByCallerMagnitudes; + + /** + * Gameplay effect to apply to the hit target. + * 应用于命中目标的游戏效果。 + * @note Modified during attack request processing. + * @注意 在攻击请求处理期间修改。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Gameplay Effects") + TSoftClassPtr TargetEffectClass; + + /** + * Level of the target gameplay effect. + * 目标游戏效果的等级。 + * @note If < 1, uses the ability's level. + * @注意 如果<1,使用能力的等级。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Gameplay Effects") + int32 TargetEffectClassLevel{1}; + + /** + * Effect container to apply to the target. + * 应用于目标的效果容器。 + * @note Used for instant targeting; ability level determines effect level. + * @注意 用于即时目标;能力等级决定效果等级。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Gameplay Effects", meta=(ForceInlineRow)) + FGGA_GameplayEffectContainer TargetEffectContainer; + + /** + * Gameplay cues to trigger on the target upon hit. + * 命中目标时触发的游戏反馈。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Gameplay Cues", meta=(Categories="GameplayCue")) + TArray TargetGameplayCues; + + /** + * Knockback distance applied to the target. + * 应用于目标的击退距离。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "HitReaction", meta=(Units="cm")) + float KnockbackDistance{100}; + + /** + * Multiplier for knockback effect. + * 击退效果的倍增器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "HitReaction", meta=(ClampMin=1)) + float KnockbackMultiplier{1}; + + /** + * Duration of animation stall on hit (disabled if <= 0). + * 命中时动画停滞的持续时间(<=0时禁用)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feedback", meta=(ClampMin=0, Units="s")) + float HitStallingDuration{0}; + + /** + * Play rate factor for hit animation. + * 命中动画的播放速率因子。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feedback", meta=(ClampMin=0.1, ClampMax=0.9)) + float HitPlayRateFactor{0.1}; + + /** + * Native Instanced struct for extending the attack definition. + * 实例化结构体用于扩充攻击定义的字段。 + * @attention For C++ users only. 仅针对C++用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + TInstancedStruct NativeExtension; + + /** + * Blueprint Instanced struct for extending the attack definition. + * 实例化结构体用于扩充攻击定义的字段。 + * @attention For blueprint users only. 仅针对蓝图用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + FInstancedStruct Extension; + + /** + * User-defined settings for the attack. + * 攻击的用户定义设置。 + */ + UE_DEPRECATED(1.5, "Using extension field to add custom fields!") + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Deprecated", meta=(ForceInlineRow, BaseStruct = "/Script/GenericCombatSystem.GCS_UserSetting")) + TMap UserSettings; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackRequest.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackRequest.h new file mode 100644 index 0000000..8681f83 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackRequest.h @@ -0,0 +1,224 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Bullet/GCS_BulletStructLibrary.h" +#include "GCS_AttackDefinition.h" +#include "UObject/Object.h" +#include "GCS_AttackRequest.generated.h" + +/** + * Base class for all attack request types. + * 所有攻击请求类型的基类。 + */ +UCLASS(Abstract, Blueprintable, BlueprintType, Const, DefaultToInstanced, EditInlineNew, meta=(DisplayName="GCS Attack Request")) +class UGCS_AttackRequest_Base : public UObject +{ + GENERATED_BODY() + +public: + /** + * Gets the attack definition handle. + * 获取攻击定义句柄。 + * @return The attack definition handle. 攻击定义句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GCS|Attack") + FDataTableRowHandle GetAttackDefinitionHandle() const; + + /** + * Gets the attack definition. + * 获取攻击定义。 + * @return The attack definition. 攻击定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS|Attack") + FGCS_AttackDefinition GetAttackDefinition() const; +}; + +/** + * Attack request for melee attacks. + * 近战攻击请求。 + */ +UCLASS(meta=(DisplayName="GCS Attack Request (Melee)")) +class GENERICCOMBATSYSTEM_API UGCS_AttackRequest_Melee : public UGCS_AttackRequest_Base +{ + GENERATED_BODY() + +public: + /** + * Gets the attack definition handle. + * 获取攻击定义句柄。 + * @return The attack definition handle. 攻击定义句柄。 + */ + virtual FDataTableRowHandle GetAttackDefinitionHandle_Implementation() const override; + + /** + * Tags for traces activated during the notify state. + * 在通知状态期间激活的追踪标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Attack") + FGameplayTagContainer TracesToControl; + +protected: + /** + * Handle to the attack definition. + * 攻击定义的句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Attack", meta=(RowType="/Script/GenericCombatSystem.GCS_AttackDefinition")) + FDataTableRowHandle AttackDefinitionHandle; +}; + +/** + * Enum for ability targeting source types. + * 能力目标来源类型的枚举。 + */ +UENUM(BlueprintType) +enum class EGCS_AbilityTargetingSourceType : uint8 +{ + /** + * From the player's camera towards camera focus. + * 从玩家相机朝向相机焦点。 + */ + CameraTowardsFocus, + + /** + * From the pawn's location/socket, in the pawn's orientation. + * 从Pawn的位置/插槽,沿Pawn的朝向。 + */ + PawnForward, + + /** + * From the pawn's location/socket, oriented towards camera focus. + * 从Pawn的位置/插槽,朝向相机焦点。 + */ + PawnTowardsFocus, + + /** + * From the weapon's location/socket, in the pawn's orientation. + * 从武器的位置/插槽,沿Pawn的朝向。 + */ + WeaponForward, + + /** + * From the weapon's location/socket, towards camera focus. + * 从武器的位置/插槽,朝向相机焦点。 + */ + WeaponTowardsFocus, + + /** + * Custom targeting, requires overriding GetTargetingTransform. + * 自定义目标,需重写GetTargetingTransform。 + */ + Custom +}; + +/** + * Attack request for firing bullets. + * 发射子弹的攻击请求。 + */ +UCLASS(Blueprintable, BlueprintType, Const, EditInlineNew, meta=(DisplayName="GCS Attack Request (Bullet)")) +class GENERICCOMBATSYSTEM_API UGCS_AttackRequest_Bullet : public UGCS_AttackRequest_Base +{ + GENERATED_BODY() + +public: + /** + * Gets the bullet definition. + * 获取子弹定义。 + * @return The bullet definition. 子弹定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Attack") + FGCS_BulletDefinition GetBulletDefinition() const; + + /** + * Gets the attack definition handle. + * 获取攻击定义句柄。 + * @return The attack definition handle. 攻击定义句柄。 + */ + virtual FDataTableRowHandle GetAttackDefinitionHandle_Implementation() const override; + + /** + * Gets the targeting transform for the attack. + * 获取攻击的目标变换。 + * @param SourcePawn The source pawn. 来源Pawn。 + * @param Source The targeting source type. 目标来源类型。 + * @return The targeting transform. 目标变换。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "GCS|Attack") + FTransform GetTargetingTransform(APawn* SourcePawn, EGCS_AbilityTargetingSourceType Source) const; + + /** + * Gets the weapon targeting source location. + * 获取武器目标来源位置。 + * @param SourcePawn The source pawn. 来源Pawn。 + * @return The weapon source location. 武器来源位置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "GCS|Attack") + FVector GetWeaponTargetingSourceLocation(APawn* SourcePawn) const; + + /** + * Gets the pawn targeting source location. + * 获取Pawn目标来源位置。 + * @param SourcePawn The source pawn. 来源Pawn。 + * @return The pawn source location. Pawn来源位置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "GCS|Attack") + FVector GetPawnTargetingSourceLocation(APawn* SourcePawn) const; + + /** + * Handle to the bullet definition. + * 子弹定义的句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Parameters, meta=(RowType="/Script/GenericCombatSystem.GCS_BulletDefinition")) + FDataTableRowHandle BulletDefinitionHandle; + + /** + * Type of targeting source for the attack. + * 攻击的目标来源类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Parameters) + EGCS_AbilityTargetingSourceType TargetingSourceType{EGCS_AbilityTargetingSourceType::PawnForward}; + + /** + * Tag name for looking up the source component. + * 用于查找来源组件的标签名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Parameters, meta=(EditCondition="TargetingSourceType != EGCS_AbilityTargetingSourceType::CameraTowardsFocus")) + FName SourceComponentLookupTagName{NAME_None}; + + /** + * Source socket name, falls back to source location if not found. + * 来源插槽名称,如果未找到则回退到来源位置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Parameters, meta=(EditCondition="TargetingSourceType != EGCS_AbilityTargetingSourceType::CameraTowardsFocus")) + FName SourceSocketName{NAME_None}; + + /** + * Weapon socket name, falls back to source location if not found. + * 武器插槽名称,如果未找到则回退到来源位置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Parameters, meta=(EditCondition="TargetingSourceType != EGCS_AbilityTargetingSourceType::CameraTowardsFocus")) + FName SourceWeaponSocketName{NAME_None}; + + /** + * Additional offset to the source location. + * 来源位置的附加偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Parameters) + FVector LocationOffset{FVector::Zero()}; + + /** + * Whether targeting is required for the attack. + * 攻击是否需要目标。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Parameters) + bool bRequireTargeting{false}; + + /** + * Targeting preset for the attack. + * 攻击的目标预设。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Parameters) + TObjectPtr TargetingPreset; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResult.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResult.h new file mode 100644 index 0000000..cba50ff --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResult.h @@ -0,0 +1,205 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectTypes.h" +#include "GCS_CombatStructLibrary.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Object.h" +#include "GCS_AttackResult.generated.h" + +class UGCS_AttackRequest_Melee; +class UGCS_CombatFlow; +class UGCS_CombatSystemComponent; + +/** + * Structure representing the result of a processed attack. + * 表示已处理攻击结果的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_AttackResult : public FFastArraySerializerItem +{ + GENERATED_BODY() + + + void PostReplicatedAdd(const struct FGCS_AttackResultContainer& InArray); + + + /** + * Deprecated. + * 已经弃用。 + */ + UE_DEPRECATED(1.5, "Use TaggedValues within FGCS_ContextPayload_Combat") + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GCS", NotReplicated, meta=(DeprecatedProperty, DeprecationMessage="Use TaggedValues within FGCS_ContextPayload_Combat!")) + TArray TaggedValues; + + /** + * Optional object related to the attack. + * 与攻击相关的可选对象。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + TObjectPtr OptionalObject; + + /** + * Context handle for the gameplay effect. + * 游戏效果的上下文句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayEffectContextHandle EffectContextHandle; + + /** + * Aggregated source tags for the attack. + * 攻击的聚合来源标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayTagContainer AggregatedSourceTags; + + /** + * Aggregated target tags for the attack. + * 攻击的聚合目标标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayTagContainer AggregatedTargetTags; + + /** + * Whether the attack result has been consumed. + * 攻击结果是否已被消耗。 + */ + UPROPERTY(NotReplicated) + bool bConsumed{false}; + + /** + * Indicates this attack result was found in existing processed array with same prediction key. + * 表示此攻击结果在已处理攻击结果列表中有相同的预测Key。 + */ + UPROPERTY(NotReplicated) + bool bWasPredicated{false}; + + /** + * Indicates this attack result was replicated via fast array serializer. + * 表示此攻击结果是经由fast array serializer同步而来。 + */ + UPROPERTY(NotReplicated) + bool bWasReplicated{false}; +}; + +/** + * Container for storing combat results with network serialization. + * 用于存储战斗结果的容器,支持网络序列化。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_AttackResultContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor. + * 默认构造函数。 + */ + FGCS_AttackResultContainer(); + + /** + * Constructor with combat flow and max size. + * 带有战斗流程和最大尺寸的构造函数。 + * @param InCombatFlow The combat flow instance. 战斗流程实例。 + * @param InMaxSize The maximum size of the container. 容器最大尺寸。 + */ + FGCS_AttackResultContainer(UGCS_CombatFlow* InCombatFlow, int32 InMaxSize); + + /** + * Constructor with combat system component and max size. + * 带有战斗系统组件和最大尺寸的构造函数。 + * @param InCombatSystemComponent The combat system component. 战斗系统组件。 + * @param InMaxSize The maximum size of the container. 容器最大尺寸。 + */ + FGCS_AttackResultContainer(UGCS_CombatSystemComponent* InCombatSystemComponent, int32 InMaxSize); + + /** + * Sets the owning combat system component. + * 设置所属战斗系统组件。 + * @param InCombatSystemComponent The combat system component. 战斗系统组件。 + */ + void SetOwningCombatSystem(UGCS_CombatSystemComponent* InCombatSystemComponent) { CombatSystemComponent = InCombatSystemComponent; } + + /** + * Sets the combat flow. + * 设置战斗流程。 + * @param InCombatFlow The combat flow instance. 战斗流程实例。 + */ + void SetCombatFlow(UGCS_CombatFlow* InCombatFlow) { CombatFlow = InCombatFlow; } + + /** + * Adds a new attack result entry to the container. + * 向容器添加新的攻击结果条目。 + * @param NewEntry The attack result to add. 要添加的攻击结果。 + */ + void AddEntry(FGCS_AttackResult& NewEntry); + + /** + * Handles post-replication addition of entries. + * 处理条目添加后的复制。 + * @param AddedIndices The indices of added entries. 添加的条目索引。 + * @param FinalSize The final size of the container. 容器最终尺寸。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Handles post-replication changes to entries. + * 处理条目更改后的复制。 + * @param ChangedIndices The indices of changed entries. 更改的条目索引。 + * @param FinalSize The final size of the container. 容器最终尺寸。 + */ + void PostReplicatedChange(const TArrayView& ChangedIndices, int32 FinalSize); + + /** + * Serializes the container for network replication. + * 为网络复制序列化容器。 + * @param DeltaParms The network serialization parameters. 网络序列化参数。 + * @return True if serialization is successful. 如果序列化成功返回true。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Results, DeltaParms, *this); + } + + bool HasPredictedResultWithPredictedKey(FPredictionKey PredictionKey) const; + +private: + /** + * Reference to the combat flow instance. + * 战斗流程实例的引用。 + */ + UPROPERTY() + TObjectPtr CombatFlow; + + /** + * Reference to the combat system component. + * 战斗系统组件的引用。 + */ + UPROPERTY() + TObjectPtr CombatSystemComponent; + + /** + * List of attack results. + * 攻击结果列表。 + */ + UPROPERTY() + TArray Results; + + /** + * Maximum size of the container. + * 容器最大尺寸。 + */ + UPROPERTY() + int32 MaxSize; +}; + +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true, + }; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResultProcessor.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResultProcessor.h new file mode 100644 index 0000000..61370c2 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_AttackResultProcessor.h @@ -0,0 +1,287 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_AttackResult.h" +#include "UObject/Object.h" +#include "GCS_AttackResultProcessor.generated.h" + + +UENUM() +enum class EGCS_AttackResultProcessorPolicy +{ + //execute when non-predicting cross all server and clients. + Default, + //execute in predicting client first,then server and other clients.same as default is not predicting. + LocalPredicted, + //execute only on server side. + ServerOnly +}; + +/** + * Base class for processing attack results. + * 处理攻击结果的基类。 + */ +UCLASS(EditInlineNew, DefaultToInstanced, BlueprintType, Blueprintable, Abstract, Const) +class GENERICCOMBATSYSTEM_API UGCS_AttackResultProcessor : public UObject +{ + GENERATED_BODY() + +public: + /** + * Processes an incoming attack result. + * 处理传入的攻击结果。 + * @param AttackResult The attack result to process. 要处理的攻击结果。 + */ + UFUNCTION(BlueprintCallable, Category="GCS") + virtual bool ProcessIncomingAttackResult(const FGCS_AttackResult& AttackResult); + + /** + * Gets the world context for the processor. + * 获取处理器的世界上下文。 + * @return The world context. 世界上下文。 + */ + virtual UWorld* GetWorld() const override; + + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GCS") + EGCS_AttackResultProcessorPolicy GetExecutePolicy() const; + +#if WITH_EDITOR + bool GetEditorEnableState() const { return bEditorDebugEnabled; }; + +#endif + +protected: + /** + * Indicate how this processor will be executed cross network. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + EGCS_AttackResultProcessorPolicy ExecutePolicy{EGCS_AttackResultProcessorPolicy::Default}; + + /** + * Handles the incoming attack result. + * 处理传入的攻击结果。 + * @param AttackResult The attack result to handle. 要处理的攻击结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS") + void HandleIncomingAttackResult(const FGCS_AttackResult& AttackResult) const; + virtual void HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const; + + /** + * Gets the owning actor. + * 获取所属演员。 + * @return The owning actor. 所属演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + AActor* GetOwningActor() const; + + /** + * Gets the owning ability system component. + * 获取所属能力系统组件。 + * @return The ability system component. 能力系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + UAbilitySystemComponent* GetOwningAbilitySystemComponent() const; + + /** + * Gets the editor-friendly name for the processor. + * 获取处理器的编辑器友好名称。 + * @return The editor-friendly name. 编辑器友好名称。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GCS") + FString GetEditorFriendlyName() const; + virtual FString GetEditorFriendlyName_Implementation() const; + +#if WITH_EDITORONLY_DATA + + /** + * Allowing toggle on/off this processor for debugging purpose. + * 允许你开关此处理器,用于调试。 + */ + UPROPERTY(EditAnywhere, Category="GCS") + bool bEditorDebugEnabled{true}; + + /** + * Editor-friendly name for the processor. + * 处理器的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden) + FString EditorFriendlyName; + + // UPROPERTY(EditAnywhere, Category="GCS") + // bool bPrintDebugString{false}; + + /** + * Called before saving in the editor. + * 编辑器中保存前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + +/** + * Attack result processor with tag requirements. + * 具有标签要求的攻击结果处理器。 + */ +UCLASS(Abstract) +class UGCS_AttackResultProcessor_WithTagRequirement : public UGCS_AttackResultProcessor +{ + GENERATED_BODY() + +public: + /** + * Processes an incoming attack result with tag requirements. + * 处理具有标签要求的传入攻击结果。 + * @param AttackResult The attack result to process. 要处理的攻击结果。 + */ + virtual bool ProcessIncomingAttackResult(const FGCS_AttackResult& AttackResult) override; + +protected: + /** + * Source tag query for filtering attack results. + * 用于过滤攻击结果的来源标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + FGameplayTagQuery SourceTagQuery; + + /** + * Target tag query for filtering attack results. + * 用于过滤攻击结果的目标标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + FGameplayTagQuery TargetTagQuery; + + /** + * Gets the description of the source tag query. + * 获取来源标签查询的描述。 + * @return The source tag query description. 来源标签查询描述。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + FString GetSourceTagQueryDesc() const; + + /** + * Gets the description of the target tag query. + * 获取目标标签查询的描述。 + * @return The target tag query description. 目标标签查询描述。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + FString GetTargetTagQueryDesc() const; +}; + +/** + * Processor for handling death-related attack results. + * 处理与死亡相关的攻击结果的处理器。 + */ +UCLASS() +class UGCS_AttackResultProcessor_Death : public UGCS_AttackResultProcessor +{ + GENERATED_BODY() + +public: + /** + * Handles death-related attack results. + * 处理与死亡相关的攻击结果。 + * @param AttackResult The attack result to handle. 要处理的攻击结果。 + */ + virtual void HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const override; +}; + +/** + * Processor for converting attack results to gameplay events. + * 将攻击结果转换为游戏事件的处理器。 + * @note Only executes for server pawn or local controller pawn. The dynamic tags added to effect context will be merged as Instigator Tags. + * @注意 仅对服务器Pawn或本地控制器Pawn执行。 添加到Effect Context的动态标签会被合并为Instigator Tags。 + */ +UCLASS() +class UGCS_AttackResultProcessor_GameplayEvent : public UGCS_AttackResultProcessor_WithTagRequirement +{ + GENERATED_BODY() + +protected: + /** + * Handles attack results by converting to gameplay events. + * 通过转换为游戏事件处理攻击结果。 + * @param AttackResult The attack result to handle. 要处理的攻击结果。 + */ + virtual void HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const override; + + /** + * Gets the editor-friendly name for the processor. + * 获取处理器的编辑器友好名称。 + * @return The editor-friendly name. 编辑器友好名称。 + */ + virtual FString GetEditorFriendlyName_Implementation() const override; + + /** + * Whether to send the event to the attacker. + * 是否将事件发送给攻击者。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + bool bSendToAttacker{false}; + + /** + * Gameplay tags to trigger as events. + * 作为事件触发的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + TArray EventTriggers; +}; + +/** + * Processor for triggering gameplay cues from attack results. + * 从攻击结果触发游戏反馈的处理器。 + * @note Cues do not replicate as attack results are replicated. + * @注意 反馈不复制,因为攻击结果已复制。 + */ +UCLASS() +class UGCS_AttackResultProcessor_GameplayCue : public UGCS_AttackResultProcessor_WithTagRequirement +{ + GENERATED_BODY() + +protected: + /** + * Handles attack results by triggering gameplay cues. + * 通过触发游戏反馈处理攻击结果。 + * @param AttackResult The attack result to handle. 要处理的攻击结果。 + */ + virtual void HandleIncomingAttackResult_Implementation(const FGCS_AttackResult& AttackResult) const override; + + /** + * Gets the editor-friendly name for the processor. + * 获取处理器的编辑器友好名称。 + * @return The editor-friendly name. 编辑器友好名称。 + */ + virtual FString GetEditorFriendlyName_Implementation() const override; + + /** + * Modifies gameplay cue parameters before execution. + * 在执行前修改游戏反馈参数。 + * @param ParametersToModify The parameters to modify. 要修改的参数。 + */ + UFUNCTION(BlueprintImplementableEvent, Category="GCS") + void ModifyGameplayCueParametersBeforeExecute(UPARAM(ref) + FGameplayCueParameters& ParametersToModify) const; + + /** + * Gameplay cues to trigger. + * 要触发的游戏反馈。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS", meta=(Categories="GameplayCue")) + TArray GameplayCues; + + /** + * Tag for finding raw magnitude in TaggedValues. + * 在TaggedValues中查找原始幅度的标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + FGameplayTag RawMagnitudeTag; + + /** + * Tag for finding normalized magnitude in TaggedValues. + * 在TaggedValues中查找归一化幅度的标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCS") + FGameplayTag NormalizedMagnitudeTag; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_CombatFlow.h b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_CombatFlow.h new file mode 100644 index 0000000..5be180b --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/CombatFlow/GCS_CombatFlow.h @@ -0,0 +1,114 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GCS_ActorOwnedObject.h" +#include "GCS_AttackResult.h" +#include "GGA_GameplayAttributeStructLibrary.h" +#include "GCS_CombatFlow.generated.h" + +class UGCS_AttackResultProcessor; +class UGCS_CombatSystemComponent; + +/** + * Combat flow for processing incoming attacks. + * 处理传入攻击的战斗流程。 + * @note Typically one instance per character type (e.g., human, quadruped, mechanical). + * @注意 通常每种角色类型一个实例(例如人类、四足动物、机械)。 + */ +UCLASS(Abstract, BlueprintType, Blueprintable, DefaultToInstanced, EditInlineNew, CollapseCategories, meta=(DisplayName="GCS Combat Flow")) +class GENERICCOMBATSYSTEM_API UGCS_CombatFlow : public UGCS_ActorOwnedObject +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_CombatFlow(); + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Checks if networking is supported. + * 检查是否支持网络。 + * @return True if supported. 如果支持返回true。 + */ + virtual bool IsSupportedForNetworking() const override { return true; } + + /** + * Gets the actor owning this combat flow. + * 获取拥有此战斗流程的演员。 + * @return The owning actor. 所属演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Combat Flow") + AActor* GetFlowOwner() const { return Owner; } + + /** + * Initializes the combat flow with an owner. + * 使用拥有者初始化战斗流程。 + * @param NewOwner The owning actor. 所属演员。 + */ + void Initialize(AActor* NewOwner); + + /** + * Adds dynamic tags to a gameplay effect spec. + * 为游戏效果规格添加动态标签。 + * @note Requires GGA_AbilitySystemGlobals as default AbilitySystemGlobals. + * @注意 需要将GGA_AbilitySystemGlobals设置为默认AbilitySystemGlobals。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 能力系统组件。 + * @param OutDynamicTagsAppendToSpec The tags to append (output). 要附加的标签(输出)。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat Flow") + void HandlePreGameplayEffectSpecApply(const FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer& OutDynamicTagsAppendToSpec); + + /** + * Handles gameplay effect execution. + * 处理游戏效果执行。 + * @param Payload The effect modification data. 效果修改数据。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat Flow") + void HandleGameplayEffectExecute(const FGGA_GameplayEffectModCallbackData& Payload); + virtual void HandleGameplayEffectExecute_Implementation(const FGGA_GameplayEffectModCallbackData& Payload); + + /** + * Handles attack results across the network. + * 在网络上处理攻击结果。 + * @note Default implementation calls result processors. + * @注意 默认实现调用结果处理器。 + * @param Payload The attack result. 攻击结果。 + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "GCS|Combat Flow") + void HandleAttackResult(const FGCS_AttackResult& Payload); + +protected: + /** + * The actor owning this combat flow. + * 拥有此战斗流程的演员。 + */ + UPROPERTY() + TObjectPtr Owner; + + /** + * Reference to the owning combat system component. + * 所属战斗系统组件的引用。 + */ + UPROPERTY(BlueprintReadOnly, Category = "GCS|Combat Flow", meta=(BlueprintProtected)) + TObjectPtr CombatComponent; + + /** + * List of attack result processors for handling attack results. + * 处理攻击结果的攻击结果处理器列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category = "GCS|Combat Flow Settings", meta=(TitleProperty="EditorFriendlyName")) + TArray> AttackResultProcessors; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Combo/GCS_ComboDefinition.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Combo/GCS_ComboDefinition.h new file mode 100644 index 0000000..fe5c010 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Combo/GCS_ComboDefinition.h @@ -0,0 +1,107 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/DataTable.h" +#include "StructUtils/InstancedStruct.h" +#include "GCS_ComboDefinition.generated.h" + +/** + * Base struct allow you to extend the combo definition's fields using C++. + * 基础结构体,允许你通过C++拓展连击定义的字段。 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICCOMBATSYSTEM_API FGCS_ComboDefinitionExtension +{ + GENERATED_BODY() +}; + +/** + * + */ +USTRUCT(BlueprintType) +struct FGCS_ComboDefinition : public FTableRowBase +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combo") + TSoftClassPtr AbilityClass; + + /** + * Will reset the combo step if this row was selected.(Only works if MinComboStep > 0) + * 此行选中则会重置连招步骤。(仅在MinComboStep > 0时生效。) + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combo") + bool bResetComboStep{false}; + + /** + * When Current Combo Step > this value, this won't be selected. 0 means no restriction. + * How many combo steps required to select this combo. + * 至少执行过几次combo,才能选择此行作为下一个combo。0代表入口。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Requirements", meta=(ClampMin=0)) + int32 MinComboStep = 0; + + /** + * The combo event data's event tag must equals to this tag if set, otherwise this combo won't be selected!. + * 若有值,连击事件数据的event tag必须与此值相等,否则该连击不会被选择。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Requirements") + FGameplayTag EventTag; + + /** + * The combo event data's instigator tags mush matches this Query if set, otherwise this combo won't be selected! + * 若有值,连击事件数据的instigator tags必须匹配此查询,否则该连击不会被选择。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Requirements") + FGameplayTagQuery EventInstigatorTagQuery; + + /** + * The tags owned by the ability system component of the pawn must much this query if set, otherwise this combo won't be selected! + * 若有值,Pawn的技能组件所拥有的Tags必须满足此查询,否则该连击不会被选择。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Requirements") + FGameplayTagQuery TagQuery; + + /** + * Should try if this ability can be activated before select it? + * 是否在选择Ability之前测试是否可激活? + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Activation") + bool bRunActivationTest{false}; + + /** + * Should abort the whole combo or just skip this selection when activation test failed? + * 若激活测试失败,是放弃整个Combo还是跳过此选择? + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Activation", meta=(EditCondition="bRunActivationTest")) + bool bAbortIfActivationTestFailed{false}; + + /** + * Native Instanced struct for extending the combo definition. + * 实例化结构体用于扩充连击定义的字段。 + * @attention For C++ users only. 仅针对C++用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + TInstancedStruct NativeExtension; + + /** + * Blueprint Instanced struct for extending the combo definition. + * 实例化结构体用于扩充连击定义的字段。 + * @attention For blueprint users only. 仅针对蓝图用户。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Extension") + FInstancedStruct Extension; + +#if WITH_EDITORONLY_DATA + /** + * Description for developers in the editor. + * 编辑器中用于开发者的描述。 + */ + UPROPERTY(EditAnywhere, Category = "Editor") + FString DevDescription; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_ActorOwnedObject.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_ActorOwnedObject.h new file mode 100644 index 0000000..294ce5c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_ActorOwnedObject.h @@ -0,0 +1,25 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GCS_ActorOwnedObject.generated.h" + +/** + * Base class for objects owned by an actor. + * 演员拥有的对象的基类。 + */ +UCLASS(NotBlueprintable, Abstract) +class GENERICCOMBATSYSTEM_API UGCS_ActorOwnedObject : public UObject +{ + GENERATED_BODY() + +public: + /** + * Gets the world context for the object. + * 获取对象的世界上下文。 + * @return The world context. 世界上下文。 + */ + virtual UWorld* GetWorld() const override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEntityInterface.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEntityInterface.h new file mode 100644 index 0000000..1d5eb7d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEntityInterface.h @@ -0,0 +1,199 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GCS_CombatStructLibrary.h" +#include "UObject/Interface.h" +#include "GCS_CombatEntityInterface.generated.h" + +class USceneComponent; + +/** + * Interface for actors or components involved in combat. + * 参与战斗的演员或组件的接口。 + * @note Use helper function "GetCombatInterface" for access. + * @注意 使用辅助函数"GetCombatInterface"访问。 + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable) +class UGCS_CombatEntityInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Combat interface for handling combat-related functionality. + * 处理战斗相关功能的接口。 + * @note Implementing objects should group related functionality. + * @注意 实现对象应分组相关功能。 + */ +class GENERICCOMBATSYSTEM_API IGCS_CombatEntityInterface +{ + GENERATED_BODY() + +public: + /** + * Gets the current combat target actor. + * 获取当前战斗目标演员。 + * @return The combat target actor. 战斗目标演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + AActor* GetCombatTargetActor() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + const UDataTable* GetComboDefinitionTable() const; + + /** + * Gets the current combat target object as a scene component. + * 获取当前战斗目标对象的场景组件。 + * @return The combat target scene component. 战斗目标场景组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + USceneComponent* GetCombatTargetObject() const; + + /** + * Queries ability actions based on tags. + * 根据标签查询能力动作。 + * @param AbilityTags Tags for the ability. 能力标签。 + * @param SourceTags Source tags for filtering. 来源标签。 + * @param TargetTags Target tags for filtering. 目标标签。 + * @param AbilityActions The matching ability actions (output). 匹配的能力动作(输出)。 + * @return True if valid results are found. 如果找到有效结果返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat", + meta=(ExpandBoolAsExecs="ReturnValue", DeprecatedFunction, DeprecationMessage="QueryAbilityActionsByContext as it is more reliable!")) + bool QueryAbilityActions(FGameplayTagContainer AbilityTags, FGameplayTagContainer SourceTags, FGameplayTagContainer TargetTags, TArray& AbilityActions); + + /** + * Queries ability actions based on tags and context object. + * 根据上下文和标签查询能力动作。 + * @param Context An optional Context object, Usually the source object if called from ability. + * @param AbilityTags Tags for the ability. 能力标签。 + * @param SourceTags Source tags for filtering. 来源标签。 + * @param TargetTags Target tags for filtering. 目标标签。 + * @param AbilityActions The matching ability actions (output). 匹配的能力动作(输出)。 + * @return True if valid results are found. 如果找到有效结果返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat", meta=(ExpandBoolAsExecs="ReturnValue")) + bool QueryAbilityActionsByContext(UObject* Context, FGameplayTagContainer AbilityTags, FGameplayTagContainer SourceTags, FGameplayTagContainer TargetTags, + TArray& AbilityActions); + + /** + * Queries a weapon based on a tag query. + * 根据标签查询武器。 + * @param Query The tag query for filtering. 标签查询。 + * @return The object implementing GCS_WeaponInterface. 实现武器接口的对象。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat", meta=(DisplayName="Query Weapon")) + UObject* QueryWeapon(const FGameplayTagQuery& Query) const; + + /** + * Sets the character's rotation mode (e.g., strafe). + * 设置角色的旋转模式(例如靶向移动)。 + * @param NewRotationMode The new rotation mode. 新旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + void SetRotationMode(FGameplayTag NewRotationMode); + + /** + * Gets the current rotation mode. + * 获取当前旋转模式。 + * @return The current rotation mode. 当前旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + FGameplayTag GetRotationMode() const; + + /** + * Sets the movement set (e.g., ADS, Guard). + * 设置运动集(例如瞄准、防御)。 + * @param NewMovementSet The new movement set. 新运动集。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + void SetMovementSet(FGameplayTag NewMovementSet); + + /** + * Gets the current movement set. + * 获取当前运动集。 + * @return The current movement set. 当前运动集。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + FGameplayTag GetMovementSet() const; + + /** + * Sets the movement state (e.g., walk, jog, sprint). + * 设置运动状态(例如走、跑、疾跑)。 + * @param NewMovementState The new movement state. 新运动状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + void SetMovementState(FGameplayTag NewMovementState); + + /** + * Gets the current movement state. + * 获取当前运动状态。 + * @return The current movement state. 当前运动状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Movement") + FGameplayTag GetMovementState() const; + + /** + * Initiates the death process (e.g., disable collision, drop weapons). + * 启动死亡流程(例如禁用碰撞、丢弃武器)。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Lifecycle") + void StartDeath(); + + /** + * Finalizes the death process (e.g., ragdoll, destroy actor). + * 完成死亡流程(例如布娃娃、销毁演员)。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Lifecycle") + void FinishDeath(); + + /** + * Checks if the character is dead. + * 检查角色是否死亡。 + * @return True if the character is dead. 如果角色死亡返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Lifecycle") + bool IsDead() const; + + /** + * Gets the movement input direction. + * 获取移动输入方向。 + * @return The movement input direction. 移动输入方向。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Input") + FVector GetMovementIntent() const; + + /** + * Gets the current used weapon. (You may have multiple weapon active at the same time, use this interface to get the current one.) + * 获取当前使用的武器。( 你可能有多个武器同时激活,使用此接口获取当前使用的那一个。) + * @param Context Optional context for querying. 可选查询上下文。 + * @return The object implementing GCS_WeaponInterface. 实现武器接口的对象。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon") + UObject* GetCurrentWeapon(UObject* Context = nullptr) const; + + /** + * Set the current used weapon.(You may have multiple weapon active at the same time, use this interface to set the current one.) + * 设置当前使用的武器。(你可能有多个武器同时激活,使用此接口设置当前使用的那一个。) + * @note This is not for weapon switching, only used to mark which weapon will be used for next action(Press X to use primary weapon/Y to use secondary weapon.). 这并非用于武器切换,仅用于标识当前使用的哪个武器(比如X使用主武器,Y使用副武器,在使用之前设置到底是哪一个。) + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon") + void SetCurrentWeapon(UObject* Weapon); + + /** + * Gets the relative transform for a mesh attached to a socket. + * 获取附加到插槽的网格的相对变换。 + * @param InSkeletalMeshComponent The skeletal mesh component. 骨骼网格组件。 + * @param StaticMesh The static mesh. 静态网格。 + * @param SkeletalMesh The skeletal mesh. 骨骼网格。 + * @param SocketName The socket name. 插槽名称。 + * @param OutTransform The relative transform (output). 相对变换(输出)。 + * @return True if transform is provided. 如果提供变换返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintPure=false, Category="GCS|Weapon", meta=(ExpandBoolAsExecs="ReturnValue")) + bool GetRelativeTransformToSocket(const USkeletalMeshComponent* InSkeletalMeshComponent, const UStaticMesh* StaticMesh, const USkeletalMesh* SkeletalMesh, FName SocketName, + FTransform& OutTransform) const; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEnumLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEnumLibrary.h new file mode 100644 index 0000000..1810602 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatEnumLibrary.h @@ -0,0 +1,95 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GCS_CombatEnumLibrary.generated.h" + +/** + * Enum for basic directional inputs. + * 基本方向输入的枚举。 + */ +UENUM(BlueprintType) +enum class EGCS_Direction : uint8 +{ + /** + * Forward direction. + * 前进方向。 + */ + Forward, + + /** + * Backward direction. + * 后退方向。 + */ + Backward, + + /** + * Left direction. + * 左方向。 + */ + Left, + + /** + * Right direction. + * 右方向。 + */ + Right +}; + +/** + * Enum for eight-directional inputs. + * 八方向输入的枚举。 + */ +UENUM(BlueprintType) +enum class EGCS_Direction_8 : uint8 +{ + /** + * Forward direction. + * 前进方向。 + */ + Forward, + + /** + * Forward-left direction. + * 前左方向。 + */ + Forward_Left, + + /** + * Forward-right direction. + * 前右方向。 + */ + Forward_Right, + + /** + * Backward direction. + * 后退方向。 + */ + Backward, + + /** + * Backward-left direction. + * 后左方向。 + */ + Backward_Left, + + /** + * Backward-right direction. + * 后右方向。 + */ + Backward_Right, + + /** + * Left direction. + * 左方向。 + */ + Left, + + /** + * Right direction. + * 右方向。 + */ + Right +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatStructLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatStructLibrary.h new file mode 100644 index 0000000..029f319 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatStructLibrary.h @@ -0,0 +1,230 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffect.h" +#include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/Object.h" +#include "GCS_CombatStructLibrary.generated.h" + +class UTargetingPreset; +class UAnimMontage; + +/** + * Structure for tagged value pairs. + * 标记值对的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_TaggedValue +{ + GENERATED_BODY() + + /** + * The gameplay tag for the attribute. + * 属性的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayTag Attribute; + + /** + * The value applied to the attribute. + * 应用于属性的值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + float Value{0}; +}; + +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICCOMBATSYSTEM_API FGCS_AbilityActionExtension +{ + GENERATED_BODY() +}; + +/** + * Structure for ability actions. + * 能力动作的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_AbilityAction +{ + GENERATED_BODY() + + /** + * The animation montage to play. + * 要播放的动画蒙太奇。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + TObjectPtr Animation; + + /** + * The playback rate for the montage. + * 蒙太奇的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + float PlayRate{1.f}; + + /** + * The starting section name for the montage. + * 蒙太奇的起始片段名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + FName StartSection{NAME_None}; + + /** + * Whether to stop the montage when the ability ends. + * 能力结束时是否停止蒙太奇。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + bool bStopWhenAbilityEnds{true}; + + /** + * Indicate if the selected anim sequence has root motion enabled.(It was auto calculated during save.) + * 标识选择的动画序列是启用了根运动。(保存时自动设置。) + */ + UPROPERTY(VisibleAnywhere, Category = "Animation", Meta=(EditCondition=False, EditConditionHides)) + bool bHasRootMotion{false}; + + /** + * Scale for animation root motion translation. + * 动画根运动平移的缩放。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + float AnimRootMotionTranslationScale{1.f}; + + /** + * Start time for the montage in seconds. + * 蒙太奇的起始时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + float StartTimeSeconds{0.f}; + + /** + * Whether to allow interruption after blend out. + * 是否允许在混合结束时中断。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation") + bool bAllowInterruptAfterBlendOut{false}; + + /** + * Gameplay effect for ability cost. + * 能力消耗的游戏效果。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayEffects") + TSubclassOf CostGameplayEffect; + + /** + * Allowing C++ users to add custom fields to ability action. + * 允许C++用户添加自定义字段到AbilityAction。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Extension") + TInstancedStruct Extension; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the ability action. + * 能力动作的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Structure for ability actions with tag queries. + * 带有标签查询的能力动作结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_AbilityActionsWithQuery +{ + GENERATED_BODY() + + /** + * Source tag query for filtering. + * 用于过滤的来源标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS") + FGameplayTagQuery SourceTagQuery; + + /** + * Target tag query for filtering. + * 用于过滤的目标标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS") + FGameplayTagQuery TargetTagQuery; + + /** + * Array of ability actions. + * 能力动作数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS", meta=(TitleProperty="EditorFriendlyName")) + TArray Actions; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the action set. + * 动作集的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Structure for ability action sets. + * 能力动作集的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_AbilityActionSet +{ + GENERATED_BODY() + + /** + * The gameplay tag for the ability. + * 能力的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS") + FGameplayTag AbilityTag; + + /** + * Array of ability actions. + * 能力动作数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS", meta=(TitleProperty="EditorFriendlyName")) + TArray Actions; + + /** + * Layered action sets for conditional selection based on tags. + * 基于标签条件选择的层次动作集。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS", meta=(TitleProperty="EditorFriendlyName")) + TArray Layered; +}; + +/** + * Base structure for user settings. + * 用户设置的基结构。 + */ +USTRUCT(BlueprintType) +struct UE_DEPRECATED(1.5, "Using Extension field insted of this one.") GENERICCOMBATSYSTEM_API FGCS_UserSetting +{ + GENERATED_BODY() +}; + +/** + * User settings structure for tag-to-float mappings. + * 标签到浮点映射的用户设置结构。 + */ +USTRUCT(BlueprintType) +struct UE_DEPRECATED(1.5, "this sample also deprecated due to FGCS_UserSetting was deprecated.") FGCS_UserSetting_Attributes : public FGCS_UserSetting +{ + GENERATED_BODY() + + /** + * Map of gameplay tags to float attributes. + * 游戏标签到浮点属性的映射。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="UserSettings") + TMap Attributes; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemComponent.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemComponent.h new file mode 100644 index 0000000..24ab07d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemComponent.h @@ -0,0 +1,411 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemGlobals.h" +#include "CombatFlow/GCS_AttackResult.h" +#include "GCS_CombatSystemComponent.generated.h" + +class UGCS_CombatFlow; + +/** + * Structure for requesting montage playback. + * 请求蒙太奇播放的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICCOMBATSYSTEM_API FGCS_PlayMontageRequest +{ + GENERATED_BODY() + + /** + * The animation montage to play. + * 要播放的动画蒙太奇。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS") + TObjectPtr AnimMontage{nullptr}; + + /** + * The playback rate for the montage. + * 蒙太奇的播放速率。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS") + float PlayRate{1.0f}; + + /** + * The starting section name for the montage. + * 蒙太奇的起始片段名称。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS") + FName StartSectionName{NAME_None}; + + /** + * The scale for root motion translation. + * 根运动平移的缩放。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS") + float RootTranslationScale{1.0f}; + + /** + * The start time for the montage in seconds. + * 蒙太奇的起始时间(秒)。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GCS") + float StartTimeSeconds{0.0f}; +}; + +/** + * Structure for predicted montage information. + * 预测蒙太奇信息的结构。 + */ +USTRUCT() +struct FGCS_PredictedMontageInfo +{ + GENERATED_BODY() + + /** + * The animation montage. + * 动画蒙太奇。 + */ + UPROPERTY() + TObjectPtr AnimMontage{nullptr}; + + /** + * The playback rate. + * 播放速率。 + */ + UPROPERTY() + float PlayRate{1.0f}; + + /** + * The starting section name. + * 起始片段名称。 + */ + UPROPERTY() + FName StartSectionName{NAME_None}; + + /** + * The time the montage was triggered. + * 蒙太奇触发的时间。 + */ + UPROPERTY() + float TriggeredTime{0.0f}; +}; + +/** + * Structure for replicated montage information. + * 复制蒙太奇信息的结构。 + */ +USTRUCT() +struct FGCS_ReplicatedMontageInfo +{ + GENERATED_BODY() + + /** + * The animation montage. + * 动画蒙太奇。 + */ + UPROPERTY() + TObjectPtr AnimMontage{nullptr}; + + /** + * The playback rate. + * 播放速率。 + */ + UPROPERTY() + float PlayRate{1.0f}; + + /** + * The starting section name. + * 起始片段名称。 + */ + UPROPERTY() + FName StartSectionName{NAME_None}; + + /** + * The time the montage was triggered. + * 蒙太奇触发的时间。 + */ + UPROPERTY() + float TriggeredTime{0.0f}; +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_ComboStepChangedEventSignature, int32, PrevComboStep); + + +/** + * Component for handling offensive and defensive combat behaviors. + * 处理进攻和防御战斗行为的组件。 + */ +UCLASS(ClassGroup=GCS, Blueprintable, BlueprintType, AutoExpandCategories=("GCS"), meta=(BlueprintSpawnableComponent)) +class GENERICCOMBATSYSTEM_API UGCS_CombatSystemComponent : public UActorComponent, public IGGA_AbilitySystemGlobalsEventReceiver +{ + GENERATED_BODY() + + friend UGCS_CombatFlow; + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_CombatSystemComponent(); + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the combat system component for an actor. + * 获取演员的战斗系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @return The combat system component. 战斗系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Combat", Meta = (DefaultToSelf="Actor")) + static UGCS_CombatSystemComponent* GetCombatSystemComponent(const AActor* Actor); + + /** + * Finds the combat system component for an actor. + * 查找演员的战斗系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @param CombatComponent The found component (output). 找到的组件(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Combat", Meta = (DefaultToSelf="Actor", ExpandBoolAsExecs = "ReturnValue")) + static bool FindCombatSystemComponent(const AActor* Actor, UGCS_CombatSystemComponent*& CombatComponent); + + /** + * Finds a typed combat system component for an actor. + * 查找演员的特定类型战斗系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @param DesiredClass The desired component class. 期望的组件类。 + * @param Component The found component (output). 找到的组件(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Combat", meta=(DefaultToSelf="Actor", DeterminesOutputType="DesiredClass", DynamicOutputParam="Component", ExpandBoolAsExecs="ReturnValue")) + static bool FindTypedCombatSystemComponent(AActor* Actor, TSubclassOf DesiredClass, UGCS_CombatSystemComponent*& Component); + + /** + * Gets the combat flow for handling incoming attacks. + * 获取处理传入攻击的战斗流程。 + * @return The combat flow instance. 战斗流程实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Combat", meta=(DisplayName="Get Combat Flow")) + UGCS_CombatFlow* GetCombatFlow() const; + + /** + * Registers an attack result. + * 注册攻击结果。 + * @param Payload The attack result to register. 要注册的攻击结果。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Combat") + void RegisterAttackResult(UPARAM(ref) + FGCS_AttackResult& Payload); + + /** + * Gets the last processed attack result. + * 获取最后处理的攻击结果。 + * @return The last attack result. 最后攻击结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Combat") + FGCS_AttackResult GetLastProcessedAttackResult() const; + + /** + * Sets the last processed attack result. + * 设置最后处理的攻击结果。 + * @param Payload The attack result to set. 要设置的攻击结果。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Combat") + void SetLastProcessedAttackResult(const FGCS_AttackResult& Payload); + + /** + * Plays a predictable montage for a target combat system component. + * 为目标战斗系统组件播放可预测的蒙太奇。 + * @param TargetCSC The target combat system component. 目标战斗系统组件。 + * @param Request The montage play request. 蒙太奇播放请求。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Combat") + void PlayPredictableMontageForTarget(UGCS_CombatSystemComponent* TargetCSC, FGCS_PlayMontageRequest Request); + + /** + * Server RPC to play a predictable montage for a target. + * 为目标播放可预测蒙太奇的服务器RPC。 + * @param TargetCSC The target combat system component. 目标战斗系统组件。 + * @param Request The montage play request. 蒙太奇播放请求。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GCS|Combat") + void ServerPlayPredictableMontageForTarget(UGCS_CombatSystemComponent* TargetCSC, FGCS_PlayMontageRequest Request); + + /** + * Sets the replicated montage information. + * 设置复制的蒙太奇信息。 + * @param Request The montage play request. 蒙太奇播放请求。 + */ + void SetReplicatedMontage(const FGCS_PlayMontageRequest& Request); + + /** + * Timer handle for montage-related operations. + * 蒙太奇相关操作的计时器句柄。 + */ + FTimerHandle TimerHandle; + + /** + * Handles replication of montage information. + * 处理蒙太奇信息的复制。 + */ + UFUNCTION() + void OnRep_ReplicatedMontageInfo(); + + /** + * Plays a predicted montage. + * 播放预测的蒙太奇。 + * @param Request The montage play request. 蒙太奇播放请求。 + */ + void PlayPredictedMontage(const FGCS_PlayMontageRequest& Request); + + /** + * Gets the character's skeletal mesh component. + * 获取角色的骨骼网格组件。 + * @return The skeletal mesh component. 骨骼网格组件。 + */ + USkeletalMeshComponent* GetCharacterMeshComponent() const; + +protected: + /** + * Handles pre-gameplay effect spec application. + * 处理游戏效果规格应用前逻辑。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 能力系统组件。 + */ + virtual void OnGlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) override; + + /** + * Handles replication of the combat flow. + * 处理战斗流程的复制。 + */ + UFUNCTION() + void OnRep_CombatFlow(); + + /** + * The class of the combat flow to instantiate. + * 要实例化的战斗流程类。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GCS|Combat Settings") + TSubclassOf CombatFlowClass; + + /** + * The instantiated combat flow. + * 实例化的战斗流程。 + */ + UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_CombatFlow, Category = "GCS|Combat State", meta=(ShowInnerProperties)) + TObjectPtr CombatFlow; + + /** + * The last attack result processed by the combat flow. + * 战斗流程处理的最后攻击结果。 + */ + UPROPERTY(VisibleAnywhere, Category = "GCS|Combat State") + FGCS_AttackResult LastProcessedAttackResult; + + /** + * Container for attack results. + * 攻击结果容器。 + */ + UPROPERTY(VisibleAnywhere, Replicated, Category="GCS|Combat State") + FGCS_AttackResultContainer AttackResultContainer; + + /** + * Replicated montage information. + * 复制的蒙太奇信息。 + */ + UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_ReplicatedMontageInfo, Category = "GCS|Combat State") + FGCS_ReplicatedMontageInfo ReplicatedMontageInfo; + + /** + * Predicted montage information. + * 预测的蒙太奇信息。 + */ + UPROPERTY(VisibleAnywhere, Category = "GCS|Combat State") + FGCS_PredictedMontageInfo PredictedMontageInfo; + + +#pragma region Combo System + +public: + /** + * Get the current combo step. + * @return The current combo step. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Combat") + int32 GetComboStep() const; + UFUNCTION(BlueprintCallable, Category="GCS|Combat") + void UpdateComboStep(int32 NewComboStep); + + UFUNCTION(BlueprintCallable, Category="GCS|Combat") + virtual void ResetComboState(); + + /** + * Event for combo step changed. + * 连击步骤变更事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FGCS_ComboStepChangedEventSignature OnComboStepChangedEvent; + +private: + void UpdateComboStep(int32 NewComboStep, bool bSendRpc); + + UFUNCTION() + void OnReplicated_ComboStep(int32 PrevComboStep); + + /** + * Client RPC to set the combo step. + * 客户端RPC设置运动集。 + * @param NewComboStep combo step. 新运动集。 + */ + UFUNCTION(Client, Reliable, WithValidation) + void ClientUpdateComboStep(int32 NewComboStep); + + /** + * Server RPC to set the combo step. + * 服务器RPC设置运动集。 + * @param NewComboStep The new combo step. 新运动集。 + */ + UFUNCTION(Server, Reliable, WithValidation) + void ServerUpdateComboStep(int32 NewComboStep); + +protected: + virtual bool ClientUpdateComboStep_Validate(int32 NewComboStep); + virtual bool ServerUpdateComboStep_Validate(int32 NewComboStep); + + UFUNCTION(BlueprintNativeEvent, Category="GMS|Combat") + void OnComboStepChanged(int32 PrevComboStep); + virtual void OnComboStepChanged_Implementation(int32 PrevComboStep); + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnReplicated_ComboStep, Category = "GCS|Combat State") + int32 ComboStep{0}; + +private: +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemSettings.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemSettings.h new file mode 100644 index 0000000..106ee15 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_CombatSystemSettings.h @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "GCS_CombatSystemSettings.generated.h" + +/** + * Settings for the combat system. + * 战斗系统的设置。 + */ +UCLASS(Config=Game, DefaultConfig) +class GENERICCOMBATSYSTEM_API UGCS_CombatSystemSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Gets the combat system settings instance. + * 获取战斗系统设置实例。 + * @return The combat system settings. 战斗系统设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static const UGCS_CombatSystemSettings* Get(); + + /** + * Tag name for querying the main skeletal mesh component. + * 查询主要骨骼网格组件的标签名称。 + */ + UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, NoClear, Category="Common", meta=(DisplayName="Main Mesh Lookup Tag Name")) + FName CharacterMeshLookupTag{TEXT("Main")}; + + /** + * Disables affiliation checks for debugging (allows cross-team damage). + * 禁用归属检查以进行调试(允许跨队伍伤害)。 + */ + UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, NoClear, Category="Debug") + bool bDisableAffiliationCheck{false}; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_EffectCauserInterface.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_EffectCauserInterface.h new file mode 100644 index 0000000..400a281 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_EffectCauserInterface.h @@ -0,0 +1,105 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectTypes.h" +#include "GameplayTagContainer.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "UObject/Interface.h" +#include "GCS_EffectCauserInterface.generated.h" + +class UGameplayEffect; + +/** + * Interface for objects that cause gameplay effects based on combat impact. + * 基于战斗影响产生游戏效果的对象的接口。 + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable) +class UGCS_EffectCauserInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for effect causers (e.g., bullets, weapons, traps). + * 效果触发者(例如子弹、武器、陷阱)的接口。 + */ +class GENERICCOMBATSYSTEM_API IGCS_EffectCauserInterface +{ + GENERATED_BODY() + +public: + /** + * Gets a pre-existing gameplay effect spec handle. + * 获取预存在的游戏效果规格句柄。 + * @param OutHandle The effect spec handle (output). 效果规格句柄(输出)。 + * @return True if a handle is provided. 如果提供句柄返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + bool GetEffectSpecHandle(FGameplayEffectSpecHandle& OutHandle); + virtual bool GetEffectSpecHandle_Implementation(FGameplayEffectSpecHandle& OutHandle) = 0; + + /** + * Gets the effect container. + * 获取效果容器。 + * @return The effect container. 效果容器。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + FGGA_GameplayEffectContainer GetEffectContainer(); + virtual FGGA_GameplayEffectContainer GetEffectContainer_Implementation() const = 0; + + /** + * Gets the effect container level override. + * 获取效果容器等级覆盖。 + * @return The level override. 等级覆盖。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + int32 GetEffectContainerLevelOverride() const; + virtual int32 GetEffectContainerLevelOverride_Implementation() const = 0; + + /** + * Sets the effect container spec. + * 设置效果容器规格。 + * @param InEffectContainerSpec The effect container spec. 效果容器规格。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + void SetEffectContainerSpec(const FGGA_GameplayEffectContainerSpec& InEffectContainerSpec); + virtual void SetEffectContainerSpec_Implementation(const FGGA_GameplayEffectContainerSpec& InEffectContainerSpec) = 0; + + /** + * Gets the effect container spec. + * 获取效果容器规格。 + * @return The effect container spec. 效果容器规格。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + FGGA_GameplayEffectContainerSpec GetEffectContainerSpec() const; + virtual FGGA_GameplayEffectContainerSpec GetEffectContainerSpec_Implementation() const = 0; + + /** + * Gets the gameplay effect class. + * 获取游戏效果类。 + * @return The gameplay effect class. 游戏效果类。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + TSubclassOf GetEffectClass() const; + virtual TSubclassOf GetEffectClass_Implementation() const = 0; + + /** + * Gets the effect level. + * 获取效果等级。 + * @return The effect level. 效果等级。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + int32 GetEffectLevel() const; + virtual int32 GetEffectLevel_Implementation() const = 0; + + /** + * Sets the gameplay effect spec for later use. + * 设置游戏效果规格以供后续使用。 + * @param InEffectSpec The effect spec to set. 要设置的效果规格。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Combat") + void SetEffectSpec(UPARAM(ref) FGameplayEffectSpecHandle& InEffectSpec); + virtual void SetEffectSpec_Implementation(FGameplayEffectSpecHandle& InEffectSpec) = 0; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_GameplayTags.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_GameplayTags.h new file mode 100644 index 0000000..a71f0a6 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_GameplayTags.h @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "NativeGameplayTags.h" + +namespace GCS_BulletLaunch +{ + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Always) + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DidNotHitPawn) + GENERICCOMBATSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(HitPawn) +} diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_LogChannels.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_LogChannels.h new file mode 100644 index 0000000..50ccd54 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GCS_LogChannels.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + +DECLARE_STATS_GROUP(TEXT("GCS"), STATGROUP_GCS, STATCAT_Advanced) + +GENERICCOMBATSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGCS, Log, All) + +GENERICCOMBATSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGCS_Targeting, Log, All) + +GENERICCOMBATSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGCS_Collision, Log, All) + +GENERICCOMBATSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGCS_Trace, Log, All) + + +/** + * Gets the context string for logging purposes. + * 获取用于日志记录的上下文字符串。 + * @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。 + * @return The context string. 上下文字符串。 + */ +GENERICCOMBATSYSTEM_API FString GetGCSLogContextString(const UObject* ContextObject = nullptr); + +GENERICCOMBATSYSTEM_API FString GetClientServerContextString(UObject* ContextObject = nullptr); + +#define GCS_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGCS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GCS_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGCS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGCSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GCS_CLOG_Trace(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGCS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGCSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GCS_OWNED_CLOG(LogOwner,Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGCS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGCSLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + + +#define GCS_VLOG(Verbosity, Format, ...) UE_VLOG(GetOwner(), LogGAIS_Command, Verbosity, Format, ##__VA_ARGS__) diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/GenericCombatSystem.h b/Plugins/GCS/Source/GenericCombatSystem/Public/GenericCombatSystem.h new file mode 100644 index 0000000..b44012c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/GenericCombatSystem.h @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FGenericCombatSystemModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_AttackTrace.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_AttackTrace.h new file mode 100644 index 0000000..76df8d1 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_AttackTrace.h @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimNotifies/AnimNotifyState.h" +#include "GCS_ANS_AttackTrace.generated.h" + +class UGCS_AttackRequest_Melee; + +/** + * Animation notify state for melee attack tracing. + * 近战攻击追踪的动画通知状态。 + */ +UCLASS(BlueprintType, Blueprintable, HideDropdown) +class GENERICCOMBATSYSTEM_API UGCS_ANS_AttackTrace : public UAnimNotifyState +{ + GENERATED_BODY() + +protected: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_ANS_AttackTrace(const FObjectInitializer& ObjectInitializer); + + /** + * Called after properties are initialized. + * 属性初始化后调用。 + */ + virtual void PostInitProperties() override; + + /** + * The melee attack request instance. + * 近战攻击请求实例。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category=Parameters) + TObjectPtr AttackRequest; + +#if WITH_EDITORONLY_DATA + /** + * Called before saving to validate data. + * 保存前调用以验证数据。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_BulletTrace.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_BulletTrace.h new file mode 100644 index 0000000..575cbc5 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_BulletTrace.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimNotifies/AnimNotifyState.h" +#include "GCS_ANS_BulletTrace.generated.h" + +class UGCS_AttackRequest_Bullet; +class UTargetingPreset; + +/** + * Animation notify state for bullet attack tracing. + * 子弹攻击追踪的动画通知状态。 + */ +UCLASS(BlueprintType, Blueprintable, HideDropdown) +class GENERICCOMBATSYSTEM_API UGCS_ANS_BulletTrace : public UAnimNotifyState +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_ANS_BulletTrace(const FObjectInitializer& ObjectInitializer); + + /** + * The bullet attack request instance. + * 子弹攻击请求实例。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category=Parameters) + TObjectPtr AttackRequest; + +#if WITH_EDITOR + /** + * Validates data for the notify. + * 验证通知数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; + + /** + * Called before saving to validate data. + * 保存前调用以验证数据。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_MovementCancellation.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_MovementCancellation.h new file mode 100644 index 0000000..708933a --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Notifies/GCS_ANS_MovementCancellation.h @@ -0,0 +1,74 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimNotifies/AnimNotify.h" +#include "Animation/AnimNotifies/AnimNotifyState.h" +#include "GCS_ANS_MovementCancellation.generated.h" + +/** + * Animation notify state to disable montage root motion when moving. + * 当角色移动时禁用蒙太奇根运动的动画通知状态。 + */ +UCLASS(BlueprintType, Blueprintable, Abstract, HideDropdown) +class GENERICCOMBATSYSTEM_API UGCS_ANS_MovementCancellation : public UAnimNotifyState +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_ANS_MovementCancellation(const FObjectInitializer& ObjectInitializer); + + /** + * Called when the notify begins. + * 通知开始时调用。 + * @param BranchingPointPayload The notify payload. 通知载荷。 + */ + virtual void BranchingPointNotifyBegin(FBranchingPointNotifyPayload& BranchingPointPayload) override; + + /** + * Called each frame during the notify. + * 通知期间每帧调用。 + * @param BranchingPointPayload The notify payload. 通知载荷。 + * @param FrameDeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void BranchingPointNotifyTick(FBranchingPointNotifyPayload& BranchingPointPayload, float FrameDeltaTime) override; + + /** + * Called when the notify ends. + * 通知结束时调用。 + * @param BranchingPointPayload The notify payload. 通知载荷。 + */ + virtual void BranchingPointNotifyEnd(FBranchingPointNotifyPayload& BranchingPointPayload) override; + +protected: + /** + * Checks if the skeletal mesh is moving. + * 检查骨骼网格是否在移动。 + * @param MeshComp The skeletal mesh component. 骨骼网格组件。 + * @return True if moving. 如果在移动返回true。 + */ + UFUNCTION(BlueprintNativeEvent) + bool IsMoving(USkeletalMeshComponent* MeshComp) const; + virtual bool IsMoving_Implementation(USkeletalMeshComponent* MeshComp) const; + + /** + * Indicates if root motion is disabled. + * 表示根运动是否被禁用。 + */ + bool IsRootMotionDisabled{false}; + +#if WITH_EDITOR + /** + * Checks if the notify can be placed on an animation. + * 检查通知是否可以放置在动画上。 + * @param Animation The animation sequence. 动画序列。 + * @return True if valid. 如果有效返回true。 + */ + virtual bool CanBePlaced(UAnimSequenceBase* Animation) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.h new file mode 100644 index 0000000..63d5da0 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_Affiliation.h @@ -0,0 +1,74 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Perception/AIPerceptionTypes.h" +#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h" +#include "GCS_TargetingFilterTask_Affiliation.generated.h" + +/** + * Filters targets based on team affiliation. + * 根据队伍归属过滤目标。 + */ +UCLASS(meta=(DisplayName="GCS:FilterTask (Affiliation)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingFilterTask_Affiliation : public UTargetingFilterTask_BasicFilterTemplate +{ + GENERATED_BODY() + +protected: + /** + * Affiliation filter settings. + * 归属过滤设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + FAISenseAffiliationFilter DetectionByAffiliation; + + /** + * Whether to check for CombatTeamAgentInterface. + * 是否检查CombatTeamAgentInterface。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + bool bLookCombatTeamAgentInterface{true}; + + /** + * Whether to check for GenericTeamAgentInterface. + * 是否检查GenericTeamAgentInterface。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + bool bLookGenericTeamAgentInterface{true}; + + /** + * Whether to ignore target actor who has no team assigned(teamId:255) + * 是否忽略没有队伍的目标(即TeamId为255)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + bool bIgnoreTargetWithNoTeam{true}; + + /** + * Determines if a target should be filtered based on affiliation. + * 根据归属确定是否应过滤目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return True if the target should be filtered. 如果应过滤目标返回true。 + */ + virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override; + + /** + * Gets the source team ID. + * 获取来源队伍ID。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return The source team ID. 来源队伍ID。 + */ + virtual FGenericTeamId GetSourceTeamId(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const; + + /** + * Gets the target team ID. + * 获取目标队伍ID。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return The target team ID. 目标队伍ID。 + */ + virtual FGenericTeamId GetTargetTeamId(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_IsDead.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_IsDead.h new file mode 100644 index 0000000..de1cc8c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_IsDead.h @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h" +#include "GCS_TargetingFilterTask_IsDead.generated.h" + +/** + * Filters out dead targets. + * 过滤掉已死亡的目标。 + */ +UCLASS(meta=(DisplayName="GCS:FilterTask (IsDead)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingFilterTask_IsDead : public UTargetingFilterTask_BasicFilterTemplate +{ + GENERATED_BODY() + +protected: + /** + * Determines if a target should be filtered based on death state. + * 根据死亡状态确定是否应过滤目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return True if the target is dead and should be filtered. 如果目标已死亡且应过滤返回true。 + */ + virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.h new file mode 100644 index 0000000..8fc3314 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TagsRequirements.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h" +#include "GCS_TargetingFilterTask_TagsRequirements.generated.h" + +/** + * Filters targets based on a gameplay tag query. + * 根据游戏标签查询过滤目标。 + */ +UCLASS(meta=(DisplayName="GCS:FilterTask (TagsRequirements)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingFilterTask_TagsRequirements : public UTargetingFilterTask_BasicFilterTemplate +{ + GENERATED_BODY() + +protected: + /** + * Whether to invert the filter result. + * 是否反转过滤结果。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + bool bInvert{false}; + + /** + * The tag query that targets must match. + * 目标必须匹配的标签查询。 + * @note If empty, no filtering is applied. 如果为空,不应用过滤。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter", meta = (DisplayName = "Query Must Match")) + FGameplayTagQuery TagQuery; + + /** + * Whether to fall back to GameplayTagAssetInterface if AbilitySystemComponent fails. + * 如果AbilitySystemComponent失败,是否回退到GameplayTagAssetInterface。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Filter") + bool bLookingForTagAssetInterface{false}; + + /** + * Determines if a target should be filtered. + * 确定是否应过滤目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return True if the target should be filtered. 如果应过滤目标返回true。 + */ + virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.h new file mode 100644 index 0000000..8358790 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Filters/GCS_TargetingFilterTask_TraceInstance.h @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h" +#include "GCS_TargetingFilterTask_TraceInstance.generated.h" + +/** + * Filters targets based on the CanHitActor check of a collision trace instance. + * 根据碰撞检测实例的CanHitActor检查过滤目标。 + * @note Requires SourceObject to be a collision trace instance. + * @注意 需要SourceObject是碰撞检测实例。 + */ +UCLASS(meta=(DisplayName="GCS:Filter Task (TraceInstance CanHitActor)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingFilterTask_TraceInstance : public UTargetingFilterTask_BasicFilterTemplate +{ + GENERATED_BODY() + +protected: + /** + * Determines if a target should be filtered. + * 确定是否应过滤目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return True if the target should be filtered. 如果应过滤目标返回true。 + */ + virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingFunctionLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingFunctionLibrary.h new file mode 100644 index 0000000..5747a36 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingFunctionLibrary.h @@ -0,0 +1,59 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTargetTypes.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Types/TargetingSystemTypes.h" +#include "GCS_TargetingFunctionLibrary.generated.h" + +/** + * Extended library for targeting system utilities. + * 目标系统实用程序的扩展库。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_TargetingFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Gets the targeting source context for a targeting request handle. + * 获取目标请求句柄的目标源上下文。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @return The targeting source context. 目标源上下文。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting System | Targeting Types") + static FTargetingSourceContext GetTargetingSourceContext(FTargetingRequestHandle TargetingHandle); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting System | Targeting Types") + static FString GetTargetingSourceContextDebugString(FTargetingRequestHandle TargetingHandle); + + /** + * Gets the actor targets from a targeting request handle. + * 从目标请求句柄获取Actor目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param Targets The actor targets (output). Actor目标(输出)。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting System | Targeting Results") + static void GetTargetingResultsActors(FTargetingRequestHandle TargetingHandle, TArray& Targets); + + /** + * Gets the hit results for a targeting handle. + * 获取目标句柄的命中结果。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param OutTargets The hit results (output). 命中结果(输出)。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting System | Targeting Results") + static void GetTargetingResults(FTargetingRequestHandle TargetingHandle, TArray& OutTargets); + + /** + * Converts targeting location info to a source context. + * 将目标位置信息转换为源上下文。 + * @param LocationInfo The targeting location info. 目标位置信息。 + * @return The targeting source context. 目标源上下文。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting System") + static FTargetingSourceContext ConvertTargetingLocationInfoToSourceContext(FGameplayAbilityTargetingLocationInfo LocationInfo); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSourceInterface.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSourceInterface.h new file mode 100644 index 0000000..3adbe58 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSourceInterface.h @@ -0,0 +1,77 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GCS_TargetingSourceInterface.generated.h" + +/** + * Interface for objects providing targeting system information. + * 为目标系统提供信息的对象的接口。 + * @note Used to provide additional targeting data. + * @注意 用于提供额外的目标数据。 + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable) +class UGCS_TargetingSourceInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for targeting source objects. + * 目标源对象的接口。 + */ +class GENERICCOMBATSYSTEM_API IGCS_TargetingSourceInterface +{ + GENERATED_BODY() + +public: + /** + * Gets the trace level for dynamic tracing. + * 获取动态追踪的追踪级别。 + * @return The trace level. 追踪级别。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Trace") + float GetTraceLevel() const; + virtual float GetTraceLevel_Implementation() const = 0; + + /** + * Gets the trace direction for targeting. + * 获取目标的追踪方向。 + * @param OutDirection The trace direction (output). 追踪方向(输出)。 + * @return True if a direction is provided. 如果提供方向返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Trace") + bool GetTraceDirection(FVector& OutDirection) const; + virtual bool GetTraceDirection_Implementation(FVector& OutDirection) const = 0; + + /** + * Gets the swept trace rotation for capsule or box traces. + * 获取胶囊或盒体追踪的旋转。 + * @param OutRotation The rotation (output). 旋转(输出)。 + * @return True if a rotation is provided. 如果提供旋转返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Trace") + bool GetSweptTraceRotation(FRotator& OutRotation) const; + virtual bool GetSweptTraceRotation_Implementation(FRotator& OutRotation) const = 0; + + /** + * Gets the shape component for trace properties. + * 获取追踪属性的形状组件。 + * @param OutShape The shape component (output). 形状组件(输出)。 + * @return True if a shape component is provided. 如果提供形状组件返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Trace") + bool GetTraceShape(UShapeComponent*& OutShape) const; + virtual bool GetTraceShape_Implementation(UShapeComponent*& OutShape) const = 0; + + /** + * Gets additional actors to ignore during tracing. + * 获取追踪期间忽略的额外Actor。 + * @return Array of actors to ignore. 忽略的Actor数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Trace") + TArray GetAdditionalActorsToIgnore() const; + virtual TArray GetAdditionalActorsToIgnore_Implementation() const = 0; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSystemComponent.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSystemComponent.h new file mode 100644 index 0000000..94b793e --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/GCS_TargetingSystemComponent.h @@ -0,0 +1,248 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/PawnComponent.h" +#include "GCS_TargetingSystemComponent.generated.h" + +class UTargetingPreset; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_OnTargetLockOnSignature, AActor*, NewTargetActor); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_OnTargetLockOffSignature, AActor*, PrevTargetActor); + + +/** + * Component for managing combat targeting. + * 管理战斗目标的组件。 + */ +UCLASS(ClassGroup=(GCS), AutoExpandCategories=("GCS"), meta=(BlueprintSpawnableComponent), Blueprintable) +class GENERICCOMBATSYSTEM_API UGCS_TargetingSystemComponent : public UPawnComponent +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_TargetingSystemComponent(const FObjectInitializer& ObjectInitializer); + + /** + * Gets the targeting system component from an actor. + * 从Actor获取目标系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The targeting system component. 目标系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting", Meta = (DefaultToSelf="Actor")) + static UGCS_TargetingSystemComponent* GetTargetingSystemComponent(const AActor* Actor); + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + +protected: + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * The currently targeted actor. + * 当前目标Actor。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Replicated, Category = "GCS|Targeting") + TObjectPtr TargetedActor = nullptr; + + /** + * List of potential target actors (server-side only). + * 潜在目标Actor列表(仅限服务器)。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GCS|Targeting", meta=(AllowPrivateAccess=true)) + TArray> PotentialTargets; + + /** + * Whether to automatically update potential targets based on tick rate. + * 是否根据tick频率自动更新潜在目标。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS|Targeting", meta=(AllowPrivateAccess=true)) + bool bAutoUpdatePotentialTargets{true}; + + /** + * Targeting preset for searching and filtering targets. + * 用于搜索和过滤目标的目标预设。 + * @note The component's owner is the SourceActor, and the component is the SourceObject. + * @注意 组件的Owner作为SourceActor,组件本身作为SourceObject。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GCS|Targeting", meta=(AllowPrivateAccess=true)) + TObjectPtr TargetingPreset; + + /** + * Flag to indicate we should be using async targeting + * 是否使用异步定位? + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GCS|Targeting", meta=(AllowPrivateAccess=true)) + bool bUseAsyncTargeting{true}; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + /** + * Refreshes targeting based on the delta time. + * 根据时间增量刷新目标。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshTargeting(float DeltaTime); + + /** + * Forces collection of potential targets and selects the best one. + * 强制收集潜在目标并选择最佳目标。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Targeting") + void SearchForActorToTarget(); + + /** + * Selects the closest actor from potential targets within a radius. + * 从潜在目标中选择指定范围内最近的Actor。 + * @param Radius The search radius. 搜索半径。 + * @return The closest actor. 最近的Actor。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Targeting") + AActor* SelectClosestActorFromPotentialTargets(float Radius = 100) const; + + /** + * Filters actors using a targeting preset. + * 使用目标预设过滤Actor。 + * @param InTargetingPreset The targeting preset. 目标预设。 + * @param InTargets The input targets. 输入目标。 + * @param OutActors The filtered actors (output). 过滤后的Actor(输出)。 + * @return True if filtering succeeded. 如果过滤成功返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Targeting", meta=(ExpandBoolAsExecs="ReturnValue")) + bool FilterActorsWithPreset(UTargetingPreset* InTargetingPreset, const TArray InTargets, TArray& OutActors); + + /** + * Selects a target from potential targets. + * 从潜在目标中选择一个目标。 + */ + virtual void SelectFromPotentialTargets(); + + /** + * Switches to a new target based on direction. + * 根据方向切换到新目标。 + * @param RightDirection Whether to switch to the right. 是否向右切换。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Targeting") + void StaticSwitchToNewTarget(bool RightDirection); + + /** + * Refreshes the list of potential targets using the targeting preset. + * 使用目标预设刷新潜在目标列表。 + */ + virtual void RefreshPotentialTargets(); + + /** + * Checks if an actor can be targeted. + * 检查Actor是否可以被目标。 + * @param ActorToTarget The actor to check. 要检查的Actor。 + * @return True if the actor can be targeted. 如果Actor可被目标返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Targeting") + bool CanBeTargeted(AActor* ActorToTarget); + virtual bool CanBeTargeted_Implementation(AActor* ActorToTarget); + + /** + * Gets the currently targeted actor. + * 获取当前目标Actor。 + * @return The targeted actor. 目标Actor。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting") + FORCEINLINE AActor* GetTargetedActor() { return TargetedActor; } + + /** + * Sets the targeted actor. + * 设置目标Actor。 + * @param NewActor The new target actor. 新目标Actor。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS|Targeting") + void SetTargetedActor(AActor* NewActor); + +private: + /** + * Sets the targeted actor with optional RPC. + * 设置目标Actor,可选择是否发送RPC。 + * @param NewActor The new target actor. 新目标Actor。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetTargetedActor(AActor* NewActor, bool bSendRpc); + + /** + * Client RPC to set the targeted actor. + * 设置目标Actor的客户端RPC。 + * @param NewActor The new target actor. 新目标Actor。 + */ + UFUNCTION(Client, Reliable) + void ClientSetTargetedActor(AActor* NewActor); + virtual void ClientSetTargetedActor_Implementation(AActor* NewActor); + + /** + * Server RPC to set the targeted actor. + * 设置目标Actor的服务器RPC。 + * @param NewActor The new target actor. 新目标Actor。 + */ + UFUNCTION(Server, Reliable) + void ServerSetTargetedActor(AActor* NewActor); + virtual void ServerSetTargetedActor_Implementation(AActor* NewActor); + +protected: + /** + * Handles target lock-off events. + * 处理目标解锁事件。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Targeting") + void OnLockOff(); + virtual void OnLockOff_Implementation(); + + /** + * Handles target lock-on events. + * 处理目标锁定事件。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Targeting") + void OnLockOn(); + virtual void OnLockOn_Implementation(); + + /** + * Delegate for target lock-on events. + * 目标锁定事件的委托。 + */ + UPROPERTY(BlueprintAssignable, Category="GCS|Targeting") + FGCS_OnTargetLockOnSignature OnTargetLockOnEvent; + + /** + * Delegate for target lock-off events. + * 目标解锁事件的委托。 + */ + UPROPERTY(BlueprintAssignable, Category="GCS|Targeting") + FGCS_OnTargetLockOffSignature OnTargetLockOffEvent; + + /** + * Calculates the view angle to a target actor. + * 计算到目标Actor的视角角度。 + * @param TargetActor The target actor. 目标Actor。 + * @return The view angle. 视角角度。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS|Targeting") + virtual float CalculateViewAngle(const AActor* TargetActor); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.h new file mode 100644 index 0000000..fa0c18d --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_LineTrace.h @@ -0,0 +1,132 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CollisionShape.h" +#include "Engine/CollisionProfile.h" +#include "Types/TargetingSystemTypes.h" +#include "ScalableFloat.h" +#include "Tasks/TargetingTask.h" +#include "UObject/Object.h" + +#include "GCS_TargetingSelectionTask_LineTrace.generated.h" + +class UTargetingSubsystem; +struct FCollisionQueryParams; +struct FTargetingDebugInfo; +struct FTargetingDefaultResultData; +struct FTargetingRequestHandle; +struct FTraceDatum; +struct FTraceHandle; + +/** +* @class UGCS_TargetingSelectionTask_LineTrace +* Selection task that can perform a synchronous or asynchronous line trace, always generate hit results. +* to find all targets up to the first blocking hit (or its end point). +*/ +UCLASS(Blueprintable, meta=(DisplayName="GCS:SelectionTask (Line Trace)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingSelectionTask_LineTrace : public UTargetingTask +{ + GENERATED_BODY() + +public: + UGCS_TargetingSelectionTask_LineTrace(const FObjectInitializer& ObjectInitializer); + + /** Evaluation function called by derived classes to process the targeting request */ + virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override; + +protected: + /** Native Event to get the source location for the Trace */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + FVector GetSourceLocation(const FTargetingRequestHandle& TargetingHandle) const; + + /** Native Event to get a source location offset for the Trace */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + FVector GetSourceOffset(const FTargetingRequestHandle& TargetingHandle) const; + + /** + * Native Event to get the direction for the Trace + * Default will use pawn's control rotation or fallback to actor forward direction. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + FVector GetTraceDirection(const FTargetingRequestHandle& TargetingHandle) const; + + /** Native Event to get the length for the Trace */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + float GetTraceLength(const FTargetingRequestHandle& TargetingHandle) const; + + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + float GetTraceLevel(const FTargetingRequestHandle& TargetingHandle) const; + + /** Native Event to get additional actors the Trace should ignore */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "Target Trace Selection") + void GetAdditionalActorsToIgnore(const FTargetingRequestHandle& TargetingHandle, TArray& OutAdditionalActorsToIgnore) const; + +protected: + /** Method to process the trace task immediately */ + void ExecuteImmediateTrace(const FTargetingRequestHandle& TargetingHandle) const; + + /** Method to process the trace task asynchronously */ + void ExecuteAsyncTrace(const FTargetingRequestHandle& TargetingHandle) const; + + /** Callback for an async trace */ + void HandleAsyncTraceComplete(const FTraceHandle& InTraceHandle, FTraceDatum& InTraceDatum, FTargetingRequestHandle TargetingHandle) const; + + /** Method to take the hit results and store them in the targeting result data */ + void ProcessHitResults(const FTargetingRequestHandle& TargetingHandle, const TArray& Hits) const; + + /** Setup CollisionQueryParams for the trace */ + void InitCollisionParams(const FTargetingRequestHandle& TargetingHandle, FCollisionQueryParams& OutParams) const; + +protected: + /** The trace channel to use */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Collision Data") + TEnumAsByte TraceChannel; + + /** The collision profile name to use instead of trace channel (does not work for async traces) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Collision Data") + FCollisionProfileName CollisionProfileName; + + /** The default trace length to use if GetTraceLength is not overridden by a child */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + FScalableFloat DefaultTraceLength = 10.0f; + + /** The default source location offset used by GetSourceOffset */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + FVector DefaultSourceOffset = FVector::ZeroVector; + + /** Indicates the trace should perform a complex trace */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + uint8 bComplexTrace : 1; + + /** Indicates the trace should ignore the source actor */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + uint8 bIgnoreSourceActor : 1; + + /** Indicates the trace should ignore the source actor */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + uint8 bIgnoreInstigatorActor : 1; + + // If there were no hits, add a default HitResult at the end of the trace + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Target Trace Selection | Trace Data") + uint8 bGenerateDefaultHitResult : 1; + +protected: +#if WITH_EDITOR + virtual bool CanEditChange(const FProperty* InProperty) const override; +#endif + + /** Debug Helper Methods */ +#if ENABLE_DRAW_DEBUG + +private: + virtual void DrawDebug(UTargetingSubsystem* TargetingSubsystem, FTargetingDebugInfo& Info, const FTargetingRequestHandle& TargetingHandle, float XOffset, float YOffset, + int32 MinTextRowsToAdvance) const override; + + /** Draw debug info showing the results of the shape trace used for targeting. */ + virtual void DrawDebugTrace(const FTargetingRequestHandle TargetingHandle, const FVector& StartLocation, const FVector& EndLocation, const bool bHit, const TArray& Hits) const; + void BuildTraceResultsDebugString(const FTargetingRequestHandle& TargetingHandle, const TArray& TargetResults) const; + void ResetTraceResultsDebugString(const FTargetingRequestHandle& TargetingHandle) const; +#endif // ENABLE_DRAW_DEBUG + /** ~Debug Helper Methods */ +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.h new file mode 100644 index 0000000..f6f7266 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt.h @@ -0,0 +1,67 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Tasks/TargetingSelectionTask_Trace.h" +#include "GCS_TargetingSelectionTask_TraceExt.generated.h" + +class UDEPRECATED_GCS_CollisionTraceInstance; + +/** +* @class UGCS_TargetingSelectionTask_TraceExt +* Specialized version of SelectionTask_Trace,Allow passing data via source object to Trace execution. +* @attention SourceObject should be provided and implement GCS_TargetingSourceInterface. +*/ +UCLASS(meta=(DisplayName="GCS:SelectionTask (Trace)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingSelectionTask_TraceExt : public UTargetingSelectionTask_Trace +{ + GENERATED_BODY() + +public: + virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override; + +protected: + /** + * If ticked, user context's source location as trace source location. + * Or it will try to get context's source actor location first,then fall back to context's source location. + * 如果勾选,会使用上下文的源位置作为Trace的源位置。 + * 否则它会先从上下文的源Actor上获取位置,如果没有Actor,则回退到上下文的源位置。 + */ + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Trace Data") + bool bUseContextLocationAsSourceLocation{false}; + + virtual FVector GetSourceLocation_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + virtual FVector GetTraceDirection_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + virtual void GetAdditionalActorsToIgnore_Implementation(const FTargetingRequestHandle& TargetingHandle, TArray& OutAdditionalActorsToIgnore) const override; + + /** Native Event to get the source location for the Trace */ + UE_DEPRECATED(1.5, "CollisionTraceInstance no longer required!") + UFUNCTION(BlueprintNativeEvent, Category = "Target Trace Selection", meta=(DeprecatedFunction)) + UDEPRECATED_GCS_CollisionTraceInstance* GetSourceTraceInstance(const FTargetingRequestHandle& TargetingHandle) const; + + UFUNCTION(BlueprintNativeEvent, Category = "Target Trace Selection") + float GetTraceLevel(const FTargetingRequestHandle& TargetingHandle) const; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Trace Data") + bool bTraceLengthLevel{true}; + + virtual float GetTraceLength_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Swept Data") + bool bSweptTraceRadiusLevel{true}; + + virtual float GetSweptTraceRadius_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Swept Data") + bool bSweptTraceCapsuleHalfHeightLevel{true}; + + virtual float GetSweptTraceCapsuleHalfHeight_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Swept Data") + bool bSweptTraceBoxHalfExtentLevel{true}; + + virtual FVector GetSweptTraceBoxHalfExtents_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.h new file mode 100644 index 0000000..cafa3e8 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Targeting/Selections/GCS_TargetingSelectionTask_TraceExt_BindShape.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_TargetingSelectionTask_TraceExt.h" +#include "GCS_TargetingSelectionTask_TraceExt_BindShape.generated.h" + + +class UShapeComponent; + +UENUM(BlueprintType) +enum class EGCS_TraceDataModifyType :uint8 +{ + None UMETA(DisplayName="None"), + Add UMETA(DisplayName="Add"), + Multiply UMETA(DisplayName = "Multiply"), +}; + +/** + * + */ +UCLASS(meta=(DisplayName="GCS:SelectionTask (Trace Bind Shape)")) +class GENERICCOMBATSYSTEM_API UGCS_TargetingSelectionTask_TraceExt_BindShape : public UGCS_TargetingSelectionTask_TraceExt +{ + GENERATED_BODY() + +public: + virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override; + +protected: + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Trace Data") + EGCS_TraceDataModifyType SweptTraceRadiusModType{EGCS_TraceDataModifyType::None}; + + virtual float GetSweptTraceRadius_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Swept Data") + EGCS_TraceDataModifyType SweptTraceCapsuleHalfHeightModType{EGCS_TraceDataModifyType::None}; + + virtual float GetSweptTraceCapsuleHalfHeight_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + UPROPERTY(EditAnywhere, Category = "Target Trace Selection | Swept Data") + EGCS_TraceDataModifyType SweptTraceBoxHalfExtentModType{EGCS_TraceDataModifyType::None}; + + virtual FVector GetSweptTraceBoxHalfExtents_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + + virtual UShapeComponent* GetTraceShape(const FTargetingRequestHandle& TargetingHandle) const; + + virtual FRotator GetSweptTraceRotation_Implementation(const FTargetingRequestHandle& TargetingHandle) const override; + +public: +#if WITH_EDITORONLY_DATA + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentComponent.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentComponent.h new file mode 100644 index 0000000..d100d13 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentComponent.h @@ -0,0 +1,99 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatTeamAgentInterface.h" +#include "Components/ActorComponent.h" +#include "GCS_CombatTeamAgentComponent.generated.h" + +/** + * Component for managing combat team affiliations. + * 管理战斗队伍归属的组件。 + */ +UCLASS(ClassGroup=(GCS), meta=(BlueprintSpawnableComponent), AutoExpandCategories=(GCS)) +class GENERICCOMBATSYSTEM_API UGCS_CombatTeamAgentComponent : public UActorComponent, public IGCS_CombatTeamAgentInterface +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGCS_CombatTeamAgentComponent(); + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the delegate for team ID changes. + * 获取队伍ID更改的委托。 + * @return The team ID changed delegate. 队伍ID更改委托。 + */ + virtual FGCS_CombatTeamIdChangedSignature* GetOnTeamIdChangedDelegate() override; + + /** + * Gets the current combat team ID. + * 获取当前战斗队伍ID。 + * @return The combat team ID. 战斗队伍ID。 + */ + virtual FGenericTeamId GetCombatTeamId_Implementation() const override; + + /** + * Sets the combat team ID. + * 设置战斗队伍ID。 + * @param NewTeamId The new team ID. 新队伍ID。 + */ + virtual void SetCombatTeamId_Implementation(FGenericTeamId NewTeamId) override; + + /** + * Handles replication of the combat team ID. + * 处理战斗队伍ID的复制。 + * @param OldTeamID The previous team ID. 旧队伍ID。 + */ + UFUNCTION() + void OnRep_CombatTeamId(FGenericTeamId OldTeamID); + + /** + * Delegate for team ID change events. + * 队伍ID更改事件的委托。 + */ + UPROPERTY(BlueprintAssignable, Category="GCS") + FGCS_CombatTeamIdChangedSignature OnTeamIdChangedEvent; + +protected: + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * The current team ID of this agent. + * 此代理的当前队伍ID。 + */ + UPROPERTY(EditAnywhere, Category="GCS", ReplicatedUsing=OnRep_CombatTeamId) + FGenericTeamId CombatTeamId; + + /** + * Whether to assign the team ID to the controller. + * 是否将队伍ID分配给控制器。 + */ + UPROPERTY(EditAnywhere, Category="GCS") + bool bAssignTeamIdToController{true}; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentInterface.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentInterface.h new file mode 100644 index 0000000..07b553f --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Team/GCS_CombatTeamAgentInterface.h @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GenericTeamAgentInterface.h" +#include "UObject/Interface.h" +#include "GCS_CombatTeamAgentInterface.generated.h" + +/** + * Delegate for team ID change events. + * 队伍ID更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGCS_CombatTeamIdChangedSignature, UObject*, ObjectChangingTeam, FGenericTeamId, OldTeamID, FGenericTeamId, NewTeamID); + +/** + * Interface for combat team agents. + * 战斗队伍代理的接口。 + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable) +class UGCS_CombatTeamAgentInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for managing combat team affiliations. + * 管理战斗队伍归属的接口。 + */ +class GENERICCOMBATSYSTEM_API IGCS_CombatTeamAgentInterface +{ + GENERATED_BODY() + +public: + /** + * Sets the combat team ID. + * 设置战斗队伍ID。 + * @note Default implementation converts to GenericTeamAgentInterface. + * @注意 默认实现转换为GenericTeamAgentInterface。 + * @param NewTeamId The new team ID. 新队伍ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|CombatTeam") + void SetCombatTeamId(FGenericTeamId NewTeamId); + virtual void SetCombatTeamId_Implementation(FGenericTeamId NewTeamId); + + /** + * Gets the current combat team ID. + * 获取当前战斗队伍ID。 + * @return The combat team ID. 战斗队伍ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|CombatTeam") + FGenericTeamId GetCombatTeamId() const; + virtual FGenericTeamId GetCombatTeamId_Implementation() const; + + /** + * Gets the delegate for team ID changes. + * 获取队伍ID更改的委托。 + * @return The team ID changed delegate. 队伍ID更改委托。 + */ + virtual FGCS_CombatTeamIdChangedSignature* GetOnTeamIdChangedDelegate() = 0; + + /** + * Conditionally broadcasts team change events. + * 有条件地广播队伍更改事件。 + * @param This The combat team agent interface. 战斗队伍代理接口。 + * @param OldTeamID The previous team ID. 旧队伍ID。 + * @param NewTeamID The new team ID. 新队伍ID。 + */ + static void ConditionalBroadcastTeamChanged(TScriptInterface This, FGenericTeamId OldTeamID, FGenericTeamId NewTeamID); + + /** + * Gets the team changed delegate with validation. + * 获取经过验证的队伍更改委托。 + * @return The team changed delegate. 队伍更改委托。 + */ + FGCS_CombatTeamIdChangedSignature& GetTeamChangedDelegateChecked() + { + FGCS_CombatTeamIdChangedSignature* Result = GetOnTeamIdChangedDelegate(); + check(Result); + return *Result; + } +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_AttackDefinitionFunctionLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_AttackDefinitionFunctionLibrary.h new file mode 100644 index 0000000..adb62c1 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_AttackDefinitionFunctionLibrary.h @@ -0,0 +1,22 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GCS_AttackDefinitionFunctionLibrary.generated.h" + +/** + * + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_AttackDefinitionFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "MigrateAttackDefinitionTable") + static void MigrateAttackDefinitionTable(UDataTable* InTable); +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_CombatFunctionLibrary.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_CombatFunctionLibrary.h new file mode 100644 index 0000000..2efd3ad --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Utility/GCS_CombatFunctionLibrary.h @@ -0,0 +1,309 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCS_CombatEnumLibrary.h" +#include "GenericTeamAgentInterface.h" +#include "GCS_CombatStructLibrary.h" +#include "AbilitySystem/GCS_GameplayEffectContext.h" +#include "CombatFlow/GCS_AttackDefinition.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GCS_CombatFunctionLibrary.generated.h" + +class UGCS_AttackRequest_Base; +class IGCS_CombatTeamAgentInterface; +class IGCS_WeaponInterface; +class IGCS_CombatEntityInterface; + +/** + * Utility functions for combat-related operations. + * 战斗相关操作的实用函数。 + */ +UCLASS() +class GENERICCOMBATSYSTEM_API UGCS_CombatFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Gets the combat team agent interface from an actor or its components. + * 从Actor或其组件获取战斗队伍代理接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The combat team agent interface. 战斗队伍代理接口。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|CombatTeam", meta=(DefaultToSelf="Actor")) + static TScriptInterface GetCombatTeamAgentInterface(AActor* Actor); + + /** + * Finds the combat team agent interface on an actor or its components. + * 在Actor或其组件上查找战斗队伍代理接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @param OutInterface The found interface (output). 找到的接口(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|CombatTeam", meta=(DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindCombatTeamAgentInterface(AActor* Actor, TScriptInterface& OutInterface); + + /** + * Gets the combat interface from an actor or its components. + * 从Actor或其组件获取战斗接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The combat interface. 战斗接口。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS", meta=(DefaultToSelf="Actor")) + static TScriptInterface GetCombatEntityInterface(AActor* Actor); + + /** + * Gets the implementer(entity) of the combat interface. + * 获取战斗接口的实现者。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The object implementing the combat interface. 实现战斗接口的对象。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS", meta=(DefaultToSelf="Actor")) + static UObject* GetCombatEntity(AActor* Actor); + + /** + * Gets the weapon interface from an actor or its components. + * 从Actor或其组件获取武器接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The weapon interface. 武器接口。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS", meta=(DefaultToSelf="Actor")) + static TScriptInterface GetWeaponInterface(AActor* Actor); + + /** + * Gets the main skeletal mesh component from an actor. + * 从Actor获取主要骨骼网格组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param OverrideMeshLookupTag Optional override tag for lookup. 可选的覆盖查找标签。 + * @return The skeletal mesh component. 骨骼网格组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS", meta=(DefaultToSelf="Actor")) + static USkeletalMeshComponent* GetMainCharacterMeshComponent(AActor* Actor, FName OverrideMeshLookupTag = NAME_None); + + /** + * Gets the main mesh component from an actor. + * 从Actor获取主要网格组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param OverrideMeshLookupTag Optional override tag for lookup. 可选的覆盖查找标签。 + * @return The mesh component. 网格组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS", meta=(DefaultToSelf="Actor")) + static UMeshComponent* GetMainMeshComponent(AActor* Actor, FName OverrideMeshLookupTag = NAME_None); + + /** + * Gets socket names with a specified prefix from a component. + * 从组件获取具有指定前缀的插槽名称。 + * @param Component The component to query. 要查询的组件。 + * @param Prefix The prefix to match. 要匹配的前缀。 + * @param SearchCase The case sensitivity for the search. 搜索的大小写敏感性。 + * @return Array of matching socket names. 匹配的插槽名称数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCS") + static TArray GetSocketNamesWithPrefix(const USceneComponent* Component, FString Prefix, ESearchCase::Type SearchCase); + + /** + * Finds the combat interface on an actor or its components. + * 在Actor或其组件上查找战斗接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @param OutInterface The found interface (output). 找到的接口(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS", meta=(DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindCombatInterface(AActor* Actor, TScriptInterface& OutInterface); + + /** + * Finds the weapon interface on an actor or its components. + * 在Actor或其组件上查找武器接口。 + * @param Actor The actor to query. 要查询的Actor。 + * @param OutInterface The found interface (output). 找到的接口(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category = "GCS", meta=(DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindWeaponInterface(AActor* Actor, TScriptInterface& OutInterface); + + /** + * Calculates the angle between two actors. + * 计算两个Actor之间的角度。 + * @param From The source actor. 来源Actor。 + * @param To The target actor. 目标Actor。 + * @return The angle as a rotator. 角度(旋转器)。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static FRotator CalculateAngleBetweenActors(const AActor* From, const AActor* To); + + /** + * Check team relationship using CombatTeamAgentInterface. + * @param A The first actor. + * @param B The second actor. + * @return True if both actors are in the same team. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static bool IsSameCombatTeam(const AActor* A, const AActor* B); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static FGenericTeamId GetCombatTeamId(const AActor* Actor); + + static FGenericTeamId QueryCombatTeamId(const AActor* Actor, bool bCombatAgent = true, bool bGenericAgent = true); + + /** + * Determines the direction from an angle. + * 从角度确定方向。 + * @param Angle The input angle. 输入角度。 + * @return The direction enum. 方向枚举。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static EGCS_Direction CalculateDirectionFromAngle(const float Angle); + + /** + * Selects a montage based on direction. + * 根据方向选择蒙太奇。 + * @param Direction The direction enum. 方向枚举。 + * @param Montages Array of montages to choose from. 可选的蒙太奇数组。 + * @return The selected montage. 选中的蒙太奇。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCS") + static TSoftObjectPtr SelectMontageByDirection(EGCS_Direction Direction, TArray> Montages); + + /** + * Adds a tagged value to an array. + * 向数组添加标记值。 + * @param TaggedValues The tagged values array (modified). 标记值数组(修改)。 + * @param Tag The gameplay tag. 游戏标签。 + * @param ValueToAdd The value to add. 要添加的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS", meta=(DeprecatedFunction, DeprecationMessage="Use Set Tagged Value To Combat Payload.")) + static void AddTaggedValue(UPARAM(ref) + TArray& TaggedValues, FGameplayTag Tag, float ValueToAdd); + + /** + * Gets a tagged value from an array. + * 从数组获取标记值。 + * @param TaggedValues The tagged values array. 标记值数组。 + * @param Tag The gameplay tag to find. 要查找的游戏标签。 + * @return The associated value. 关联值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS", meta=(DeprecatedFunction, DeprecationMessage="Use Get Tagged Value From Combat Payload.")) + static float GetTaggedValue(const TArray TaggedValues, FGameplayTag Tag); + + /** + * Filters a gameplay tag container based on another container. + * 根据另一个容器过滤游戏标签容器。 + * @param TagContainer The container to filter. 要过滤的容器。 + * @param OtherContainer The container to filter against. 过滤依据的容器。 + * @return The filtered tag container. 过滤后的标签容器。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS") + static FGameplayTagContainer FilterGameplayTagContainer(const FGameplayTagContainer& TagContainer, FGameplayTagContainer OtherContainer); + + /** + * Adds an attack definition handle to a gameplay effect spec. + * 将攻击定义句柄添加到游戏效果规格。 + * @param SpecHandle The effect spec handle. 效果规格句柄。 + * @param AttackHandle The attack definition handle. 攻击定义句柄。 + * @return The modified effect spec handle. 修改后的效果规格句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS") + static FGameplayEffectSpecHandle AddAttackHandleToGameplayEffectSpec(FGameplayEffectSpecHandle SpecHandle, FDataTableRowHandle AttackHandle); + + /** + * Adds an attack definition to a gameplay effect spec. + * 将攻击定义添加到游戏效果规格。 + * @param SpecHandle The effect spec handle. 效果规格句柄。 + * @param AtkDefinition The attack definition. 攻击定义。 + * @return The modified effect spec handle. 修改后的效果规格句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS") + static FGameplayEffectSpecHandle AddAttackDefinitionToGameplayEffectSpec(FGameplayEffectSpecHandle SpecHandle, const FGCS_AttackDefinition& AtkDefinition); + + /** + * Adds an attack definition handle to a gameplay effect container spec. + * 将攻击定义句柄添加到游戏效果容器规格。 + * @param ContainerSpec The effect container spec. 效果容器规格。 + * @param AttackHandle The attack definition handle. 攻击定义句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GCS") + static void AddAttackHandleToGameplayEffectContainerSpec(FGGA_GameplayEffectContainerSpec ContainerSpec, FDataTableRowHandle AttackHandle); + + /** + * Sets the attack definition handle in an effect context. + * 在效果上下文中设置攻击定义句柄。 + * @param EffectContext The effect context. 效果上下文。 + * @param Handle The attack definition handle. 攻击定义句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "Ability|EffectContext", Meta = (DisplayName = "Set Attack Definition Handle")) + static void EffectContextSetAttackDefinitionHandle(FGameplayEffectContextHandle EffectContext, UPARAM(meta=(RowType="/Script/GenericCombatSystem.GCS_AttackDefinition")) + FDataTableRowHandle Handle); + + /** + * Gets the combat payload required by GCS within effect context. + * 获取效果上下文中,GCS所需的数据荷载。 + * @param EffectContext The effect context. 效果上下文。 + * @return The required data payload within GameplayEffectContext for GCS. GCS在GameplayEffectContext中所需的数据何在。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Ability|EffectContext", Meta = (DisplayName = "Get Combat Payload")) + static FGCS_ContextPayload_Combat EffectContextGetCombatPayload(FGameplayEffectContextHandle EffectContext); + + // Quicker access to Combat payload. + static FGCS_ContextPayload_Combat* EffectContextGetMutableCombatPayload(const FGameplayEffectContextHandle& EffectContext); + + /** + * Add single tag to the DynamicTags within combat payload. + * 获取战斗数据中的动态标签。 + * @param EffectContext The effect context. 效果上下文。 + * @param TagToAdd The tag to add. 要添加的标签。 + */ + UFUNCTION(BlueprintCallable, Category="Ability|EffectContext", Meta = (DisplayName = "Add Tag To Combat Payload")) + static void EffectContextAddTagToCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag TagToAdd); + + /** + * Shortcut to get the dynamic tags within combat payload. + * 获取战斗数据中的动态标签。 + * @param EffectContext The effect context. 效果上下文。 + * @param OutTags The dynamic tags. 动态标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Ability|EffectContext", meta=( DisplayName="Get DynamicTags From Combat Payload")) + static void EffectContextGetDynamicTagsFromCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTagContainer& OutTags); + + /** + * Sets a tagged value to combat payload + * 向战斗数据添加以标签关联的数值。 + * @param EffectContext The effect context. 效果上下文。 + * @param Tag The gameplay tag. 游戏标签。 + * @param NewValue The value to set. 要设置的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="Ability|EffectContext", meta=( DisplayName="Set Tagged Value To Combat Payload")) + static void EffectContextSetTaggedValueToCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag Tag, float NewValue); + + /** + * Gets a tagged value from combat payload. + * 从战斗数据获取以标签关联的数值 + * @param EffectContext The effect context. 效果上下文。 + * @param Tag The gameplay tag to find. 要查找的游戏标签。 + * @return The associated value,0 if not found. 关联值,没找到就返回0。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="Ability|EffectContext", meta=( DisplayName="Get Tagged Value From Combat Payload")) + static float EffectContextGetTaggedValueFromCombatPayload(FGameplayEffectContextHandle EffectContext, FGameplayTag Tag); + + /** + * Gets the attack definition handle from an effect context. + * 从效果上下文中获取攻击定义句柄。 + * @param EffectContext The effect context. 效果上下文。 + * @return The attack definition handle. 攻击定义句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Ability|EffectContext", Meta = (DisplayName = "Get Attack Definition Handle")) + static FDataTableRowHandle EffectContextGetAttackDefinitionHandle(FGameplayEffectContextHandle EffectContext); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Ability|EffectContext", Meta = (DisplayName = "Is Predicting Context")) + static bool EffectContextGetIsPredictingContext(FGameplayEffectContextHandle EffectContext); + + /** + * Gets the attack definition from an effect context. + * 从效果上下文中获取攻击定义。 + * @param EffectContext The effect context. 效果上下文。 + * @return The attack definition. 攻击定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Ability|EffectContext", Meta = (DisplayName = "Get Attack Definition")) + static FGCS_AttackDefinition EffectContextGetAttackDefinition(FGameplayEffectContextHandle EffectContext); +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_AttachmentRelationshipMapping.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_AttachmentRelationshipMapping.h new file mode 100644 index 0000000..a3d31d4 --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_AttachmentRelationshipMapping.h @@ -0,0 +1,84 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "GCS_AttachmentRelationshipMapping.generated.h" + +class USkeleton; +class USkeletalMesh; +class UStaticMesh; +class USkeletalMeshComponent; + +/** + * Deprecated!! Use SocketRelationshipMapping from GGS! + * 弃用了,使用GGS中的SocketRelationshipMapping。 + */ +USTRUCT(BlueprintType) +struct FGCS_AttachmentRelationship +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + TSoftObjectPtr StaticMesh; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + TSoftObjectPtr SkeletalMesh; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + FName SocketName{NAME_None}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + FTransform RelativeTransform; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category="GGS", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + + +/** + * Deprecated!! Use SocketRelationshipMapping from GGS! + * 弃用了,使用GGS中的SocketRelationshipMapping。 + */ +UCLASS(BlueprintType, Const) +class GENERICCOMBATSYSTEM_API UGCS_AttachmentRelationshipMapping : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * @param InSkeletalMeshComponent The parent skeletal mesh component that need to be attached to. + * @param InStaticMesh The static mesh you want to attach. + * @param InSkeletalMesh The skeletal mesh you want to attach. + * @param InSocketName The socket name you want to attach. + * @param OutRelationship The result attachment relationship. + * @return true if any matching found. + */ + UFUNCTION(BlueprintCallable,BlueprintPure=False, Category="GGS|Utilities",meta=(DeprecatedFunction,DeprecationMessage="Use SocketRelationshipMapping from GGS!")) + bool FindRelationshipForMesh(UPARAM(meta=(DisplayName="In Parent Mesh")) const USkeletalMeshComponent* InSkeletalMeshComponent, const UStaticMesh* InStaticMesh, const USkeletalMesh* InSkeletalMesh, FName InSocketName, + FGCS_AttachmentRelationship& OutRelationship) const; + + /** + * Will restrict this mapping to CompatibleSkeletons, or no restriction if left empty. + */ + UPROPERTY(EditAnywhere, Category="GGS") + TArray> CompatibleSkeletons; + + UPROPERTY(EditAnywhere, Category="GGS") + TArray CompatibleSkeletonNames; + + UPROPERTY(EditAnywhere, Category="GGS") + bool bUseNameMatching{true}; + + UPROPERTY(EditAnywhere, Category="GGS", meta=(TitleProperty="EditorFriendlyName")) + TArray Relationships; + + +#if WITH_EDITOR + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponActor.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponActor.h new file mode 100644 index 0000000..ed118db --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponActor.h @@ -0,0 +1,217 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagAssetInterface.h" +#include "GCS_CombatStructLibrary.h" +#include "GCS_WeaponInterface.h" +#include "Collision/GCS_TraceStructLibrary.h" +#include "GameFramework/Actor.h" +#include "GCS_WeaponActor.generated.h" + +/** + * Delegate for weapon active state changes. + * 武器激活状态更改的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGCS_WeaponActiveStateChangedSignature, bool, bIsActive); + +/** + * Default implementation of the weapon interface as an actor. + * 作为Actor的武器接口默认实现。 + * @note Extend this class for custom weapon logic. 扩展此类以实现自定义武器逻辑。 + */ +UCLASS(BlueprintType, Blueprintable, Abstract, ClassGroup=(GCS)) +class GENERICCOMBATSYSTEM_API AGCS_WeaponActor : public AActor, public IGCS_WeaponInterface, public IGameplayTagAssetInterface +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + AGCS_WeaponActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Gets the pawn owning this weapon. + * 获取拥有此武器的Pawn。 + * @return The owning pawn. 所属Pawn。 + */ + virtual APawn* GetWeaponOwner_Implementation() const override; + + /** + * Gets the gameplay tags associated with the weapon. + * 获取与武器关联的游戏标签。 + * @return The weapon's gameplay tags. 武器游戏标签。 + */ + virtual const FGameplayTagContainer GetWeaponTags_Implementation() const override; + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Sets the weapon's active state. + * 设置武器的激活状态。 + * @param bNewActive The new active state. 新激活状态。 + */ + virtual void SetWeaponActive_Implementation(bool bNewActive) override; + + /** + * Checks if the weapon is active. + * 检查武器是否激活。 + * @return True if the weapon is active. 如果武器激活返回true。 + */ + virtual bool IsWeaponActive_Implementation() const override; + + /** + * Gets the main primitive component of the weapon. + * 获取武器的主要原始组件。 + * @return The primitive component. 原始组件。 + */ + virtual UPrimitiveComponent* GetPrimitiveComponent_Implementation() const override; + + /** + * Gets the owned gameplay tags. + * 获取拥有的游戏标签。 + * @param TagContainer The gameplay tag container (output). 游戏标签容器(输出)。 + */ + virtual void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const override; + +protected: + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Handles weapon active state changes. + * 处理武器激活状态变化。 + * @param Prev The previous active state. 之前的激活状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Weapon") + void OnWeaponActiveStateChanged(bool Prev); + + /** + * Refreshes trace instances and registers/unregisters trace events. + * 刷新碰撞检测实例并注册/取消注册碰撞事件。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|WeaponTrace", meta=(BlueprintProtected)) + void RefreshTraceInstance(); + virtual void RefreshTraceInstance_Implementation(); + + /** + * Allow you to customize the source object used for weapon traces. + * 允许你自定义用于武器碰撞检测的源对象。 + * @note The source object is the weapon itself by default. 默认是武器本身就是源对象. + * @return The object used as source object for weapon trace. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|WeaponTrace", meta=(BlueprintProtected)) + UObject* GetSourceObjectForTrace(); + virtual UObject* GetSourceObjectForTrace_Implementation(); + + /** + * Allow you to customize the source component used for different weapon traces. + * 允许你自定义用于不同武器碰撞检测的源组件。 + * @note The source component is the weapon primitive component by default. 默认是武器的PrimitiveComponent就是源组件. + * @return The component used as source component for weapon trace. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|WeaponTrace", meta=(BlueprintProtected)) + UPrimitiveComponent* GetSourceComponentForTrace(const FGameplayTag& TraceTag) const; + virtual UPrimitiveComponent* GetSourceComponentForTrace_Implementation(const FGameplayTag& TraceTag) const; + + /** + * Handles weapon trace hits. + * 处理武器碰撞命中。 + * @param TraceHandle The collision trace instance. 碰撞检测实例。 + * @param HitResult The hit result. 命中结果。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|WeaponTrace") + void OnAnyTraceHit(const FGCS_TraceHandle& TraceHandle, const FHitResult& HitResult); + + /** + * Handles trace state changes. + * 处理碰撞状态变化。 + * @param TraceHandle The collision trace instance. 碰撞检测实例。 + * @param NewState The new state. 新状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|WeaponTrace") + void OnAnyTraceStateChanged(const FGCS_TraceHandle& TraceHandle, bool NewState); + + /** + * Delegate for weapon active state changes. + * 武器激活状态更改的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGCS_WeaponActiveStateChangedSignature OnWeaponActiveStateChangedEvent; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaSeconds Time since last frame. 上一帧以来的时间。 + */ + virtual void Tick(float DeltaSeconds) override; + +protected: + /** + * Gameplay tags for the weapon. + * 武器的游戏标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "WeaponSetting") + FGameplayTagContainer WeaponTags; + + /** + * List of collision trace settings created when the weapon is activated. + * 武器激活时创建的碰撞检测设置列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "WeaponSetting|Trace") + TArray TraceDefinitions; + + /** + * Tag name for looking up the mesh component. + * 查找网格组件的标签名称。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="WeaponSetting") + FName WeaponMeshTagName{TEXT("WeaponMesh")}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="WeaponSetting", meta=(RequiredAssetDataTags = "RowStructure=/Script/GenericCombatSystem.GCS_ComboDefinition")) + bool bGiveAbilitiesFromComboDefinitionTable{true}; + + /** + * The combo definition table associated with this weapon. + * 与此武器关联的连击定义表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="WeaponSetting", meta=(RequiredAssetDataTags = "RowStructure=/Script/GenericCombatSystem.GCS_ComboDefinition")) + TObjectPtr ComboDefinitionTable{nullptr}; + + /** + * Traces associated with this weapon. + * 与该武器关联的碰撞检测. + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="WeaponState") + TArray TraceHandles; + + /** + * Indicates if the weapon is active. + * 表示武器是否激活。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing = OnWeaponActiveStateChanged, Category = "WeaponState") + bool bWeaponActive; + +#if WITH_EDITOR + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponInterface.h b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponInterface.h new file mode 100644 index 0000000..7bdcf9c --- /dev/null +++ b/Plugins/GCS/Source/GenericCombatSystem/Public/Weapon/GCS_WeaponInterface.h @@ -0,0 +1,96 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "UObject/Interface.h" +#include "GCS_WeaponInterface.generated.h" + +class APawn; +class AActor; + +/** + * Interface for objects acting as weapons. + * 作为武器的对象的接口。 + */ +UINTERFACE() +class UGCS_WeaponInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for weapon-related functionality. + * 武器相关功能的接口。 + */ +class GENERICCOMBATSYSTEM_API IGCS_WeaponInterface +{ + GENERATED_BODY() + +public: + /** + * Gets the pawn owning this weapon. + * 获取拥有此武器的Pawn。 + * @return The owning pawn. 所属Pawn。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon") + APawn* GetWeaponOwner() const; + + /** + * Gets the gameplay tags associated with the weapon. + * 获取与武器关联的游戏标签。 + * @return The weapon's gameplay tags. 武器游戏标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon") + const FGameplayTagContainer GetWeaponTags() const; + + /** + * Sets the weapon's active state. + * 设置武器的激活状态。 + * @param bNewActive The new active state. 新激活状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Weapon") + void SetWeaponActive(bool bNewActive); + + /** + * Checks if the weapon is active. + * 检查武器是否激活。 + * @return True if the weapon is active. 如果武器激活返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Weapon") + bool IsWeaponActive() const; + + /** + * Gets the main primitive component of the weapon. + * 获取武器的主要原始组件。 + * @return The primitive component. 原始组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GCS|Weapon") + UPrimitiveComponent* GetPrimitiveComponent() const; + virtual UPrimitiveComponent* GetPrimitiveComponent_Implementation() const; + + /** + * Gets the targeting start transform for ranged weapons. + * 获取远程武器的目标起始变换。 + * @return The targeting start transform. 目标起始变换。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon", meta=(DisplayName="Get Targeting Start Transform")) + FTransform GCS_GetTargetingStartTransform() const; + + /** + * Toggles targeting for the weapon. + * 切换武器的目标状态。 + * @param bEnable Whether to enable targeting. 是否启用目标。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon", meta=(DisplayName="Toggle Targeting")) + void GCS_ToggleTargeting(bool bEnable); + + /** + * Toggles trails for the weapon. + * 开关武器拖尾。 + * @param bEnable Whether to enable trails. 是否启用武器拖尾 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GCS|Weapon", meta=(DisplayName="Toggle Trail")) + void ToggleTrail(bool bEnable); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/GenericGameplayAbilities.Build.cs b/Plugins/GCS/Source/GenericGameplayAbilities/GenericGameplayAbilities.Build.cs new file mode 100644 index 0000000..654299d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/GenericGameplayAbilities.Build.cs @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +using System.IO; +using UnrealBuildTool; + +public class GenericGameplayAbilities : ModuleRules +{ + public GenericGameplayAbilities(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new[] + { + Path.Combine(ModuleDirectory, "Public"), + Path.Combine(ModuleDirectory, "Public/Globals"), + Path.Combine(ModuleDirectory, "Public/Abilities") + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] + { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new[] + { + "Core", "GameplayAbilities", "GameplayTags", "ModularGameplay", + "GameplayTasks", "DeveloperSettings", "EnhancedInput" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", "NetCore", + "Engine", "EnhancedInput", "TargetingSystem" + + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilityCost.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilityCost.cpp new file mode 100644 index 0000000..c54e243 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilityCost.cpp @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Abilities/GGA_AbilityCost.h" + +bool UGGA_AbilityCost::CheckCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, + FGameplayTagContainer* OptionalRelevantTags) const +{ + return BlueprintCheckCost(Ability, Handle, *ActorInfo, *OptionalRelevantTags); +} + +void UGGA_AbilityCost::ApplyCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, + const FGameplayAbilityActivationInfo ActivationInfo) +{ + return BlueprintApplyCost(Ability, Handle, *ActorInfo, ActivationInfo); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilitySet.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilitySet.cpp new file mode 100644 index 0000000..f8d2ad7 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_AbilitySet.cpp @@ -0,0 +1,350 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Abilities/GGA_AbilitySet.h" +#include "Abilities/GameplayAbility.h" +#include "Runtime/Launch/Resources/Version.h" +#include "AbilitySystemComponent.h" +#include "Utilities/GGA_AbilitySystemFunctionLibrary.h" + +DEFINE_LOG_CATEGORY(LogGGA_AbilitySet) + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GGA_AbilitySet) + +void FGGA_AbilitySet_GrantedHandles::AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle) +{ + if (Handle.IsValid()) + { + AbilitySpecHandles.Add(Handle); + } +} + +void FGGA_AbilitySet_GrantedHandles::AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle) +{ + if (Handle.IsValid()) + { + GameplayEffectHandles.Add(Handle); + } +} + +void FGGA_AbilitySet_GrantedHandles::AddAttributeSet(UAttributeSet* Set) +{ + GrantedAttributeSets.Add(Set); +} + +void FGGA_AbilitySet_GrantedHandles::TakeFromAbilitySystem(UAbilitySystemComponent* ASC) +{ + check(ASC); + + if (!ASC->IsOwnerActorAuthoritative()) + { + // Must be authoritative to give or take ability sets. + return; + } + + for (const FGameplayAbilitySpecHandle& Handle : AbilitySpecHandles) + { + if (Handle.IsValid()) + { + ASC->ClearAbility(Handle); + } + } + + for (const FActiveGameplayEffectHandle& Handle : GameplayEffectHandles) + { + if (Handle.IsValid()) + { + ASC->RemoveActiveGameplayEffect(Handle); + } + } + + for (UAttributeSet* Set : GrantedAttributeSets) + { + ASC->RemoveSpawnedAttribute(Set); + } + + AbilitySpecHandles.Reset(); + GameplayEffectHandles.Reset(); + GrantedAttributeSets.Reset(); +} + +UGGA_AbilitySet::UGGA_AbilitySet(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UGGA_AbilitySet::GiveToAbilitySystem(UAbilitySystemComponent* ASC, FGGA_AbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject, int32 OverrideLevel) const +{ + check(ASC); + + if (!ASC->IsOwnerActorAuthoritative()) + { + // Must be authoritative to give or take ability sets. + return; + } + + // Grant the attribute sets. + for (int32 SetIndex = 0; SetIndex < GrantedAttributes.Num(); ++SetIndex) + { + const FGGA_AbilitySet_AttributeSet& SetToGrant = GrantedAttributes[SetIndex]; + + const TSubclassOf AttributeSetClass = SetToGrant.AttributeSet.LoadSynchronous(); + + if (!AttributeSetClass) + { + UE_LOG(LogGGA_AbilitySet, Error, TEXT("GrantedAttributes[%d] on ability set [%s]: AttributeSet is not valid"), SetIndex, *GetNameSafe(this)); + continue; + } + + if (UAttributeSet* ExistingOne = UGGA_AbilitySystemFunctionLibrary::GetAttributeSetByClass(ASC, AttributeSetClass)) + { + UE_LOG(LogGGA_AbilitySet, Error, TEXT("GrantedAttributes[%d] on ability set [%s]: AttributeSet already exists."), SetIndex, *GetNameSafe(this)); + continue; + } + + +#if WITH_EDITORONLY_DATA + if (!SetToGrant.bAttributeSetEnabled) + { + UE_LOG(LogGGA_AbilitySet, Display, TEXT( "GrantedAttributes[%d] on ability set [%s]:skipped for debugging."), SetIndex, *GetNameSafe(this)); + continue; + } +#endif + + UAttributeSet* NewSet = NewObject(ASC->GetOwner(), AttributeSetClass); + ASC->AddAttributeSetSubobject(NewSet); + + if (OutGrantedHandles) + { + OutGrantedHandles->AddAttributeSet(NewSet); + } + } + + // Grant the gameplay abilities. + for (int32 AbilityIndex = 0; AbilityIndex < GrantedGameplayAbilities.Num(); ++AbilityIndex) + { + const FGGA_AbilitySet_GameplayAbility& AbilityToGrant = GrantedGameplayAbilities[AbilityIndex]; + + const TSubclassOf AbilityClass = AbilityToGrant.Ability.LoadSynchronous(); + + if (!AbilityClass) + { + UE_LOG(LogGGA_AbilitySet, Error, TEXT("GrantedGameplayAbilities[%d] on ability set [%s]: Ability class is not valid."), AbilityIndex, *GetNameSafe(this)); + continue; + } + +#if WITH_EDITORONLY_DATA + if (!AbilityToGrant.bAbilityEnabled) + { + UE_LOG(LogGGA_AbilitySet, Display, TEXT( "GrantedGameplayAbilities[%d] on ability set [%s]: Skipped for debugging."), AbilityIndex, *GetNameSafe(this)); + continue; + } +#endif + + + UGameplayAbility* AbilityCDO = AbilityClass->GetDefaultObject(); + + FGameplayAbilitySpec AbilitySpec(AbilityCDO, OverrideLevel > 0 ? OverrideLevel : AbilityToGrant.AbilityLevel); + AbilitySpec.SourceObject = SourceObject; + + if (AbilityToGrant.InputID > 0) + { + AbilitySpec.InputID = AbilityToGrant.InputID; + } + + if (!AbilityToGrant.DynamicTags.IsEmpty()) + { +#if ENGINE_MINOR_VERSION > 4 + AbilitySpec.GetDynamicSpecSourceTags().AppendTags(AbilityToGrant.DynamicTags); +#else + AbilitySpec.DynamicAbilityTags.AppendTags(AbilityToGrant.DynamicTags); +#endif + } + + const FGameplayAbilitySpecHandle AbilitySpecHandle = ASC->GiveAbility(AbilitySpec); + + if (OutGrantedHandles) + { + OutGrantedHandles->AddAbilitySpecHandle(AbilitySpecHandle); + } + } + + // Grant the gameplay effects. + for (int32 EffectIndex = 0; EffectIndex < GrantedGameplayEffects.Num(); ++EffectIndex) + { + const FGGA_AbilitySet_GameplayEffect& EffectToGrant = GrantedGameplayEffects[EffectIndex]; + const TSubclassOf EffectClass = EffectToGrant.GameplayEffect.LoadSynchronous(); + + if (!EffectClass) + { + UE_LOG(LogGGA_AbilitySet, Error, TEXT("GrantedGameplayEffects[%d] on ability set [%s]:Effect Class is not valid"), EffectIndex, *GetNameSafe(this)); + continue; + } + +#if WITH_EDITORONLY_DATA + if (!EffectToGrant.bEffectEnabled) + { + UE_LOG(LogGGA_AbilitySet, Display, TEXT( "GrantedGameplayEffects[%d] on ability set [%s]:Skipped for debugging."), EffectIndex, *GetNameSafe(this)); + continue; + } +#endif + + const UGameplayEffect* GameplayEffectCDO = EffectClass->GetDefaultObject(); + + const FActiveGameplayEffectHandle GameplayEffectHandle = ASC-> + ApplyGameplayEffectToSelf(GameplayEffectCDO, OverrideLevel > 0 ? OverrideLevel : EffectToGrant.EffectLevel, ASC->MakeEffectContext()); + if (OutGrantedHandles) + { + OutGrantedHandles->AddGameplayEffectHandle(GameplayEffectHandle); + if (GameplayEffectCDO->DurationPolicy == EGameplayEffectDurationType::Infinite && !GameplayEffectHandle.IsValid()) + { + UE_LOG(LogGGA_AbilitySet, Warning, TEXT("Granted Infinite GameplayEffects[%d] on ability set [%s] failed to apply"), EffectIndex, *GetNameSafe(this)); + } + } + } +} + +FGGA_AbilitySet_GrantedHandles UGGA_AbilitySet::GiveAbilitySetToAbilitySystem(TSoftObjectPtr AbilitySet, UAbilitySystemComponent* ASC, UObject* SourceObject, int32 OverrideLevel) +{ + FGGA_AbilitySet_GrantedHandles GrantedHandles; + if (IsValid(ASC) && !AbilitySet.IsNull()) + { + if (!AbilitySet.IsValid()) + { + AbilitySet.LoadSynchronous(); + } + AbilitySet->GiveToAbilitySystem(ASC, &GrantedHandles, SourceObject, OverrideLevel); + } + return GrantedHandles; +} + +TArray UGGA_AbilitySet::GiveAbilitySetsToAbilitySystem(TArray> AbilitySets, UAbilitySystemComponent* ASC, UObject* SourceObject, + int32 OverrideLevel) +{ + TArray Handles; + for (auto& AbilitySet : AbilitySets) + { + Handles.Add(GiveAbilitySetToAbilitySystem(AbilitySet, ASC, SourceObject, OverrideLevel)); + } + return Handles; +} + +void UGGA_AbilitySet::TakeAbilitySetFromAbilitySystem(FGGA_AbilitySet_GrantedHandles& GrantedHandles, UAbilitySystemComponent* ASC) +{ + if (IsValid(ASC)) + { + GrantedHandles.TakeFromAbilitySystem(ASC); + } +} + +void UGGA_AbilitySet::TakeAbilitySetsFromAbilitySystem(TArray& GrantedHandles, UAbilitySystemComponent* ASC) +{ + if (IsValid(ASC)) + { + for (FGGA_AbilitySet_GrantedHandles& Handle : GrantedHandles) + { + Handle.TakeFromAbilitySystem(ASC); + } + } +} + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void FGGA_AbilitySet_GameplayAbility::MakeEditorFriendlyName() +{ + EditorFriendlyName = "Empty Ability"; + + if (!Ability.IsNull()) + { + if (TSubclassOf Loaded = Ability.LoadSynchronous()) + { + EditorFriendlyName = Loaded->GetDisplayNameText().ToString(); + } + } +} + +void FGGA_AbilitySet_GameplayEffect::MakeEditorFriendlyName() +{ + EditorFriendlyName = "Empty Effect"; + + if (!GameplayEffect.IsNull()) + { + if (TSubclassOf Loaded = GameplayEffect.LoadSynchronous()) + { + EditorFriendlyName = Loaded->GetDisplayNameText().ToString(); + } + } +} + +void FGGA_AbilitySet_AttributeSet::MakeEditorFriendlyName() +{ + EditorFriendlyName = "Empty Attribute Set"; + + if (!AttributeSet.IsNull()) + { + if (TSubclassOf Loaded = AttributeSet.LoadSynchronous()) + { + EditorFriendlyName = Loaded->GetDisplayNameText().ToString(); + } + } +} + +void UGGA_AbilitySet::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + if (!IsRunningCommandlet()) + { + for (auto& Ability : GrantedGameplayAbilities) + { + Ability.MakeEditorFriendlyName(); + } + for (auto& Effect : GrantedGameplayEffects) + { + Effect.MakeEditorFriendlyName(); + } + + for (auto& Attribute : GrantedAttributes) + { + Attribute.MakeEditorFriendlyName(); + } + } +} + +void UGGA_AbilitySet::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); +} + +void UGGA_AbilitySet::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) +{ + FName MemberName = PropertyChangedEvent.PropertyChain.GetActiveMemberNode()->GetValue()->GetFName(); + if (PropertyChangedEvent.GetPropertyName() == TEXT("Ability") && MemberName == GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedGameplayAbilities)) + { + const int32 Index = PropertyChangedEvent.GetArrayIndex(GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedGameplayAbilities).ToString()); + if (Index != INDEX_NONE) + { + GrantedGameplayAbilities[Index].MakeEditorFriendlyName(); + } + } + + if (PropertyChangedEvent.GetPropertyName() == TEXT("GameplayEffect") && MemberName == GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedGameplayEffects)) + { + const int32 Index = PropertyChangedEvent.GetArrayIndex(GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedGameplayEffects).ToString()); + if (Index != INDEX_NONE) + { + GrantedGameplayEffects[Index].MakeEditorFriendlyName(); + } + } + + if (PropertyChangedEvent.GetPropertyName() == TEXT("AttributeSet") && MemberName == GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedAttributes)) + { + const int32 Index = PropertyChangedEvent.GetArrayIndex(GET_MEMBER_NAME_CHECKED(UGGA_AbilitySet, GrantedGameplayEffects).ToString()); + if (Index != INDEX_NONE) + { + GrantedAttributes[Index].MakeEditorFriendlyName(); + } + } + + Super::PostEditChangeChainProperty(PropertyChangedEvent); +} +#endif diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbility.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbility.cpp new file mode 100644 index 0000000..657083d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbility.cpp @@ -0,0 +1,651 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Abilities/GGA_GameplayAbility.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemGlobals.h" +#include "AbilitySystemLog.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Abilities/GGA_AbilityCost.h" +#include "GGA_AbilitySystemComponent.h" +#include "GGA_GameplayTags.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/Controller.h" +#include "GameFramework/Pawn.h" +#include "GGA_LogChannels.h" +#include "Misc/DataValidation.h" +#include "Utilities/GGA_GameplayEffectContainerFunctionLibrary.h" + +#define ENSURE_ABILITY_IS_INSTANTIATED_OR_RETURN(FunctionName, ReturnValue) \ + { \ + if (!ensure(IsInstantiated())) \ + { \ + ABILITY_LOG(Error, TEXT("%s: " #FunctionName " cannot be called on a non-instanced ability. Check the instancing policy."), *GetPathName()); \ + return ReturnValue; \ + } \ + } + +UGGA_GameplayAbility::UGGA_GameplayAbility(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateNo; + bServerRespectsRemoteAbilityCancellation = false; + + InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor; + NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted; + NetSecurityPolicy = EGameplayAbilityNetSecurityPolicy::ClientOrServer; + + ActivationGroup = EGGA_AbilityActivationGroup::Independent; + + bReplicateInputDirectly = false; + + bEnableTick = false; +} + +void UGGA_GameplayAbility::Tick(float DeltaTime) +{ + AbilityTick(DeltaTime); +} + +TStatId UGGA_GameplayAbility::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UGGA_GameplayAbility, STATGROUP_GameplayAbility); +} + +bool UGGA_GameplayAbility::IsTickable() const +{ + return IsInstantiated() && bEnableTick && GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor && IsActive(); +} + +void UGGA_GameplayAbility::AbilityTick_Implementation(float DeltaTime) +{ +} + +AController* UGGA_GameplayAbility::GetControllerFromActorInfo() const +{ + if (CurrentActorInfo) + { + if (AController* PC = CurrentActorInfo->PlayerController.Get()) + { + return PC; + } + + // Look for a player controller or pawn in the owner chain. + AActor* TestActor = CurrentActorInfo->OwnerActor.Get(); + while (TestActor) + { + if (AController* C = Cast(TestActor)) + { + return C; + } + + if (APawn* Pawn = Cast(TestActor)) + { + return Pawn->GetController(); + } + + TestActor = TestActor->GetOwner(); + } + } + + return nullptr; +} + +void UGGA_GameplayAbility::SetActivationGroup(EGGA_AbilityActivationGroup NewGroup) +{ + ActivationGroup = NewGroup; +} + +void UGGA_GameplayAbility::TryActivateAbilityOnSpawn(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) const +{ + PRAGMA_DISABLE_DEPRECATION_WARNINGS + +#if ENGINE_MINOR_VERSION > 4 + // Fixing this up to use the instance activation, but this function should be deprecated as it cannot work with InstancedPerExecution + UE_CLOG(Spec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerExecution, LogAbilitySystem, Warning, + TEXT("%hs: %s is InstancedPerExecution. This is unreliable for Input as you may only interact with the latest spawned Instance"), __func__, *GetNameSafe(Spec.Ability)); + TArray Instances = Spec.GetAbilityInstances(); + const FGameplayAbilityActivationInfo& ActivationInfo = Instances.IsEmpty() ? Spec.ActivationInfo : Instances.Last()->GetCurrentActivationInfoRef(); + const bool bIsPredicting = (ActivationInfo.ActivationMode == EGameplayAbilityActivationMode::Predicting); +#else + const bool bIsPredicting = (Spec.ActivationInfo.ActivationMode == EGameplayAbilityActivationMode::Predicting); +#endif + PRAGMA_ENABLE_DEPRECATION_WARNINGS + + // Try to activate if activation policy is on spawn. +#if ENGINE_MINOR_VERSION > 4 + if (ActorInfo && !Spec.IsActive() && !bIsPredicting && GetAssetTags().HasTagExact(GGA_AbilityTraitTags::ActivationOnSpawn)) +#else + if (ActorInfo && !Spec.IsActive() && !bIsPredicting && AbilityTags.HasTagExact(GGA_AbilityTraitTags::ActivationOnSpawn)) +#endif + { + UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get(); + const AActor* AvatarActor = ActorInfo->AvatarActor.Get(); + + // If avatar actor is torn off or about to die, don't try to activate until we get the new one. + if (ASC && AvatarActor && !AvatarActor->GetTearOff() && (AvatarActor->GetLifeSpan() <= 0.0f)) + { + const bool bIsLocalExecution = (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::LocalPredicted) || (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::LocalOnly); + const bool bIsServerExecution = (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::ServerOnly) || (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::ServerInitiated); + + const bool bClientShouldActivate = ActorInfo->IsLocallyControlled() && bIsLocalExecution; + const bool bServerShouldActivate = ActorInfo->IsNetAuthority() && bIsServerExecution; + + if (bClientShouldActivate || bServerShouldActivate) + { + ASC->TryActivateAbility(Spec.Handle); + } + } + } +} + +void UGGA_GameplayAbility::HandleActivationFailed(const FGameplayTagContainer& FailedReason) const +{ + OnActivationFailed(FailedReason); +} + +bool UGGA_GameplayAbility::HasEffectContainer(FGameplayTag ContainerTag) +{ + return EffectContainerMap.Contains(ContainerTag); +} + +FGGA_GameplayEffectContainerSpec UGGA_GameplayAbility::MakeEffectContainerSpec(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel) +{ + FGGA_GameplayEffectContainer* FoundContainer = EffectContainerMap.Find(ContainerTag); + + if (FoundContainer) + { + return UGGA_GameplayEffectContainerFunctionLibrary::MakeEffectContainerSpec(*FoundContainer, EventData, OverrideGameplayLevel, this); + } + return FGGA_GameplayEffectContainerSpec(); +} + +TArray UGGA_GameplayAbility::ApplyEffectContainer(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel) +{ + FGGA_GameplayEffectContainer* FoundContainer = EffectContainerMap.Find(ContainerTag); + + if (FoundContainer) + { + const FGGA_GameplayEffectContainerSpec Spec = UGGA_GameplayEffectContainerFunctionLibrary::MakeEffectContainerSpec(*FoundContainer, EventData, OverrideGameplayLevel, this); + return UGGA_GameplayEffectContainerFunctionLibrary::ApplyEffectContainerSpec(this, Spec); + } + return TArray(); +} + +void UGGA_GameplayAbility::PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData) +{ + Super::PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData); + UAbilitySystemComponent* Comp = ActorInfo->AbilitySystemComponent.Get(); + + for (const FGGA_GameplayTagCount& TagCount : ActivationOwnedLooseTags) + { + Comp->AddLooseGameplayTag(TagCount.Tag, TagCount.Count); + } +} + +void UGGA_GameplayAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + bool bReplicateEndAbility, bool bWasCancelled) +{ + if (IsEndAbilityValid(Handle, ActorInfo)) + { + if (UAbilitySystemComponent* Comp = ActorInfo->AbilitySystemComponent.Get()) + { + for (const FGGA_GameplayTagCount& TagCount : ActivationOwnedLooseTags) + { + Comp->RemoveLooseGameplayTag(TagCount.Tag, TagCount.Count); + } + } + } + + Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); +} + +void UGGA_GameplayAbility::OnActivationFailed_Implementation(const FGameplayTagContainer& FailedReason) const +{ +} + +bool UGGA_GameplayAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, + const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const +{ + if (!ActorInfo || !ActorInfo->AbilitySystemComponent.IsValid()) + { + return false; + } + + if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags)) + { + return false; + } + + //@TODO Possibly remove after setting up tag relationships + UGGA_AbilitySystemComponent* GASC = CastChecked(ActorInfo->AbilitySystemComponent.Get()); + if (GASC->IsActivationGroupBlocked(ActivationGroup)) + { + if (OptionalRelevantTags) + { + OptionalRelevantTags->AddTag(GGA_AbilityActivateFailTags::ActivationGroup); + } + return false; + } + + return true; +} + +void UGGA_GameplayAbility::SetCanBeCanceled(bool bCanBeCanceled) +{ + // The ability can not block canceling if it's replaceable. + if (!bCanBeCanceled && (ActivationGroup == EGGA_AbilityActivationGroup::Exclusive_Replaceable)) + { + UE_LOG(LogGGA_Ability, Error, TEXT("SetCanBeCanceled: Ability [%s] can not block canceling because its activation group is replaceable."), *GetName()); + return; + } + + Super::SetCanBeCanceled(bCanBeCanceled); +} + +void UGGA_GameplayAbility::OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) +{ + Super::OnGiveAbility(ActorInfo, Spec); + + K2_OnGiveAbility(); + + TryActivateAbilityOnSpawn(ActorInfo, Spec); +} + +void UGGA_GameplayAbility::OnRemoveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) +{ + K2_OnRemoveAbility(); + + Super::OnRemoveAbility(ActorInfo, Spec); +} + +void UGGA_GameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) +{ + Super::OnAvatarSet(ActorInfo, Spec); + K2_OnAvatarSet(); +} + +bool UGGA_GameplayAbility::ShouldActivateAbility(ENetRole Role) const +{ + return K2_ShouldActivateAbility(Role) && Super::ShouldActivateAbility(Role); + // Don't violate security policy if we're not the server +} + +bool UGGA_GameplayAbility::K2_ShouldActivateAbility_Implementation(ENetRole Role) const +{ + return true; +} + +void UGGA_GameplayAbility::InputPressed(const FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + const FGameplayAbilityActivationInfo ActivationInfo) +{ + Super::InputPressed(Handle, ActorInfo, ActivationInfo); + K2_OnInputPressed(Handle, *ActorInfo, ActivationInfo); +} + +void UGGA_GameplayAbility::InputReleased(const FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo* ActorInfo, + const FGameplayAbilityActivationInfo ActivationInfo) +{ + Super::InputReleased(Handle, ActorInfo, ActivationInfo); + K2_OnInputReleased(Handle, *ActorInfo, ActivationInfo); +} + +bool UGGA_GameplayAbility::IsInputPressed() const +{ + FGameplayAbilitySpec* Spec = GetCurrentAbilitySpec(); + return Spec && Spec->InputPressed; +} + +bool UGGA_GameplayAbility::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately) +{ + UGGA_AbilitySystemComponent* ASC = Cast(GetAbilitySystemComponentFromActorInfo()); + if (ASC) + { + return ASC->BatchRPCTryActivateAbility(InAbilityHandle, EndAbilityImmediately); + } + return false; +} + +void UGGA_GameplayAbility::ExternalEndAbility() +{ + check(CurrentActorInfo); + + bool bReplicateEndAbility = true; + bool bWasCancelled = false; + EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled); +} + +bool UGGA_GameplayAbility::CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, + OUT FGameplayTagContainer* OptionalRelevantTags) const +{ + if (!Super::CheckCost(Handle, ActorInfo, OptionalRelevantTags)) + { + return false; + } + + if (!K2_OnCheckCost(Handle, *ActorInfo)) + { + return false; + } + for (TObjectPtr AdditionalCost : AdditionalCosts) + { + if (AdditionalCost != nullptr) + { + if (!AdditionalCost->CheckCost(this, Handle, ActorInfo, OptionalRelevantTags)) + { + return false; + } + } + } + return true; +} + +bool UGGA_GameplayAbility::K2_OnCheckCost_Implementation(const FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo& ActorInfo) const +{ + return true; +} + +void UGGA_GameplayAbility::ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, + const FGameplayAbilityActivationInfo ActivationInfo) const +{ + Super::ApplyCost(Handle, ActorInfo, ActivationInfo); + + check(ActorInfo); + + K2_OnApplyCost(Handle, *ActorInfo, ActivationInfo); + + // Used to determine if the ability actually hit a target (as some costs are only spent on successful attempts) + auto DetermineIfAbilityHitTarget = [&]() + { + if (ActorInfo->IsNetAuthority()) + { + if (UGGA_AbilitySystemComponent* ASC = Cast(ActorInfo->AbilitySystemComponent.Get())) + { + FGameplayAbilityTargetDataHandle TargetData; + ASC->GetAbilityTargetData(Handle, ActivationInfo, TargetData); + for (int32 TargetDataIdx = 0; TargetDataIdx < TargetData.Data.Num(); ++TargetDataIdx) + { + if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetData, TargetDataIdx)) + { + return true; + } + } + } + } + + return false; + }; + + // Pay any additional costs + bool bAbilityHitTarget = false; + bool bHasDeterminedIfAbilityHitTarget = false; + for (TObjectPtr AdditionalCost : AdditionalCosts) + { + if (AdditionalCost != nullptr) + { + if (AdditionalCost->ShouldOnlyApplyCostOnHit()) + { + if (!bHasDeterminedIfAbilityHitTarget) + { + bAbilityHitTarget = DetermineIfAbilityHitTarget(); + bHasDeterminedIfAbilityHitTarget = true; + } + + if (!bAbilityHitTarget) + { + continue; + } + } + + AdditionalCost->ApplyCost(this, Handle, ActorInfo, ActivationInfo); + } + } +} + +UGameplayEffect* UGGA_GameplayAbility::GetCostGameplayEffect() const +{ + if (TSubclassOf GE = K2_GetCostGameplayEffect()) + { + if (GE) + { + return GE->GetDefaultObject(); + } + return nullptr; + } + return nullptr; +} + +TSubclassOf UGGA_GameplayAbility::K2_GetCostGameplayEffect_Implementation() const +{ + return CostGameplayEffectClass; +} + +void UGGA_GameplayAbility::K2_OnApplyCost_Implementation(const FGameplayAbilitySpecHandle Handle, + const FGameplayAbilityActorInfo& ActorInfo, + const FGameplayAbilityActivationInfo ActivationInfo) const +{ +} + +void UGGA_GameplayAbility::ApplyAbilityTagsToGameplayEffectSpec(FGameplayEffectSpec& Spec, FGameplayAbilitySpec* AbilitySpec) const +{ + Super::ApplyAbilityTagsToGameplayEffectSpec(Spec, AbilitySpec); +} + +bool UGGA_GameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, + FGameplayTagContainer* OptionalRelevantTags) const +{ + // Define a common lambda to check for blocked tags + bool bBlocked = false; + auto CheckForBlocked = [&](const FGameplayTagContainer& ContainerA, const FGameplayTagContainer& ContainerB) + { + // Do we not have any tags in common? Then we're not blocked + if (ContainerA.IsEmpty() || ContainerB.IsEmpty() || !ContainerA.HasAny(ContainerB)) + { + return; + } + + if (OptionalRelevantTags) + { + // Ensure the global blocking tag is only added once + if (!bBlocked) + { + UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get(); + const FGameplayTag& BlockedTag = AbilitySystemGlobals.ActivateFailTagsBlockedTag; + OptionalRelevantTags->AddTag(BlockedTag); + } + + // Now append all the blocking tags + OptionalRelevantTags->AppendMatchingTags(ContainerA, ContainerB); + } + + bBlocked = true; + }; + + // Define a common lambda to check for missing required tags + bool bMissing = false; + auto CheckForRequired = [&](const FGameplayTagContainer& TagsToCheck, const FGameplayTagContainer& RequiredTags) + { + // Do we have no requirements, or have met all requirements? Then nothing's missing + if (RequiredTags.IsEmpty() || TagsToCheck.HasAll(RequiredTags)) + { + return; + } + + if (OptionalRelevantTags) + { + // Ensure the global missing tag is only added once + if (!bMissing) + { + UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get(); + const FGameplayTag& MissingTag = AbilitySystemGlobals.ActivateFailTagsMissingTag; + OptionalRelevantTags->AddTag(MissingTag); + } + + FGameplayTagContainer MissingTags = RequiredTags; + MissingTags.RemoveTags(TagsToCheck.GetGameplayTagParents()); + OptionalRelevantTags->AppendTags(MissingTags); + } + + bMissing = true; + }; + + const UGGA_AbilitySystemComponent* GASC = Cast(&AbilitySystemComponent); + static FGameplayTagContainer AllRequiredTags; + static FGameplayTagContainer AllBlockedTags; + + AllRequiredTags = ActivationRequiredTags; + AllBlockedTags = ActivationBlockedTags; + + // Expand our ability tags to add additional required/blocked tags + if (GASC) + { + GASC->GetAdditionalActivationTagRequirements(GetAssetTags(), AllRequiredTags, AllBlockedTags); + } + + // Start by checking all of the blocked tags first (so OptionalRelevantTags will contain blocked tags first) + CheckForBlocked(GetAssetTags(), AbilitySystemComponent.GetBlockedAbilityTags()); + CheckForBlocked(AbilitySystemComponent.GetOwnedGameplayTags(), AllBlockedTags); + + + // Check to see the required/blocked tags for this ability + if (AllBlockedTags.Num() || AllRequiredTags.Num()) + { + static FGameplayTagContainer AbilitySystemComponentTags; + + AbilitySystemComponentTags.Reset(); + AbilitySystemComponent.GetOwnedGameplayTags(AbilitySystemComponentTags); + + if (AbilitySystemComponentTags.HasAny(AllBlockedTags)) + { + if (OptionalRelevantTags) + { + OptionalRelevantTags->AppendTags(AllBlockedTags); + } + + bBlocked = true; + } + + if (!AbilitySystemComponentTags.HasAll(AllRequiredTags)) + { + if (OptionalRelevantTags) + { + OptionalRelevantTags->AppendTags(AllRequiredTags); + } + bMissing = true; + } + } + + if (SourceTags != nullptr) + { + CheckForBlocked(*SourceTags, SourceBlockedTags); + } + + if (TargetTags != nullptr) + { + CheckForBlocked(*TargetTags, TargetBlockedTags); + } + + // Now check all required tags + CheckForRequired(AbilitySystemComponent.GetOwnedGameplayTags(), AllRequiredTags); + + if (SourceTags != nullptr) + { + CheckForRequired(*SourceTags, SourceRequiredTags); + } + if (TargetTags != nullptr) + { + CheckForRequired(*TargetTags, TargetRequiredTags); + } + + if (!bBlocked && !bMissing) + { + // If it's a custom implementation that blocks, we can't specify exactly which tag so just use the generic + bBlocked = AbilitySystemComponent.AreAbilityTagsBlocked(GetAssetTags()); + if (bBlocked && OptionalRelevantTags) + { + UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get(); + const FGameplayTag& BlockedTag = AbilitySystemGlobals.ActivateFailTagsBlockedTag; + OptionalRelevantTags->AddTag(BlockedTag); + } + } + + // We succeeded if there were no blocked tags and no missing required tags + return !bBlocked && !bMissing; +} + +void UGGA_GameplayAbility::SendTargetDataToServer(const FGameplayAbilityTargetDataHandle& TargetData) +{ + if (IsPredictingClient()) + { + UAbilitySystemComponent* ASC = CurrentActorInfo->AbilitySystemComponent.Get(); + check(ASC); + + // Create new prediction window for next operation. 为接下来的操作新增一个pk + FScopedPredictionWindow(ASC, true); + + FGameplayTag ApplicationTag; + // tell server about it. 告诉服务器设置TargetData,传入技能的uid和激活id,Data和本次操作的id + CurrentActorInfo->AbilitySystemComponent->CallServerSetReplicatedTargetData( + CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), + TargetData, ApplicationTag, ASC->ScopedPredictionKey); + } +} + +#if WITH_EDITOR +EDataValidationResult UGGA_GameplayAbility::IsDataValid(FDataValidationContext& Context) const +{ + if (bReplicateInputDirectly == true) + { + Context.AddError(FText::FromString(TEXT("bReplicateInputDirectly is not recommended to use according to best practices."))); + return EDataValidationResult::Invalid; + } + if (bServerRespectsRemoteAbilityCancellation == true) + { + Context.AddError(FText::FromString(TEXT("bServerRespectsRemoteAbilityCancellation is not recommended to use according to best practices."))); + return EDataValidationResult::Invalid; + } + + PRAGMA_DISABLE_DEPRECATION_WARNINGS + if (InstancingPolicy == EGameplayAbilityInstancingPolicy::NonInstanced) + { + Context.AddError(FText::FromString(TEXT("NonInstanced ability is deprecated since UE5.5, Use InstancedPerActor as the default to avoid confusing corner cases"))); + return EDataValidationResult::Invalid; + } + PRAGMA_ENABLE_DEPRECATION_WARNINGS + + // if (ReplicationPolicy == EGameplayAbilityReplicationPolicy::Type::ReplicateYes) + // { + // Context.AddError(FText::FromString(TEXT("ReplicationPolicy->ReplicateYes is not acceptable, Pelease use other option!"))); + // return EDataValidationResult::Invalid; + // } + + // if (!AbilityTriggers.IsEmpty() && NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::Type::ServerInitiated) + // { + // ValidationErrors.Add(FText::FromString(TEXT("Ability with triggers doesn't work with ServerInitiated Net Execution Policy!"))); + // return EDataValidationResult::Invalid; + // } + + // if (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::Type::ServerInitiated) + // { + // ValidationErrors.Add(FText::FromString(TEXT("NetExecutionPolicy->ServerInitiated is not acceptable, Pelease use other option!"))); + // return EDataValidationResult::Invalid; + // } + + if (bHasBlueprintActivateFromEvent && bHasBlueprintActivate && !AbilityTriggers.IsEmpty()) + { + Context.AddError(FText::FromString(TEXT("ActivateAbilityFromEvent will not run! Please remove ActivateAbility node!"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} + +#include "UObject/ObjectSaveContext.h" + +void UGGA_GameplayAbility::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbilityInterface.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbilityInterface.cpp new file mode 100644 index 0000000..ad78e3a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Abilities/GGA_GameplayAbilityInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Abilities/GGA_GameplayAbilityInterface.h" + + +// Add default functionality here for any IGGA_GroupedAbilityInterface functions that are not pure virtual. diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.cpp new file mode 100644 index 0000000..9426929 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.cpp @@ -0,0 +1,43 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "AbilitySystemComponent.h" + +UGGA_AbilityTask_NetworkSyncPoint* UGGA_AbilityTask_NetworkSyncPoint::WaitNetSyncWithTimeout(UGameplayAbility* OwningAbility, EAbilityTaskNetSyncType InSyncType, float InTimeout) +{ + UGGA_AbilityTask_NetworkSyncPoint* MyObj = NewAbilityTask(OwningAbility); + MyObj->SyncType = InSyncType; + MyObj->Time = InTimeout; + return MyObj; +} + +void UGGA_AbilityTask_NetworkSyncPoint::Activate() +{ + Super::Activate(); + if (TaskState != EGameplayTaskState::Finished && AbilitySystemComponent.IsValid()) + { + UWorld* World = GetWorld(); + TimeStarted = World->GetTimeSeconds(); + if (Time <= 0.0f) + { + World->GetTimerManager().SetTimerForNextTick(this, &ThisClass::OnTimeFinish); + } + else + { + // Use a dummy timer handle as we don't need to store it for later but we don't need to look for something to clear + FTimerHandle TimerHandle; + World->GetTimerManager().SetTimer(TimerHandle, this, &ThisClass::OnTimeFinish, Time, false); + } + } +} + +void UGGA_AbilityTask_NetworkSyncPoint::OnTimeFinish() +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + SyncFinished(); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.cpp new file mode 100644 index 0000000..44109cf --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.cpp @@ -0,0 +1,335 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" +#include "GGA_LogChannels.h" +#include "Animation/AnimMontage.h" +#include "Animation/AnimInstance.h" +#include "GameFramework/Character.h" + +static bool GUseAggressivePlayMontageAndWaitEndTask = true; +static FAutoConsoleVariableRef CVarAggressivePlayMontageAndWaitEndTask( + TEXT("GGA.PlayMontage.AggressiveEndTask"), GUseAggressivePlayMontageAndWaitEndTask, + TEXT("This should be set to true in order to avoid multiple callbacks off an GGA_AbilityTask_PlayMontageAndWaitForEvent node")); + +static bool GPlayMontageAndWaitFireInterruptOnAnimEndInterrupt = true; +static FAutoConsoleVariableRef CVarPlayMontageAndWaitFireInterruptOnAnimEndInterrupt( + TEXT("GGA.PlayMontage.FireInterruptOnAnimEndInterrupt"), GPlayMontageAndWaitFireInterruptOnAnimEndInterrupt, + TEXT("This is a fix that will cause GGA_AbilityTask_PlayMontageAndWaitForEvent to fire its Interrupt event if the underlying AnimInstance ends in an interrupted")); + + +UGGA_AbilityTask_PlayMontageAndWaitForEvent::UGGA_AbilityTask_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + Rate = 1.f; + bAllowInterruptAfterBlendOut = false; + bStopWhenAbilityEnds = true; +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted) +{ + const bool bPlayingThisMontage = (Montage == MontageToPlay) && Ability && Ability->GetCurrentMontage() == MontageToPlay; + if (bPlayingThisMontage) + { + // Reset AnimRootMotionTranslationScale + ACharacter* Character = Cast(GetAvatarActor()); + if (Character && (Character->GetLocalRole() == ROLE_Authority || + (Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted))) + { + Character->SetAnimRootMotionTranslationScale(1.f); + } + } + + if (bPlayingThisMontage && (bInterrupted || !bAllowInterruptAfterBlendOut)) + { + if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get()) + { + ASC->ClearAnimatingAbility(Ability); + } + } + + if (ShouldBroadcastAbilityTaskDelegates()) + { + if (bInterrupted) + { + bAllowInterruptAfterBlendOut = false; + OnInterrupted.Broadcast(FGameplayTag(), FGameplayEventData()); + + if (GUseAggressivePlayMontageAndWaitEndTask) + { + EndTask(); + } + } + else + { + OnBlendOut.Broadcast(FGameplayTag(), FGameplayEventData()); + } + } +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendedIn(UAnimMontage* Montage) +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnInterrupted.Broadcast(FGameplayTag(), FGameplayEventData()); + } +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnGameplayAbilityCancelled() +{ + if (StopPlayingMontage() || bAllowInterruptAfterBlendOut) + { + // Let the BP handle the interrupt as well + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData()); + } + } + + if (GUseAggressivePlayMontageAndWaitEndTask) + { + EndTask(); + } +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted) +{ + if (!bInterrupted) + { + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnCompleted.Broadcast(FGameplayTag(), FGameplayEventData()); + } + } + else if (bAllowInterruptAfterBlendOut && GUseAggressivePlayMontageAndWaitEndTask) + { + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnInterrupted.Broadcast(FGameplayTag(), FGameplayEventData()); + } + } + + EndTask(); +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload) +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + FGameplayEventData TempData = *Payload; + TempData.EventTag = EventTag; + + EventReceived.Broadcast(EventTag, TempData); + } +} + +UGGA_AbilityTask_PlayMontageAndWaitForEvent* UGGA_AbilityTask_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility, + FName TaskInstanceName, UAnimMontage* MontageToPlay, + FGameplayTagContainer EventTags, float Rate, FName StartSection, + bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale, + float StartTimeSeconds, bool bAllowInterruptAfterBlendOut) +{ + UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate); + + UGGA_AbilityTask_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask(OwningAbility, TaskInstanceName); + MyObj->MontageToPlay = MontageToPlay; + MyObj->Rate = Rate; + MyObj->StartSection = StartSection; + MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale; + MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds; + MyObj->bAllowInterruptAfterBlendOut = bAllowInterruptAfterBlendOut; + MyObj->StartTimeSeconds = StartTimeSeconds; + MyObj->EventTags = EventTags; + + return MyObj; +} + +UGGA_AbilityTask_PlayMontageAndWaitForEvent* UGGA_AbilityTask_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEventExt(UGameplayAbility* OwningAbility, + FGGA_PlayMontageAndWaitForEventTaskParams Params) +{ + UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Params.Rate); + + UGGA_AbilityTask_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask(OwningAbility, Params.TaskInstanceName); + MyObj->MontageToPlay = Params.MontageToPlay; + MyObj->Rate = Params.Rate; + MyObj->StartSection = Params.StartSection; + MyObj->AnimRootMotionTranslationScale = Params.AnimRootMotionTranslationScale; + MyObj->bStopWhenAbilityEnds = Params.bStopWhenAbilityEnds; + MyObj->bAllowInterruptAfterBlendOut = Params.bAllowInterruptAfterBlendOut; + MyObj->StartTimeSeconds = Params.StartTimeSeconds; + MyObj->EventTags = Params.EventTags; + + return MyObj; +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::Activate() +{ + if (Ability == nullptr) + { + return; + } + + bool bPlayedMontage = false; + + if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get()) + { + const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo(); + UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance(); + if (AnimInstance != nullptr) + { + if (ASC->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f) + { + // Playing a montage could potentially fire off a callback into game code which could kill this ability! Early out if we are pending kill. + if (ShouldBroadcastAbilityTaskDelegates() == false) + { + return; + } + + // Bind to event callback + EventHandle = ASC->AddGameplayEventTagContainerDelegate( + EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent)); + + InterruptedHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnGameplayAbilityCancelled); + + BlendedInDelegate.BindUObject(this, &UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendedIn); + AnimInstance->Montage_SetBlendedInDelegate(BlendedInDelegate, MontageToPlay); + + BlendingOutDelegate.BindUObject(this, &UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut); + AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay); + + MontageEndedDelegate.BindUObject(this, &UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded); + AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay); + + ACharacter* Character = Cast(GetAvatarActor()); + if (Character && (Character->GetLocalRole() == ROLE_Authority || + (Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted))) + { + Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale); + } + + bPlayedMontage = true; + } + } + else + { + UE_LOG(LogGGA_Tasks, Warning, TEXT("GGA_AbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!")); + } + } + else + { + UE_LOG(LogGGA_Tasks, Warning, TEXT("GGA_AbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent")); + } + + if (!bPlayedMontage) + { + UE_LOG(LogGGA_Tasks, Warning, TEXT("GGA_AbilityTask_PlayMontageAndWaitForEvent called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), + *GetNameSafe(MontageToPlay), *InstanceName.ToString()); + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData()); + } + } + + SetWaitingOnAvatar(); +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::ExternalCancel() +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData()); + } + + Super::ExternalCancel(); +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded) +{ + // Note: Clearing montage end delegate isn't necessary since its not a multicast and will be cleared when the next montage plays. + // (If we are destroyed, it will detect this and not do anything) + + // This delegate, however, should be cleared as it is a multicast + if (Ability) + { + Ability->OnGameplayAbilityCancelled.Remove(InterruptedHandle); + if (AbilityEnded && bStopWhenAbilityEnds) + { + StopPlayingMontage(); + } + } + + if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get()) + { + ASC->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle); + } + + Super::OnDestroy(AbilityEnded); +} + +void UGGA_AbilityTask_PlayMontageAndWaitForEvent::EndTaskByOwner() +{ + TaskOwnerEnded(); +} + +bool UGGA_AbilityTask_PlayMontageAndWaitForEvent::StopPlayingMontage() +{ + if (Ability == nullptr) + { + return false; + } + + const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo(); + if (ActorInfo == nullptr) + { + return false; + } + + UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance(); + if (AnimInstance == nullptr) + { + return false; + } + + // Check if the montage is still playing + // The ability would have been interrupted, in which case we should automatically stop the montage + UAbilitySystemComponent* ASC = AbilitySystemComponent.Get(); + if (ASC && Ability) + { + if (ASC->GetAnimatingAbility() == Ability + && ASC->GetCurrentMontage() == MontageToPlay) + { + // Unbind delegates so they don't get called as well + FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(MontageToPlay); + if (MontageInstance) + { + MontageInstance->OnMontageBlendedInEnded.Unbind(); + MontageInstance->OnMontageBlendingOutStarted.Unbind(); + MontageInstance->OnMontageEnded.Unbind(); + } + + ASC->CurrentMontageStop(); + return true; + } + } + + return false; +} + +FString UGGA_AbilityTask_PlayMontageAndWaitForEvent::GetDebugString() const +{ + UAnimMontage* PlayingMontage = nullptr; + if (Ability) + { + const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo(); + UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance(); + + if (AnimInstance != nullptr) + { + PlayingMontage = AnimInstance->Montage_IsActive(MontageToPlay) ? ToRawPtr(MontageToPlay) : AnimInstance->GetCurrentActiveMontage(); + } + } + + return FString::Printf(TEXT("PlayMontageAndWaitForEvent. MontageToPlay: %s (Currently Playing): %s"), *GetNameSafe(MontageToPlay), *GetNameSafe(PlayingMontage)); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.cpp new file mode 100644 index 0000000..d783626 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.cpp @@ -0,0 +1,78 @@ +//// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// +//#include "AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.h" +// +//#include "GameplayTasksComponent.h" +//#include "CustomTasks/GGA_CustomAbilityTask.h" +//#include "Net/UnrealNetwork.h" +// +//UGGA_AbilityTask_RunCustomAbilityTask::UGGA_AbilityTask_RunCustomAbilityTask() +//{ +//} +// +//void UGGA_AbilityTask_RunCustomAbilityTask::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +//{ +// Super::GetLifetimeReplicatedProps(OutLifetimeProps); +// DOREPLIFETIME(UGGA_AbilityTask_RunCustomAbilityTask, TaskInstance); +//} +// +//UGGA_AbilityTask_RunCustomAbilityTask* UGGA_AbilityTask_RunCustomAbilityTask::RunCustomAbilityTask(UGameplayAbility* OwningAbility, TSoftClassPtr AbilityTaskClass) +//{ +// if (TSubclassOf RealClass = AbilityTaskClass.LoadSynchronous()) +// { +// UGGA_AbilityTask_RunCustomAbilityTask* MyObj = NewAbilityTask(OwningAbility); +// +// TObjectPtr CustomAbilityTask = NewObject(MyObj, RealClass); +// CustomAbilityTask->SetSourceTask(MyObj); +// MyObj->bSimulatedTask = CustomAbilityTask->bSimulatedTask; +// MyObj->bTickingTask = CustomAbilityTask->bTickingTask; +// return MyObj; +// } +// return nullptr; +//} +// +//void UGGA_AbilityTask_RunCustomAbilityTask::Activate() +//{ +// if (UGameplayTasksComponent* Component = GetGameplayTasksComponent()) +// { +// if (IsSimulatedTask()) +// { +// if (Component->IsUsingRegisteredSubObjectList() && Component->IsReadyForReplication()) +// { +// Component->AddReplicatedSubObject(TaskInstance, COND_SkipOwner); +// } +// } +// } +// TaskInstance->OnTaskActivate(); +//} +// +//void UGGA_AbilityTask_RunCustomAbilityTask::TickTask(float DeltaTime) +//{ +// TaskInstance->OnTaskTick(DeltaTime); +//} +// +//void UGGA_AbilityTask_RunCustomAbilityTask::OnDestroy(bool bInOwnerFinished) +//{ +// if (!bWasSuccessfullyDestroyed) +// { +// if (UGameplayTasksComponent* Component = GetGameplayTasksComponent()) +// { +// if (IsSimulatedTask()) +// { +// if (Component->IsUsingRegisteredSubObjectList()) +// { +// Component->RemoveReplicatedSubObject(TaskInstance); +// } +// } +// } +// TaskInstance->OnTaskDestroy(bInOwnerFinished); +// } +// Super::OnDestroy(bInOwnerFinished); +//} +// +//void UGGA_AbilityTask_RunCustomAbilityTask::InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent) +//{ +// Super::InitSimulatedTask(InGameplayTasksComponent); +// TaskInstance->OnInitSimulatedTask(); +//} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.cpp new file mode 100644 index 0000000..c57203d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.cpp @@ -0,0 +1,59 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.h" +#include "AbilitySystemComponent.h" + +UGGA_AbilityTask_ServerWaitForClientTargetData::UGGA_AbilityTask_ServerWaitForClientTargetData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UGGA_AbilityTask_ServerWaitForClientTargetData* UGGA_AbilityTask_ServerWaitForClientTargetData::ServerWaitForClientTargetData(UGameplayAbility* OwningAbility, FName TaskInstanceName, bool TriggerOnce) +{ + UGGA_AbilityTask_ServerWaitForClientTargetData* MyObj = NewAbilityTask(OwningAbility, TaskInstanceName); + MyObj->bTriggerOnce = TriggerOnce; + return MyObj; +} + +void UGGA_AbilityTask_ServerWaitForClientTargetData::Activate() +{ + // ClientPath + if (!Ability || !Ability->GetCurrentActorInfo()->IsNetAuthority()) + { + return; + } + + // ServerPath + FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle(); + FPredictionKey ActivationPredictionKey = GetActivationPredictionKey(); + AbilitySystemComponent->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(this, &UGGA_AbilityTask_ServerWaitForClientTargetData::OnTargetDataReplicatedCallback); +} + +void UGGA_AbilityTask_ServerWaitForClientTargetData::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& Data, FGameplayTag ActivationTag) +{ + FGameplayAbilityTargetDataHandle MutableData = Data; + AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey()); + + if (ShouldBroadcastAbilityTaskDelegates()) + { + ValidData.Broadcast(MutableData); + } + + if (bTriggerOnce) + { + EndTask(); + } +} + +void UGGA_AbilityTask_ServerWaitForClientTargetData::OnDestroy(bool AbilityEnded) +{ + if (AbilitySystemComponent.IsValid()) + { + FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle(); + FPredictionKey ActivationPredictionKey = GetActivationPredictionKey(); + AbilitySystemComponent->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).RemoveAll(this); + } + + Super::OnDestroy(AbilityEnded); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.cpp new file mode 100644 index 0000000..2be0005 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.h" +#include "Engine/World.h" +#include "TimerManager.h" + +UGGA_AbilityTask_WaitDelayOneFrame::UGGA_AbilityTask_WaitDelayOneFrame(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UGGA_AbilityTask_WaitDelayOneFrame::Activate() +{ + GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UGGA_AbilityTask_WaitDelayOneFrame::OnDelayFinish); +} + +UGGA_AbilityTask_WaitDelayOneFrame* UGGA_AbilityTask_WaitDelayOneFrame::WaitDelayOneFrame(UGameplayAbility* OwningAbility) +{ + UGGA_AbilityTask_WaitDelayOneFrame* MyObj = NewAbilityTask(OwningAbility); + return MyObj; +} + +void UGGA_AbilityTask_WaitDelayOneFrame::OnDelayFinish() +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnFinish.Broadcast(); + } + EndTask(); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.cpp new file mode 100644 index 0000000..0a41ab3 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.cpp @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.h" +#include "AbilitySystemGlobals.h" +#include "AbilitySystemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GGA_AbilityTask_WaitGameplayEvents) + +// ---------------------------------------------------------------- + +UGGA_AbilityTask_WaitGameplayEvents::UGGA_AbilityTask_WaitGameplayEvents(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UGGA_AbilityTask_WaitGameplayEvents* UGGA_AbilityTask_WaitGameplayEvents::WaitGameplayEvents(UGameplayAbility* OwningAbility, FGameplayTagContainer EventTags, AActor* OptionalExternalTarget, + bool OnlyTriggerOnce) +{ + UGGA_AbilityTask_WaitGameplayEvents* MyObj = NewAbilityTask(OwningAbility); + MyObj->EventTags = EventTags; + MyObj->SetExternalTarget(OptionalExternalTarget); + MyObj->OnlyTriggerOnce = OnlyTriggerOnce; + + return MyObj; +} + +void UGGA_AbilityTask_WaitGameplayEvents::Activate() +{ + UAbilitySystemComponent* ASC = GetTargetASC(); + if (ASC) + { + MyHandle = ASC->AddGameplayEventTagContainerDelegate( + EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &UGGA_AbilityTask_WaitGameplayEvents::GameplayEventContainerCallback)); + } + + Super::Activate(); +} + +void UGGA_AbilityTask_WaitGameplayEvents::GameplayEventContainerCallback(FGameplayTag MatchingTag, const FGameplayEventData* Payload) +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + ensureMsgf(Payload, TEXT("GameplayEventCallback expected non-null Payload")); + FGameplayEventData TempPayload = Payload ? *Payload : FGameplayEventData{}; + TempPayload.EventTag = MatchingTag; + EventReceived.Broadcast(MatchingTag, TempPayload); + } + if (OnlyTriggerOnce) + { + EndTask(); + } +} + +void UGGA_AbilityTask_WaitGameplayEvents::SetExternalTarget(AActor* Actor) +{ + if (Actor) + { + UseExternalTarget = true; + OptionalExternalTarget = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor); + } +} + +UAbilitySystemComponent* UGGA_AbilityTask_WaitGameplayEvents::GetTargetASC() +{ + if (UseExternalTarget) + { + return OptionalExternalTarget; + } + + return AbilitySystemComponent.Get(); +} + +void UGGA_AbilityTask_WaitGameplayEvents::OnDestroy(bool AbilityEnding) +{ + UAbilitySystemComponent* ASC = GetTargetASC(); + if (ASC && MyHandle.IsValid()) + { + ASC->RemoveGameplayEventTagContainerDelegate(EventTags, MyHandle); + } + + Super::OnDestroy(AbilityEnding); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.cpp new file mode 100644 index 0000000..6528083 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.cpp @@ -0,0 +1,126 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.h" +#include "Engine/World.h" +#include "AbilitySystemComponent.h" + +UGGA_AbilityTask_WaitInputPressWithTags::UGGA_AbilityTask_WaitInputPressWithTags(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + StartTime = 0.f; + bTestInitialState = false; +} + +UGGA_AbilityTask_WaitInputPressWithTags* UGGA_AbilityTask_WaitInputPressWithTags::WaitInputPressWithTags(UGameplayAbility* OwningAbility, FGameplayTagContainer RequiredTags, + FGameplayTagContainer IgnoredTags, + bool bTestAlreadyPressed) +{ + UGGA_AbilityTask_WaitInputPressWithTags* Task = NewAbilityTask(OwningAbility); + Task->bTestInitialState = bTestAlreadyPressed; + Task->TagQuery = FGameplayTagQuery::BuildQuery(FGameplayTagQueryExpression().AllTagsMatch().AddTags(RequiredTags).NoTagsMatch().AddTags(IgnoredTags)); + return Task; +} + +UGGA_AbilityTask_WaitInputPressWithTags* UGGA_AbilityTask_WaitInputPressWithTags::WaitInputPressWithTagQuery(UGameplayAbility* OwningAbility, const FGameplayTagQuery& TagQuery, + bool bTestAlreadyPressed) +{ + UGGA_AbilityTask_WaitInputPressWithTags* Task = NewAbilityTask(OwningAbility); + Task->bTestInitialState = bTestAlreadyPressed; + Task->TagQuery = TagQuery; + return Task; +} + +void UGGA_AbilityTask_WaitInputPressWithTags::OnPressCallback() +{ + float ElapsedTime = GetWorld()->GetTimeSeconds() - StartTime; + + UAbilitySystemComponent* ASC = AbilitySystemComponent.Get(); + + if (!Ability || !ASC) + { + EndTask(); + return; + } + + const FGameplayTagContainer CurrentTags = ASC->GetOwnedGameplayTags(); + if (!TagQuery.Matches(CurrentTags)) + { + Reset(); + return; + } + + ASC->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()).Remove(DelegateHandle); + + FScopedPredictionWindow ScopedPrediction(ASC, IsPredictingClient()); + + if (IsPredictingClient()) + { + // Tell the server about this + ASC->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey(), + ASC->ScopedPredictionKey); + } + else + { + ASC->ConsumeGenericReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()); + } + + // We are done. Kill us so we don't keep getting broadcast messages + if (ShouldBroadcastAbilityTaskDelegates()) + { + OnPress.Broadcast(ElapsedTime); + } + + EndTask(); +} + +void UGGA_AbilityTask_WaitInputPressWithTags::Activate() +{ + StartTime = GetWorld()->GetTimeSeconds(); + if (Ability) + { + if (bTestInitialState && IsLocallyControlled()) + { + FGameplayAbilitySpec* Spec = Ability->GetCurrentAbilitySpec(); + if (Spec && Spec->InputPressed) + { + OnPressCallback(); + return; + } + } + + DelegateHandle = AbilitySystemComponent->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()).AddUObject( + this, &UGGA_AbilityTask_WaitInputPressWithTags::OnPressCallback); + if (IsForRemoteClient()) + { + if (!AbilitySystemComponent->CallReplicatedEventDelegateIfSet(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey())) + { + SetWaitingOnRemotePlayerData(); + } + } + } +} + +void UGGA_AbilityTask_WaitInputPressWithTags::OnDestroy(bool AbilityEnded) +{ + AbilitySystemComponent->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()).Remove(DelegateHandle); + + ClearWaitingOnRemotePlayerData(); + + Super::OnDestroy(AbilityEnded); +} + +void UGGA_AbilityTask_WaitInputPressWithTags::Reset() +{ + AbilitySystemComponent->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()).Remove(DelegateHandle); + + DelegateHandle = AbilitySystemComponent->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey()).AddUObject( + this, &UGGA_AbilityTask_WaitInputPressWithTags::OnPressCallback); + if (IsForRemoteClient()) + { + if (!AbilitySystemComponent->CallReplicatedEventDelegateIfSet(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey())) + { + SetWaitingOnRemotePlayerData(); + } + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.cpp new file mode 100644 index 0000000..ec56731 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.cpp @@ -0,0 +1,325 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.h" + +#include "AbilitySystemComponent.h" +#include "TargetActors/GGA_AbilityTargetActor_Trace.h" + +UGGA_AbilityTask_WaitTargetDataUsingActor* UGGA_AbilityTask_WaitTargetDataUsingActor::WaitTargetDataWithReusableActor( + UGameplayAbility* OwningAbility, FName TaskInstanceName, + TEnumAsByte ConfirmationType, AGameplayAbilityTargetActor* InTargetActor, + bool bCreateKeyIfNotValidForMorePrediction) +{ + UGGA_AbilityTask_WaitTargetDataUsingActor* MyObj = NewAbilityTask( + OwningAbility, TaskInstanceName); //Register for task list here, providing a given FName as a key + MyObj->TargetActor = InTargetActor; + MyObj->ConfirmationType = ConfirmationType; + MyObj->bCreateKeyIfNotValidForMorePrediction = bCreateKeyIfNotValidForMorePrediction; + return MyObj; +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::Activate() +{ + if (!IsValid(this)) + { + return; + } + + if (Ability && TargetActor) + { + /** server&client 注册TargetActor上的Ready(Confirm)/Cancel事件 */ + InitializeTargetActor(); + /** server注册TargetDeta */ + RegisterTargetDataCallbacks(); + + FinalizeTargetActor(); + } + else + { + EndTask(); + } +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& Data, + FGameplayTag ActivationTag) +{ + FGameplayAbilityTargetDataHandle MutableData = Data; + + if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get()) + { + AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey()); + } + + /** + * Call into the TargetActor to sanitize/verify the data. If this returns false, we are rejecting + * the replicated target data and will treat this as a cancel. + * + * This can also be used for bandwidth optimizations. OnReplicatedTargetDataReceived could do an actual + * trace/check/whatever server side and use that data. So rather than having the client send that data + * explicitly, the client is basically just sending a 'confirm' and the server is now going to do the work + * in OnReplicatedTargetDataReceived. + */ + if (TargetActor && !TargetActor->OnReplicatedTargetDataReceived(MutableData)) + { + if (ShouldBroadcastAbilityTaskDelegates()) + { + Cancelled.Broadcast(MutableData); + } + } + else + { + if (ShouldBroadcastAbilityTaskDelegates()) + { + ValidData.Broadcast(MutableData); + } + } + + if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti) + { + EndTask(); + } +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReplicatedCancelledCallback() +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + Cancelled.Broadcast(FGameplayAbilityTargetDataHandle()); + } + EndTask(); +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data) +{ + + UAbilitySystemComponent* ASC = AbilitySystemComponent.Get(); + + if (!Ability || !ASC) + { + return; + } + + // client path + FScopedPredictionWindow ScopedPrediction(ASC, + ShouldReplicateDataToServer() && (bCreateKeyIfNotValidForMorePrediction && + !ASC->ScopedPredictionKey.IsValidForMorePrediction() + )); + + const FGameplayAbilityActorInfo* Info = Ability->GetCurrentActorInfo(); + + // client path + if (IsPredictingClient()) + { + // Rpc发送TargetData到服务器 + if (!TargetActor->ShouldProduceTargetDataOnServer) + { + FGameplayTag ApplicationTag; // Fixme: where would this be useful? + ASC->CallServerSetReplicatedTargetData(GetAbilitySpecHandle(), + GetActivationPredictionKey(), Data, + ApplicationTag, + AbilitySystemComponent->ScopedPredictionKey); + } + else if (ConfirmationType == EGameplayTargetingConfirmation::UserConfirmed) + { + // Rpc告诉服务器确认了。 + // We aren't going to send the target data, but we will send a generic confirmed message. + ASC->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::GenericConfirm, + GetAbilitySpecHandle(), GetActivationPredictionKey(), + AbilitySystemComponent->ScopedPredictionKey); + } + } + + if (ShouldBroadcastAbilityTaskDelegates()) + { + ValidData.Broadcast(Data); + } + + if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti) + { + EndTask(); + } +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data) +{ + UAbilitySystemComponent* ASC = AbilitySystemComponent.Get(); + + if(!ASC) + { + return; + } + + //client path + FScopedPredictionWindow ScopedPrediction(ASC, IsPredictingClient()); + + //client path + if (IsPredictingClient()) + { + if (!TargetActor->ShouldProduceTargetDataOnServer) + { + ASC->ServerSetReplicatedTargetDataCancelled( + GetAbilitySpecHandle(), GetActivationPredictionKey(), ASC->ScopedPredictionKey); + } + else + { + // We aren't going to send the target data, but we will send a generic confirmed message. + ASC->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::GenericCancel, + GetAbilitySpecHandle(), GetActivationPredictionKey(), + ASC->ScopedPredictionKey); + } + } + + // client&& server path. + Cancelled.Broadcast(Data); + EndTask(); +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::ExternalConfirm(bool bEndTask) +{ + if (TargetActor) + { + if (TargetActor->ShouldProduceTargetData()) + { + TargetActor->ConfirmTargetingAndContinue(); + } + } + Super::ExternalConfirm(bEndTask); +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::ExternalCancel() +{ + if (ShouldBroadcastAbilityTaskDelegates()) + { + Cancelled.Broadcast(FGameplayAbilityTargetDataHandle()); + } + Super::ExternalCancel(); +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::InitializeTargetActor() const +{ + check(TargetActor); + check(Ability); + + TargetActor->PrimaryPC = Ability->GetCurrentActorInfo()->PlayerController.Get(); + + TargetActor->TargetDataReadyDelegate.AddUObject( + const_cast(this), &UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReadyCallback); + TargetActor->CanceledDelegate.AddUObject( + const_cast(this), &UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataCancelledCallback); +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::RegisterTargetDataCallbacks() +{ + if (!ensure(IsValid(this) == true)) + { + return; + } + + UAbilitySystemComponent* ASC = AbilitySystemComponent.Get(); + + if (!ASC) + { + return; + } + + check(Ability); + + const bool bIsLocalControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled(); + const bool bShouldProduceTargetDataOnServer = TargetActor->ShouldProduceTargetDataOnServer; + + /** server path. 若不是本地控制的(server for remote client),查看TargetData是否已发送,否则在到达此处时注册回调 */ + if (!bIsLocalControlled) + { + //如果我们希望客户端发送TargetData回调,就注册TargetData回调 + if (!bShouldProduceTargetDataOnServer) // produce on client + { + FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle(); + FPredictionKey ActivationPredictionKey = GetActivationPredictionKey(); + + /** 注册TargetDataSet事件 */ + ASC->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject( + this, &UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReplicatedCallback); + + /** 注册TargetDataCancel事件*/ + ASC->AbilityTargetDataCancelledDelegate(SpecHandle, ActivationPredictionKey).AddUObject( + this, &UGGA_AbilityTask_WaitTargetDataUsingActor::OnTargetDataReplicatedCancelledCallback); + + // 检查TargetData是否已经Confirm/Cancel并执行相关操作。 + ASC->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey); + + SetWaitingOnRemotePlayerData(); + } + } +} + +void UGGA_AbilityTask_WaitTargetDataUsingActor::FinalizeTargetActor() const +{ + check(TargetActor); + check(Ability); + + TargetActor->StartTargeting(Ability); + + if (TargetActor->ShouldProduceTargetData()) + { + // If instant confirm, then stop targeting immediately. + // Note this is kind of bad: we should be able to just call a static func on the CDO to do this. + // But then we wouldn't get to set ExposeOnSpawnParameters. + if (ConfirmationType == EGameplayTargetingConfirmation::Instant) + { + TargetActor->ConfirmTargeting(); + } + else if (ConfirmationType == EGameplayTargetingConfirmation::UserConfirmed) + { + // Bind to the Cancel/Confirm Delegates (called from local confirm or from repped confirm) + TargetActor->BindToConfirmCancelInputs(); + } + } +} + + +void UGGA_AbilityTask_WaitTargetDataUsingActor::OnDestroy(bool AbilityEnded) +{ + if (TargetActor) + { + AGGA_AbilityTargetActor_Trace* TraceTargetActor = Cast(TargetActor); + if (TraceTargetActor) + { + // TargetActor 基类没有StopTracing函数. + TraceTargetActor->StopTargeting(); + } + else + { + // TargetActor doesn't have a StopTargeting function + TargetActor->SetActorTickEnabled(false); + + // Clear added callbacks + TargetActor->TargetDataReadyDelegate.RemoveAll(this); + TargetActor->CanceledDelegate.RemoveAll(this); + + AbilitySystemComponent->GenericLocalConfirmCallbacks.RemoveDynamic( + TargetActor, &AGameplayAbilityTargetActor::ConfirmTargeting); + AbilitySystemComponent->GenericLocalCancelCallbacks.RemoveDynamic( + TargetActor, &AGameplayAbilityTargetActor::CancelTargeting); + TargetActor->GenericDelegateBoundASC = nullptr; + } + } + + Super::OnDestroy(AbilityEnded); +} + +bool UGGA_AbilityTask_WaitTargetDataUsingActor::ShouldReplicateDataToServer() const +{ + if (!Ability || !TargetActor) + { + return false; + } + + const FGameplayAbilityActorInfo* Info = Ability->GetCurrentActorInfo(); + if (!Info->IsNetAuthority() && !TargetActor->ShouldProduceTargetDataOnServer) + { + return true; + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_AttributeChanged.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_AttributeChanged.cpp new file mode 100644 index 0000000..3b439db --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_AttributeChanged.cpp @@ -0,0 +1,86 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AsyncTasks/GGA_AsyncTask_AttributeChanged.h" + +UGGA_AsyncTask_AttributeChanged* UGGA_AsyncTask_AttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute) +{ + if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid()) + { + return nullptr; + } + + UGGA_AsyncTask_AttributeChanged* WaitForAttributeChangedTask = NewObject(); + WaitForAttributeChangedTask->SetAbilityActor(AbilitySystemComponent->GetAvatarActor()); + WaitForAttributeChangedTask->AttributeToListenFor = Attribute; + + return WaitForAttributeChangedTask; +} + +UGGA_AsyncTask_AttributeChanged* UGGA_AsyncTask_AttributeChanged::ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray Attributes) +{ + if (!IsValid(AbilitySystemComponent) || Attributes.IsEmpty()) + { + return nullptr; + } + + UGGA_AsyncTask_AttributeChanged* WaitForAttributeChangedTask = NewObject(); + WaitForAttributeChangedTask->SetAbilityActor(AbilitySystemComponent->GetAvatarActor()); + + WaitForAttributeChangedTask->AttributesToListenFor = Attributes; + + return WaitForAttributeChangedTask; +} + +void UGGA_AsyncTask_AttributeChanged::EndTask() +{ + EndAction(); +} + +void UGGA_AsyncTask_AttributeChanged::Activate() +{ + Super::Activate(); + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + if (AttributeToListenFor.IsValid()) + { + ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).AddUObject(this, &ThisClass::AttributeChanged); + } + for (const FGameplayAttribute& Attribute : AttributesToListenFor) + { + if (Attribute.IsValid()) + { + ASC->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(this, &ThisClass::AttributeChanged); + } + } + } + else + { + EndAction(); + } +} + +void UGGA_AsyncTask_AttributeChanged::EndAction() +{ + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + if (AttributeToListenFor.IsValid()) + { + ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this); + } + + for (FGameplayAttribute Attribute : AttributesToListenFor) + { + if (AttributeToListenFor.IsValid()) + { + ASC->GetGameplayAttributeValueChangeDelegate(Attribute).RemoveAll(this); + } + } + } + Super::EndAction(); +} + +void UGGA_AsyncTask_AttributeChanged::AttributeChanged(const FOnAttributeChangeData& Data) +{ + OnAttributeChanged.Broadcast(Data.Attribute, Data.NewValue, Data.OldValue); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.cpp new file mode 100644 index 0000000..17f653f --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.cpp @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.h" + +UGGA_AsyncTask_GameplayTagAddedRemoved* UGGA_AsyncTask_GameplayTagAddedRemoved::ListenForGameplayTagAddedOrRemoved(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer InTags) +{ + UGGA_AsyncTask_GameplayTagAddedRemoved* TaskInstance = NewObject(); + TaskInstance->SetAbilitySystemComponent(AbilitySystemComponent); + TaskInstance->Tags = InTags; + + if (!IsValid(AbilitySystemComponent) || InTags.Num() < 1) + { + TaskInstance->EndTask(); + return nullptr; + } + + TArray TagArray; + InTags.GetGameplayTagArray(TagArray); + + for (FGameplayTag Tag : TagArray) + { + AbilitySystemComponent->RegisterGameplayTagEvent(Tag, EGameplayTagEventType::NewOrRemoved).AddUObject(TaskInstance, &UGGA_AsyncTask_GameplayTagAddedRemoved::TagChanged); + } + + return TaskInstance; +} + +void UGGA_AsyncTask_GameplayTagAddedRemoved::EndTask() +{ + EndAction(); +} + +void UGGA_AsyncTask_GameplayTagAddedRemoved::EndAction() +{ + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + TArray TagArray; + Tags.GetGameplayTagArray(TagArray); + + for (FGameplayTag Tag : TagArray) + { + ASC->RegisterGameplayTagEvent(Tag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this); + } + } + + Super::EndAction(); +} + +void UGGA_AsyncTask_GameplayTagAddedRemoved::TagChanged(const FGameplayTag Tag, int32 NewCount) +{ + if (NewCount > 0) + { + OnTagAdded.Broadcast(Tag); + } + else + { + OnTagRemoved.Broadcast(Tag); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.cpp new file mode 100644 index 0000000..9ca1e4e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.cpp @@ -0,0 +1,58 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.h" + +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" + +UGGA_AsyncTask_WaitGameplayAbilityActivated* UGGA_AsyncTask_WaitGameplayAbilityActivated::WaitGameplayAbilityActivated(AActor* TargetActor) +{ + UGGA_AsyncTask_WaitGameplayAbilityActivated* MyObj = NewObject(); + MyObj->SetAbilityActor(TargetActor); + MyObj->SetAbilitySystemComponent(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor)); + return MyObj; +} + +void UGGA_AsyncTask_WaitGameplayAbilityActivated::HandleAbilityActivated(UGameplayAbility* Ability) +{ + if (ShouldBroadcastDelegates()) + { + OnAbilityActivated.Broadcast(Ability); + } + else + { + EndAction(); + } +} + +bool UGGA_AsyncTask_WaitGameplayAbilityActivated::ShouldBroadcastDelegates() const +{ + return Super::ShouldBroadcastDelegates(); +} + +void UGGA_AsyncTask_WaitGameplayAbilityActivated::Activate() +{ + Super::Activate(); + + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + DelegateHandle = ASC->AbilityActivatedCallbacks.AddUObject(this, &UGGA_AsyncTask_WaitGameplayAbilityActivated::HandleAbilityActivated); + } + else + { + EndAction(); + } +} + +void UGGA_AsyncTask_WaitGameplayAbilityActivated::EndAction() +{ + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + if (DelegateHandle.IsValid()) + { + ASC->AbilityActivatedCallbacks.Remove(DelegateHandle); + } + } + Super::EndAction(); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.cpp new file mode 100644 index 0000000..90d5eee --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.cpp @@ -0,0 +1,79 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.h" +#include "Runtime/Launch/Resources/Version.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" + +UGGA_AsyncTask_WaitGameplayAbilityEnded* UGGA_AsyncTask_WaitGameplayAbilityEnded::WaitGameplayAbilityEnded(AActor* TargetActor, + FGameplayTagQuery AbilityQuery) +{ + UGGA_AsyncTask_WaitGameplayAbilityEnded* MyObj = NewObject(); + MyObj->SetAbilityActor(TargetActor); + MyObj->SetAbilitySystemComponent(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor)); + MyObj->AbilityQuery = AbilityQuery; + return MyObj; +} + +UGGA_AsyncTask_WaitGameplayAbilityEnded* UGGA_AsyncTask_WaitGameplayAbilityEnded::WaitAbilitySpecHandleEnded(AActor* TargetActor, FGameplayAbilitySpecHandle AbilitySpecHandle) +{ + UGGA_AsyncTask_WaitGameplayAbilityEnded* MyObj = NewObject(); + MyObj->SetAbilityActor(TargetActor); + MyObj->SetAbilitySystemComponent(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor)); + MyObj->AbilitySpecHandle = AbilitySpecHandle; + return MyObj; +} + +void UGGA_AsyncTask_WaitGameplayAbilityEnded::HandleAbilityEnded(const FAbilityEndedData& Data) +{ + if (ShouldBroadcastDelegates()) + { + if (!AbilityQuery.IsEmpty()) + { +#if ENGINE_MINOR_VERSION > 4 + if (AbilityQuery.Matches(Data.AbilityThatEnded->GetAssetTags())) +#else + if (AbilityQuery.Matches(Data.AbilityThatEnded->AbilityTags)) +#endif + { + OnAbilityEnded.Broadcast(Data); + } + } + + if (AbilitySpecHandle.IsValid() && AbilitySpecHandle == Data.AbilitySpecHandle) + { + OnAbilityEnded.Broadcast(Data); + } + } + else + { + EndAction(); + } +} + +void UGGA_AsyncTask_WaitGameplayAbilityEnded::Activate() +{ + Super::Activate(); + + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + DelegateHandle = ASC->OnAbilityEnded.AddUObject(this, &UGGA_AsyncTask_WaitGameplayAbilityEnded::HandleAbilityEnded); + } + else + { + EndAction(); + } +} + +void UGGA_AsyncTask_WaitGameplayAbilityEnded::EndAction() +{ + if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent()) + { + if (DelegateHandle.IsValid()) + { + ASC->AbilityEndedCallbacks.Remove(DelegateHandle); + } + } + Super::EndAction(); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Attributes/GGA_AttributeSet.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Attributes/GGA_AttributeSet.cpp new file mode 100644 index 0000000..6205636 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Attributes/GGA_AttributeSet.cpp @@ -0,0 +1,21 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/GGA_AttributeSet.h" +#include "GGA_AbilitySystemComponent.h" + +UGGA_AttributeSet::UGGA_AttributeSet() +{ +} + +UWorld* UGGA_AttributeSet::GetWorld() const +{ + const UObject* Outer = GetOuter(); + check(Outer); + + return Outer->GetWorld(); +} + +UGGA_AbilitySystemComponent* UGGA_AttributeSet::GetGGA_AbilitySystemComponent() const +{ + return Cast(GetOwningAbilitySystemComponent()); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemComponent.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemComponent.cpp new file mode 100644 index 0000000..d1e365e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemComponent.cpp @@ -0,0 +1,591 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_AbilitySystemComponent.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemLog.h" +#include "GameplayCueManager.h" +#include "GGA_AbilitySystemGlobals.h" +#include "GGA_AbilityTagRelationshipMapping.h" +#include "GGA_GlobalAbilitySystem.h" +#include "GGA_LogChannels.h" +#include "Abilities/GGA_GameplayAbilityInterface.h" +#include "GameFramework/Pawn.h" +#include "Runtime/Launch/Resources/Version.h" + + +#pragma region Initialization + +UGGA_AbilitySystemComponent::UGGA_AbilitySystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + SetIsReplicatedByDefault(true); + + AbilitySystemReplicationMode = EGameplayEffectReplicationMode::Mixed; + + bReplicateUsingRegisteredSubObjectList = true; + + FMemory::Memset(ActivationGroupCounts, 0, sizeof(ActivationGroupCounts)); +} + +void UGGA_AbilitySystemComponent::InitializeAbilitySystem(AActor* InOwnerActor, AActor* InAvatarActor) +{ + check(InOwnerActor); + check(InAvatarActor); + + if (!bAbilitySystemInitialized) + { + FGameplayAbilityActorInfo* ActorInfo = AbilityActorInfo.Get(); + const bool AvatarChanged = InAvatarActor && (InAvatarActor != ActorInfo->AvatarActor); + InitAbilityActorInfo(InOwnerActor, InAvatarActor); + InitializeAbilitySets(InOwnerActor, InAvatarActor); + if (AttributeSetInitializeGroupName.IsValid()) + { + InitializeAttributes(AttributeSetInitializeGroupName, AttributeSetInitializeLevel, true); + } + bAbilitySystemInitialized = true; + OnAbilitySystemInitialized.Broadcast(); + } +} + +void UGGA_AbilitySystemComponent::UninitializeAbilitySystem() +{ + if (bAbilitySystemInitialized) + { + bAbilitySystemInitialized = false; + OnAbilitySystemUninitialized.Broadcast(); + } +} + + +void UGGA_AbilitySystemComponent::InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor) +{ + FGameplayAbilityActorInfo* ActorInfo = AbilityActorInfo.Get(); + check(ActorInfo); + check(InOwnerActor); + + const bool AvatarChanged = InAvatarActor && (InAvatarActor != ActorInfo->AvatarActor); + + Super::InitAbilityActorInfo(InOwnerActor, InAvatarActor); + + if (GetWorld() && !GetWorld()->IsGameWorld()) + { + return; + } + + if (AvatarChanged) + { + RegisterToGlobalAbilitySystem(); + + ABILITYLIST_SCOPE_LOCK(); + for (const FGameplayAbilitySpec& AbilitySpec : ActivatableAbilities.Items) + { + if (IGGA_GameplayAbilityInterface* AbilityCDO = Cast(AbilitySpec.Ability)) + { + AbilityCDO->TryActivateAbilityOnSpawn(AbilityActorInfo.Get(), AbilitySpec); + } + } + } +} + +void UGGA_AbilitySystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UnregisterToGlobalAbilitySystem(); + Super::EndPlay(EndPlayReason); +} + +void UGGA_AbilitySystemComponent::InitializeComponent() +{ + SetReplicationMode(AbilitySystemReplicationMode); + Super::InitializeComponent(); +} + +void UGGA_AbilitySystemComponent::InitializeAbilitySets(AActor* InOwnerActor, AActor* InAvatarActor) +{ + if (GetNetMode() != NM_Client) + { + for (int32 i = DefaultAbilitySet_GrantedHandles.Num() - 1; i >= 0; i--) + { + DefaultAbilitySet_GrantedHandles[i].TakeFromAbilitySystem(this); + } + DefaultAbilitySet_GrantedHandles.Empty(); + + for (TObjectPtr AbilitySet : DefaultAbilitySets) + { + if (!AbilitySet) + continue; + AbilitySet->GiveToAbilitySystem(this, /*inout*/ &DefaultAbilitySet_GrantedHandles.AddDefaulted_GetRef(), this); + } + } +} + +void UGGA_AbilitySystemComponent::InitializeAttributes(FGGA_AttributeGroupName GroupName, int32 Level, bool bInitialInit) +{ + if (const UGGA_AbilitySystemGlobals* Globals = Cast(UGGA_AbilitySystemGlobals::GetAbilitySystemGlobals())) + { + Globals->InitAttributeSetDefaults(this, GroupName, Level, bInitialInit); + } + else + { + UE_LOG(LogGGA_AbilitySystem, Warning, TEXT("Failed to InitializeAttributes as your project is not configured to use GGA_AbilitySystemGlobals(or derived class).")); + } +} + +void UGGA_AbilitySystemComponent::SendGameplayEventToActor_Replicated(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) +{ + if (IsValid(Actor) && EventTag.IsValid()) + { + if (Actor->HasAuthority()) + { + MulticastSendGameplayEventToActor(Actor, EventTag, Payload); + } + else + { + ServerSendGameplayEventToActor(Actor, EventTag, Payload); + } + } +} + +void UGGA_AbilitySystemComponent::ServerSendGameplayEventToActor_Implementation(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) +{ + MulticastSendGameplayEventToActor(Actor, EventTag, Payload); +} + +bool UGGA_AbilitySystemComponent::ServerSendGameplayEventToActor_Validate(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) +{ + return true; +} + +void UGGA_AbilitySystemComponent::MulticastSendGameplayEventToActor_Implementation(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) +{ + UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Actor, EventTag, Payload); +} + +void UGGA_AbilitySystemComponent::PostInitProperties() +{ + Super::PostInitProperties(); + ReplicationMode = AbilitySystemReplicationMode; +} + +void UGGA_AbilitySystemComponent::RegisterToGlobalAbilitySystem() +{ + if (bRegisteredToGlobalAbilitySystem) + return; + // Register with the global system once we actually have a pawn avatar. We wait until this time since some globally-applied effects may require an avatar. + if (UGGA_GlobalAbilitySystem* GlobalAbilitySystem = UWorld::GetSubsystem(GetWorld())) + { + GlobalAbilitySystem->RegisterASC(this); + bRegisteredToGlobalAbilitySystem = true; + } +} + +void UGGA_AbilitySystemComponent::UnregisterToGlobalAbilitySystem() +{ + if (!bRegisteredToGlobalAbilitySystem) + return; + if (UGGA_GlobalAbilitySystem* GlobalAbilitySystem = UWorld::GetSubsystem(GetWorld())) + { + GlobalAbilitySystem->UnregisterASC(this); + bRegisteredToGlobalAbilitySystem = false; + } +} + +#pragma endregion + +#pragma region AbilitiesActivation + +bool UGGA_AbilitySystemComponent::IsActivationGroupBlocked(EGGA_AbilityActivationGroup Group) const +{ + bool bBlocked = false; + + switch (Group) + { + case EGGA_AbilityActivationGroup::Independent: + // Independent abilities are never blocked. + bBlocked = false; + break; + + case EGGA_AbilityActivationGroup::Exclusive_Replaceable: + case EGGA_AbilityActivationGroup::Exclusive_Blocking: + // Exclusive abilities can activate if nothing is blocking. + bBlocked = (ActivationGroupCounts[(uint8)EGGA_AbilityActivationGroup::Exclusive_Blocking] > 0); + break; + + default: + checkf(false, TEXT("IsActivationGroupBlocked: Invalid ActivationGroup [%d]\n"), (uint8)Group); + break; + } + + return bBlocked; +} + +void UGGA_AbilitySystemComponent::AddAbilityToActivationGroup(EGGA_AbilityActivationGroup Group, UGameplayAbility* Ability) +{ + check(Ability); + check(ActivationGroupCounts[(uint8)Group] < INT32_MAX); + + ActivationGroupCounts[(uint8)Group]++; + + const bool bReplicateCancelAbility = false; + + switch (Group) + { + case EGGA_AbilityActivationGroup::Independent: + // Independent abilities do not cancel any other abilities. + break; + + case EGGA_AbilityActivationGroup::Exclusive_Replaceable: + case EGGA_AbilityActivationGroup::Exclusive_Blocking: + CancelActivationGroupAbilities(EGGA_AbilityActivationGroup::Exclusive_Replaceable, Ability, bReplicateCancelAbility); + break; + + default: + checkf(false, TEXT("AddAbilityToActivationGroup: In valid ActivationGroup [%d]\n"), (uint8)Group); + break; + } + + const int32 ExclusiveCount = ActivationGroupCounts[(uint8)EGGA_AbilityActivationGroup::Exclusive_Replaceable] + ActivationGroupCounts[(uint8)EGGA_AbilityActivationGroup::Exclusive_Blocking]; + if (!ensure(ExclusiveCount <= 1)) + { + UE_LOG(LogGGA_AbilitySystem, Error, TEXT("AddAbilityToActivationGroup: Multiple exclusive abilities are running.")); + } +} + +void UGGA_AbilitySystemComponent::RemoveAbilityFromActivationGroup(EGGA_AbilityActivationGroup Group, UGameplayAbility* Ability) +{ + check(Ability); + check(ActivationGroupCounts[(uint8)Group] > 0); + + ActivationGroupCounts[(uint8)Group]--; +} + +bool UGGA_AbilitySystemComponent::CanChangeActivationGroup(EGGA_AbilityActivationGroup NewGroup, UGameplayAbility* Ability) const +{ + if (Ability == nullptr || !Ability->IsInstantiated() || !Ability->IsActive()) + { + return false; + } + + IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability); + if (AbilityInterface == nullptr) + { + return false; + } + + if (AbilityInterface->GetActivationGroup() == NewGroup) + { + return true; + } + + + if ((AbilityInterface->GetActivationGroup() != EGGA_AbilityActivationGroup::Exclusive_Blocking) && IsActivationGroupBlocked(NewGroup)) + { + // This ability can't change groups if it's blocked (unless it is the one doing the blocking). + return false; + } + + if ((NewGroup == EGGA_AbilityActivationGroup::Exclusive_Replaceable) && !Ability->CanBeCanceled()) + { + // This ability can't become replaceable if it can't be canceled. + return false; + } + + return true; +} + +bool UGGA_AbilitySystemComponent::ChangeActivationGroup(EGGA_AbilityActivationGroup NewGroup, UGameplayAbility* Ability) +{ + if (!CanChangeActivationGroup(NewGroup, Ability)) + { + return false; + } + + IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability); + if (AbilityInterface == nullptr) + { + return false; + } + + if (AbilityInterface->GetActivationGroup() != NewGroup) + { + RemoveAbilityFromActivationGroup(AbilityInterface->GetActivationGroup(), Ability); + AddAbilityToActivationGroup(NewGroup, Ability); + AbilityInterface->SetActivationGroup(NewGroup); + } + + return true; +} + +void UGGA_AbilitySystemComponent::NotifyAbilityActivated(const FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability) +{ + Super::NotifyAbilityActivated(Handle, Ability); + + if (IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability)) + { + AddAbilityToActivationGroup(AbilityInterface->GetActivationGroup(), Ability); + } + + OnAbilityActivated.Broadcast(Handle, Ability); +} + +void UGGA_AbilitySystemComponent::NotifyAbilityFailed(const FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason) +{ + Super::NotifyAbilityFailed(Handle, Ability, FailureReason); + + if (APawn* Avatar = Cast(GetAvatarActor())) + { + if (!Avatar->IsLocallyControlled() && Ability->IsSupportedForNetworking()) + { + ClientNotifyAbilityActivationFailed(Ability, FailureReason); + return; + } + } + + HandleAbilityActivationFailed(Ability, FailureReason); +} + +void UGGA_AbilitySystemComponent::ClientNotifyAbilityActivationFailed_Implementation(const UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason) +{ + HandleAbilityActivationFailed(Ability, FailureReason); +} + +void UGGA_AbilitySystemComponent::HandleAbilityActivationFailed(const UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason) +{ + OnAbilityActivationFailed.Broadcast(Ability, FailureReason); + + if (const IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability)) + { + AbilityInterface->HandleActivationFailed(FailureReason); + } +} +#pragma endregion + +#pragma region AbilityCancellation + +void UGGA_AbilitySystemComponent::CancelAbilitiesByFunc(TShouldCancelAbilityFunc ShouldCancelFunc, bool bReplicateCancelAbility) +{ + ABILITYLIST_SCOPE_LOCK(); + for (const FGameplayAbilitySpec& AbilitySpec : ActivatableAbilities.Items) + { + if (AbilitySpec.Ability == nullptr || !AbilitySpec.IsActive()) + { + continue; + } + +#if ENGINE_MAJOR_VERSION >= 4 && ENGINE_MINOR_VERSION <5 + if (AbilitySpec.Ability->GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced) +#endif + { + // Cancel all the spawned instances, not the CDO. + TArray Instances = AbilitySpec.GetAbilityInstances(); + for (UGameplayAbility* AbilityInstance : Instances) + { + if (ShouldCancelFunc(AbilityInstance, AbilitySpec.Handle)) + { + if (AbilityInstance->CanBeCanceled()) + { + AbilityInstance->CancelAbility(AbilitySpec.Handle, AbilityActorInfo.Get(), AbilityInstance->GetCurrentActivationInfo(), bReplicateCancelAbility); + } + else + { + UE_LOG(LogGGA_AbilitySystem, Error, TEXT("CancelAbilitiesByFunc: Can't cancel ability [%s] because CanBeCanceled is false."), *AbilityInstance->GetName()); + } + } + } + } +#if ENGINE_MAJOR_VERSION >= 4 && ENGINE_MINOR_VERSION <5 + else + { + // Cancel the non-instanced ability CDO. + if (ShouldCancelFunc(AbilitySpec.Ability, AbilitySpec.Handle)) + { + // Non-instanced abilities can always be canceled. + check(AbilitySpec.Ability->CanBeCanceled()); + AbilitySpec.Ability->CancelAbility(AbilitySpec.Handle, AbilityActorInfo.Get(), FGameplayAbilityActivationInfo(), bReplicateCancelAbility); + } + } +#endif + } +} + +void UGGA_AbilitySystemComponent::CancelActivationGroupAbilities(EGGA_AbilityActivationGroup Group, UGameplayAbility* IgnoreAbility, bool bReplicateCancelAbility) +{ + auto ShouldCancelFunc = [this, Group, IgnoreAbility](const UGameplayAbility* Ability, FGameplayAbilitySpecHandle Handle) + { + bool SameGroup = false; + if (const IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability)) + { + SameGroup = AbilityInterface->GetActivationGroup() == Group; + } + return (SameGroup && (Ability != IgnoreAbility)); + }; + + CancelAbilitiesByFunc(ShouldCancelFunc, bReplicateCancelAbility); +} + +void UGGA_AbilitySystemComponent::NotifyAbilityEnded(FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability, bool bWasCancelled) +{ + Super::NotifyAbilityEnded(Handle, Ability, bWasCancelled); + + if (const IGGA_GameplayAbilityInterface* AbilityInterface = Cast(Ability)) + { + RemoveAbilityFromActivationGroup(AbilityInterface->GetActivationGroup(), Ability); + } + + AbilityEndedEvent.Broadcast(Handle, Ability, bWasCancelled); +} + +void UGGA_AbilitySystemComponent::HandleChangeAbilityCanBeCanceled(const FGameplayTagContainer& AbilityTags, UGameplayAbility* RequestingAbility, bool bCanBeCanceled) +{ + Super::HandleChangeAbilityCanBeCanceled(AbilityTags, RequestingAbility, bCanBeCanceled); + + //@TODO: Apply any special logic like blocking input or movement +} + +#pragma endregion + +#pragma region Abilities + +bool UGGA_AbilitySystemComponent::GetCooldownRemainingForTags(FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration) +{ + if (CooldownTags.Num() > 0) + { + TimeRemaining = 0.f; + CooldownDuration = 0.f; + + FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags); + TArray> DurationAndTimeRemaining = GetActiveEffectsTimeRemainingAndDuration(Query); + if (DurationAndTimeRemaining.Num() > 0) + { + int32 BestIdx = 0; + float LongestTime = DurationAndTimeRemaining[0].Key; + for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx) + { + if (DurationAndTimeRemaining[Idx].Key > LongestTime) + { + LongestTime = DurationAndTimeRemaining[Idx].Key; + BestIdx = Idx; + } + } + + TimeRemaining = DurationAndTimeRemaining[BestIdx].Key; + CooldownDuration = DurationAndTimeRemaining[BestIdx].Value; + + return true; + } + } + return false; +} + +bool UGGA_AbilitySystemComponent::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, + bool EndAbilityImmediately) +{ + bool AbilityActivated = false; + if (InAbilityHandle.IsValid()) + { + FScopedServerAbilityRPCBatcher AbilityRpcBatching(this, InAbilityHandle); + AbilityActivated = TryActivateAbility(InAbilityHandle, true); + + if (EndAbilityImmediately) + { + FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(InAbilityHandle); + if (AbilitySpec) + { + if (IGGA_GameplayAbilityInterface* AbilityInterface = Cast(AbilitySpec->GetPrimaryInstance())) + { + AbilityInterface->ExternalEndAbility(); + } + } + } + + return AbilityActivated; + } + + return AbilityActivated; +} +#pragma endregion + + +#pragma region GameplayTags +void UGGA_AbilitySystemComponent::GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const +{ + TagContainer.Reset(); // Fix for Version under 5.2 + TagContainer.AppendTags(GameplayTagCountContainer.GetExplicitGameplayTags()); +} + +FString UGGA_AbilitySystemComponent::GetOwnedGameplayTagsString() +{ + FString BlockedTagsStrings; + for (auto Tag : GameplayTagCountContainer.GetExplicitGameplayTags()) + { + BlockedTagsStrings.Append(FString::Printf(TEXT("%s (%d),\n"), *Tag.ToString(), GameplayTagCountContainer.GetTagCount(Tag))); + } + return BlockedTagsStrings; +} + +void UGGA_AbilitySystemComponent::GetAdditionalActivationTagRequirements(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer& OutActivationRequired, + FGameplayTagContainer& OutActivationBlocked) const +{ + if (TagRelationshipMapping) + { + FGameplayTagContainer ActorTags; + GetOwnedGameplayTags(ActorTags); + TagRelationshipMapping->GetRequiredAndBlockedActivationTagsV2(ActorTags, AbilityTags, &OutActivationRequired, &OutActivationBlocked); + // TagRelationshipMapping->GetRequiredAndBlockedActivationTags(AbilityTags, &OutActivationRequired, &OutActivationBlocked); + } +} + +void UGGA_AbilitySystemComponent::ApplyAbilityBlockAndCancelTags(const FGameplayTagContainer& AbilityTags, UGameplayAbility* RequestingAbility, bool bEnableBlockTags, + const FGameplayTagContainer& BlockTags, bool bExecuteCancelTags, const FGameplayTagContainer& CancelTags) +{ + FGameplayTagContainer ModifiedBlockTags = BlockTags; + FGameplayTagContainer ModifiedCancelTags = CancelTags; + + if (TagRelationshipMapping) + { + FGameplayTagContainer ActorTags; + GetOwnedGameplayTags(ActorTags); + // Use the mapping to expand the ability tags into block and cancel tag + TagRelationshipMapping->GetAbilityTagsToBlockAndCancelV2(ActorTags, AbilityTags, &ModifiedBlockTags, &ModifiedCancelTags); + // TagRelationshipMapping->GetAbilityTagsToBlockAndCancel(AbilityTags, &ModifiedBlockTags, &ModifiedCancelTags); + } + + Super::ApplyAbilityBlockAndCancelTags(AbilityTags, RequestingAbility, bEnableBlockTags, ModifiedBlockTags, bExecuteCancelTags, ModifiedCancelTags); + + //@TODO: Apply any special logic like blocking input or movement +} + +void UGGA_AbilitySystemComponent::SetTagRelationshipMapping(UGGA_AbilityTagRelationshipMapping* NewMapping) +{ + TagRelationshipMapping = NewMapping; +} + +#pragma endregion + +#pragma region Attributes + +FString UGGA_AbilitySystemComponent::GetOwnedGameplayAttributeSetString() +{ + FString AttributeSetString; + TArray Attributes; + GetAllAttributes(Attributes); + for (const auto& Attribute : Attributes) + { + AttributeSetString.Append( + FString::Printf(TEXT("%s : %.2f \n"), *Attribute.GetName(), GetNumericAttribute(Attribute))); + } + return AttributeSetString; +} + +#pragma endregion + +#pragma region TargetData +void UGGA_AbilitySystemComponent::GetAbilityTargetData(const FGameplayAbilitySpecHandle AbilityHandle, FGameplayAbilityActivationInfo ActivationInfo, + FGameplayAbilityTargetDataHandle& OutTargetDataHandle) +{ + TSharedPtr ReplicatedData = AbilityTargetDataMap.Find(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, ActivationInfo.GetActivationPredictionKey())); + if (ReplicatedData.IsValid()) + { + OutTargetDataHandle = ReplicatedData->TargetData; + } +} +#pragma endregion diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemStructLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemStructLibrary.cpp new file mode 100644 index 0000000..ec9f2d2 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilitySystemStructLibrary.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_AbilitySystemStructLibrary.h" + + +bool FGGA_GameplayEffectContainerSpec::HasValidEffects() const +{ + return TargetGameplayEffectSpecs.Num() > 0; +} + +bool FGGA_GameplayEffectContainerSpec::HasValidTargets() const +{ + return TargetData.Num() > 0; +} + +void FGGA_GameplayEffectContainerSpec::AddTargets(const TArray& HitResults, const TArray& TargetActors) +{ + for (const FHitResult& HitResult : HitResults) + { + FGameplayAbilityTargetData_SingleTargetHit* NewData = new FGameplayAbilityTargetData_SingleTargetHit(HitResult); + TargetData.Add(NewData); + } + + if (TargetActors.Num() > 0) + { + FGameplayAbilityTargetData_ActorArray* NewData = new FGameplayAbilityTargetData_ActorArray(); + NewData->TargetActorArray.Append(TargetActors); + TargetData.Add(NewData); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilityTagRelationshipMapping.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilityTagRelationshipMapping.cpp new file mode 100644 index 0000000..7da9ba0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_AbilityTagRelationshipMapping.cpp @@ -0,0 +1,180 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GGA_AbilityTagRelationshipMapping.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GGA_AbilityTagRelationshipMapping) + +void UGGA_AbilityTagRelationshipMapping::GetAbilityTagsToBlockAndCancelV2(const FGameplayTagContainer& ActorTags, const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutTagsToBlock, + FGameplayTagContainer* OutTagsToCancel) const +{ + TArray AbilitiesWithLayeredRule; + for (int32 i = 0; i < Layered.Num(); i++) + { + if (!ActorTags.IsEmpty() && Layered[i].ActorTagQuery.Matches(ActorTags)) + { + const TArray& LayeredAbilityTagRelationships = Layered[i].AbilityTagRelationships; + + // Simple iteration for now + for (int32 j = 0; j < LayeredAbilityTagRelationships.Num(); j++) + { + const FGGA_AbilityTagRelationship& Tags = LayeredAbilityTagRelationships[j]; + if (AbilityTags.HasTag(Tags.AbilityTag)) + { + if (OutTagsToBlock) + { + OutTagsToBlock->AppendTags(Tags.AbilityTagsToBlock); + } + if (OutTagsToCancel) + { + OutTagsToCancel->AppendTags(Tags.AbilityTagsToCancel); + } + AbilitiesWithLayeredRule.Add(Tags.AbilityTag); + } + } + } + } + + // Simple iteration for now + for (int32 i = 0; i < AbilityTagRelationships.Num(); i++) + { + const FGGA_AbilityTagRelationship& Tags = AbilityTagRelationships[i]; + if (AbilityTags.HasTag(Tags.AbilityTag) && !AbilitiesWithLayeredRule.Contains(Tags.AbilityTag)) + { + if (OutTagsToBlock) + { + OutTagsToBlock->AppendTags(Tags.AbilityTagsToBlock); + } + if (OutTagsToCancel) + { + OutTagsToCancel->AppendTags(Tags.AbilityTagsToCancel); + } + } + } +} + +void UGGA_AbilityTagRelationshipMapping::GetAbilityTagsToBlockAndCancel(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutTagsToBlock, FGameplayTagContainer* OutTagsToCancel) const +{ + // Simple iteration for now + for (int32 i = 0; i < AbilityTagRelationships.Num(); i++) + { + const FGGA_AbilityTagRelationship& Tags = AbilityTagRelationships[i]; + if (AbilityTags.HasTag(Tags.AbilityTag)) + { + if (OutTagsToBlock) + { + OutTagsToBlock->AppendTags(Tags.AbilityTagsToBlock); + } + if (OutTagsToCancel) + { + OutTagsToCancel->AppendTags(Tags.AbilityTagsToCancel); + } + } + } +} + +void UGGA_AbilityTagRelationshipMapping::GetRequiredAndBlockedActivationTagsV2(const FGameplayTagContainer& ActorTags, const FGameplayTagContainer& AbilityTags, + FGameplayTagContainer* OutActivationRequired, FGameplayTagContainer* OutActivationBlocked) const +{ + TArray AbilitiesWithLayeredRule; + + for (int32 i = 0; i < Layered.Num(); i++) + { + if (!ActorTags.IsEmpty() && Layered[i].ActorTagQuery.Matches(ActorTags)) + { + const TArray& LayeredAbilityTagRelationships = Layered[i].AbilityTagRelationships; + + // Simple iteration for now + for (int32 j = 0; j < LayeredAbilityTagRelationships.Num(); j++) + { + const FGGA_AbilityTagRelationship& Tags = LayeredAbilityTagRelationships[j]; + if (AbilityTags.HasTag(Tags.AbilityTag)) + { + if (OutActivationRequired) + { + OutActivationRequired->AppendTags(Tags.ActivationRequiredTags); + } + if (OutActivationBlocked) + { + OutActivationBlocked->AppendTags(Tags.ActivationBlockedTags); + } + AbilitiesWithLayeredRule.Add(Tags.AbilityTag); + } + } + } + } + + // Simple iteration for now + for (int32 i = 0; i < AbilityTagRelationships.Num(); i++) + { + const FGGA_AbilityTagRelationship& Tags = AbilityTagRelationships[i]; + if (AbilityTags.HasTag(Tags.AbilityTag) && !AbilitiesWithLayeredRule.Contains(Tags.AbilityTag)) + { + if (OutActivationRequired) + { + OutActivationRequired->AppendTags(Tags.ActivationRequiredTags); + } + if (OutActivationBlocked) + { + OutActivationBlocked->AppendTags(Tags.ActivationBlockedTags); + } + } + } +} + +void UGGA_AbilityTagRelationshipMapping::GetRequiredAndBlockedActivationTags(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutActivationRequired, + FGameplayTagContainer* OutActivationBlocked) const +{ + // Simple iteration for now + for (int32 i = 0; i < AbilityTagRelationships.Num(); i++) + { + const FGGA_AbilityTagRelationship& Tags = AbilityTagRelationships[i]; + if (AbilityTags.HasTag(Tags.AbilityTag)) + { + if (OutActivationRequired) + { + OutActivationRequired->AppendTags(Tags.ActivationRequiredTags); + } + if (OutActivationBlocked) + { + OutActivationBlocked->AppendTags(Tags.ActivationBlockedTags); + } + } + } +} + +bool UGGA_AbilityTagRelationshipMapping::IsAbilityCancelledByTag(const FGameplayTagContainer& AbilityTags, const FGameplayTag& ActionTag) const +{ + // Simple iteration for now + for (int32 i = 0; i < AbilityTagRelationships.Num(); i++) + { + const FGGA_AbilityTagRelationship& Tags = AbilityTagRelationships[i]; + + if (Tags.AbilityTag == ActionTag && Tags.AbilityTagsToCancel.HasAny(AbilityTags)) + { + return true; + } + } + + return false; +} + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGGA_AbilityTagRelationshipMapping::PreSave(FObjectPreSaveContext SaveContext) +{ + for (FGGA_AbilityTagRelationship& Rel : AbilityTagRelationships) + { + Rel.EditorFriendlyName = Rel.DevDescription.IsEmpty() ? Rel.AbilityTag.ToString() : Rel.DevDescription; + } + for (FGGA_AbilityTagRelationshipsWithQuery& RelationShips : Layered) + { + RelationShips.EditorFriendlyName = RelationShips.ActorTagQuery.IsEmpty() ? TEXT("Empty Query") : RelationShips.ActorTagQuery.GetDescription(); + for (FGGA_AbilityTagRelationship& AbilityTagRelationship : RelationShips.AbilityTagRelationships) + { + AbilityTagRelationship.EditorFriendlyName = AbilityTagRelationship.DevDescription.IsEmpty() ? AbilityTagRelationship.AbilityTag.ToString() : AbilityTagRelationship.DevDescription; + } + } + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GameplayTags.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GameplayTags.cpp new file mode 100644 index 0000000..7fbc357 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GameplayTags.cpp @@ -0,0 +1,24 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_GameplayTags.h" + +namespace GGA_AbilityActivateFailTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(IsDead, "GGF.Ability.ActivateFail.IsDead", "Ability failed to activate because its owner is dead."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Cooldown, "GGF.Ability.ActivateFail.Cooldown", "Ability failed to activate because it is on cool down."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Cost, "GGF.Ability.ActivateFail.Cost", "Ability failed to activate because it did not pass the cost checks."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(TagsBlocked, "GGF.Ability.ActivateFail.TagsBlocked", "Ability failed to activate because tags are blocking it."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(TagsMissing, "GGF.Ability.ActivateFail.TagsMissing", "Ability failed to activate because tags are missing."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Networking, "GGF.Ability.ActivateFail.Networking", "Ability failed to activate because it did not pass the network checks."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(ActivationGroup, "GGF.Ability.ActivateFail.ActivationGroup", "Ability failed to activate because of its activation group."); +} + +namespace GGA_AbilityTraitTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(ActivationOnSpawn, "GGF.Ability.Trait.ActivationOnSpawn", "Abilities with this tag will be activated right after granted."); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Persistent, "GGF.Ability.Trait.Persistent", "Abilities with this tag should be persistent during gameplay."); + +} + + diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GlobalAbilitySystem.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GlobalAbilitySystem.cpp new file mode 100644 index 0000000..f8519ac --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_GlobalAbilitySystem.cpp @@ -0,0 +1,154 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GGA_GlobalAbilitySystem.h" +#include "GGA_AbilitySystemComponent.h" +#include "Abilities/GameplayAbility.h" + + +void FGGA_GlobalAppliedAbilityList::AddToASC(TSubclassOf Ability, UGGA_AbilitySystemComponent* ASC) +{ + if (FGameplayAbilitySpecHandle* SpecHandle = Handles.Find(ASC)) + { + RemoveFromASC(ASC); + } + + UGameplayAbility* AbilityCDO = Ability->GetDefaultObject(); + FGameplayAbilitySpec AbilitySpec(AbilityCDO); + const FGameplayAbilitySpecHandle AbilitySpecHandle = ASC->GiveAbility(AbilitySpec); + Handles.Add(ASC, AbilitySpecHandle); +} + +void FGGA_GlobalAppliedAbilityList::RemoveFromASC(UGGA_AbilitySystemComponent* ASC) +{ + if (FGameplayAbilitySpecHandle* SpecHandle = Handles.Find(ASC)) + { + ASC->ClearAbility(*SpecHandle); + Handles.Remove(ASC); + } +} + +void FGGA_GlobalAppliedAbilityList::RemoveFromAll() +{ + for (auto& KVP : Handles) + { + if (KVP.Key != nullptr) + { + KVP.Key->ClearAbility(KVP.Value); + } + } + Handles.Empty(); +} + + + +void FGGA_GlobalAppliedEffectList::AddToASC(TSubclassOf Effect, UGGA_AbilitySystemComponent* ASC) +{ + if (FActiveGameplayEffectHandle* EffectHandle = Handles.Find(ASC)) + { + RemoveFromASC(ASC); + } + + const UGameplayEffect* GameplayEffectCDO = Effect->GetDefaultObject(); + const FActiveGameplayEffectHandle GameplayEffectHandle = ASC->ApplyGameplayEffectToSelf(GameplayEffectCDO, /*Level=*/ 1, ASC->MakeEffectContext()); + Handles.Add(ASC, GameplayEffectHandle); +} + +void FGGA_GlobalAppliedEffectList::RemoveFromASC(UGGA_AbilitySystemComponent* ASC) +{ + if (FActiveGameplayEffectHandle* EffectHandle = Handles.Find(ASC)) + { + ASC->RemoveActiveGameplayEffect(*EffectHandle); + Handles.Remove(ASC); + } +} + +void FGGA_GlobalAppliedEffectList::RemoveFromAll() +{ + for (auto& KVP : Handles) + { + if (KVP.Key != nullptr) + { + KVP.Key->RemoveActiveGameplayEffect(KVP.Value); + } + } + Handles.Empty(); +} + +UGGA_GlobalAbilitySystem::UGGA_GlobalAbilitySystem() +{ +} + +void UGGA_GlobalAbilitySystem::ApplyAbilityToAll(TSubclassOf Ability) +{ + if ((Ability.Get() != nullptr) && (!AppliedAbilities.Contains(Ability))) + { + FGGA_GlobalAppliedAbilityList& Entry = AppliedAbilities.Add(Ability); + for (UGGA_AbilitySystemComponent* ASC : RegisteredASCs) + { + Entry.AddToASC(Ability, ASC); + } + } +} + +void UGGA_GlobalAbilitySystem::ApplyEffectToAll(TSubclassOf Effect) +{ + if ((Effect.Get() != nullptr) && (!AppliedEffects.Contains(Effect))) + { + FGGA_GlobalAppliedEffectList& Entry = AppliedEffects.Add(Effect); + for (UGGA_AbilitySystemComponent* ASC : RegisteredASCs) + { + Entry.AddToASC(Effect, ASC); + } + } +} + +void UGGA_GlobalAbilitySystem::RemoveAbilityFromAll(TSubclassOf Ability) +{ + if ((Ability.Get() != nullptr) && AppliedAbilities.Contains(Ability)) + { + FGGA_GlobalAppliedAbilityList& Entry = AppliedAbilities[Ability]; + Entry.RemoveFromAll(); + AppliedAbilities.Remove(Ability); + } +} + +void UGGA_GlobalAbilitySystem::RemoveEffectFromAll(TSubclassOf Effect) +{ + if ((Effect.Get() != nullptr) && AppliedEffects.Contains(Effect)) + { + FGGA_GlobalAppliedEffectList& Entry = AppliedEffects[Effect]; + Entry.RemoveFromAll(); + AppliedEffects.Remove(Effect); + } +} + +void UGGA_GlobalAbilitySystem::RegisterASC(UGGA_AbilitySystemComponent* ASC) +{ + check(ASC); + + for (auto& Entry : AppliedAbilities) + { + Entry.Value.AddToASC(Entry.Key, ASC); + } + for (auto& Entry : AppliedEffects) + { + Entry.Value.AddToASC(Entry.Key, ASC); + } + + RegisteredASCs.AddUnique(ASC); +} + +void UGGA_GlobalAbilitySystem::UnregisterASC(UGGA_AbilitySystemComponent* ASC) +{ + check(ASC); + for (auto& Entry : AppliedAbilities) + { + Entry.Value.RemoveFromASC(ASC); + } + for (auto& Entry : AppliedEffects) + { + Entry.Value.RemoveFromASC(ASC); + } + + RegisteredASCs.Remove(ASC); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_LogChannels.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_LogChannels.cpp new file mode 100644 index 0000000..c7be8f8 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GGA_LogChannels.cpp @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_LogChannels.h" + + +DEFINE_LOG_CATEGORY(LogGGA_Ability); +DEFINE_LOG_CATEGORY(LogGGA_AbilitySystem); +DEFINE_LOG_CATEGORY(LogGGA_Tasks); diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_Character.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_Character.cpp new file mode 100644 index 0000000..e99ecd3 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_Character.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GameplayActors/GGA_Character.h" + +#include "AbilitySystemComponent.h" +#include "Components/GameFrameworkComponentManager.h" + + +AGGA_Character::AGGA_Character(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; +} + +void AGGA_Character::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AGGA_Character::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + Super::BeginPlay(); +} + +void AGGA_Character::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AGGA_Character::OnRep_Controller() +{ + Super::OnRep_Controller(); + + ReceivePlayerController(); +} + +void AGGA_Character::OnRep_PlayerState() +{ + Super::OnRep_PlayerState(); + + ReceivePlayerState(); +} + +UAbilitySystemComponent* AGGA_Character::GetAbilitySystemComponent() const +{ + if (UAbilitySystemComponent* BpProvidedASC = CustomGetAbilitySystemComponent()) + { + return BpProvidedASC; + } + return nullptr; +} + +void AGGA_Character::GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const +{ +} + + +// Called to bind functionality to input +void AGGA_Character::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + Super::SetupPlayerInputComponent(PlayerInputComponent); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_CharacterWithAbilities.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_CharacterWithAbilities.cpp new file mode 100644 index 0000000..d0b2b3d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_CharacterWithAbilities.cpp @@ -0,0 +1,21 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GameplayActors/GGA_CharacterWithAbilities.h" +#include "GGA_AbilitySystemComponent.h" + +AGGA_CharacterWithAbilities::AGGA_CharacterWithAbilities(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ + AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent")); + AbilitySystemComponent->SetIsReplicated(true); +} + +UAbilitySystemComponent* AGGA_CharacterWithAbilities::GetAbilitySystemComponent() const +{ + if (UAbilitySystemComponent* BpProvidedASC = CustomGetAbilitySystemComponent()) + { + return BpProvidedASC; + } + + return AbilitySystemComponent; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_GameState.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_GameState.cpp new file mode 100644 index 0000000..1e5035e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_GameState.cpp @@ -0,0 +1,116 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GameplayActors/GGA_GameState.h" + +#include "GGA_AbilitySystemComponent.h" +#include "Components/GameFrameworkComponentManager.h" +#include "Components/GameStateComponent.h" +#include "Containers/Array.h" + +// #include UE_INLINE_GENERATED_CPP_BY_NAME(ModularGameState) + +AGGA_GameStateBase::AGGA_GameStateBase(const FObjectInitializer& ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + + AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("AbilitySystemComponent")); + AbilitySystemComponent->SetIsReplicated(true); + AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); +} + +void AGGA_GameStateBase::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + if (AbilitySystemComponent) + { + AbilitySystemComponent->InitAbilityActorInfo(/*Owner=*/ this, /*Avatar=*/ this); + } +} + +UAbilitySystemComponent* AGGA_GameStateBase::GetAbilitySystemComponent() const +{ + return AbilitySystemComponent; +} + +void AGGA_GameStateBase::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AGGA_GameStateBase::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AGGA_GameStateBase::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + + +AGGA_GameState::AGGA_GameState(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + + AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("AbilitySystemComponent")); + AbilitySystemComponent->SetIsReplicated(true); + AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); +} + + + +UAbilitySystemComponent* AGGA_GameState::GetAbilitySystemComponent() const +{ + return AbilitySystemComponent; +} + +void AGGA_GameState::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AGGA_GameState::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + if (AbilitySystemComponent) + { + AbilitySystemComponent->InitAbilityActorInfo(/*Owner=*/ this, /*Avatar=*/ this); + } +} + +void AGGA_GameState::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AGGA_GameState::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AGGA_GameState::HandleMatchHasStarted() +{ + Super::HandleMatchHasStarted(); + + TArray ModularComponents; + GetComponents(ModularComponents); + for (UGameStateComponent* Component : ModularComponents) + { + Component->HandleMatchHasStarted(); + } +} + diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_PlayerState.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_PlayerState.cpp new file mode 100644 index 0000000..435939b --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GameplayActors/GGA_PlayerState.cpp @@ -0,0 +1,72 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GameplayActors/GGA_PlayerState.h" + +#include "Components/GameFrameworkComponentManager.h" +#include "Components/PlayerStateComponent.h" + +AGGA_PlayerState::AGGA_PlayerState(const FObjectInitializer& ObjectInitializer):Super(ObjectInitializer) +{ + AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("AbilitySystemComponent")); + AbilitySystemComponent->SetIsReplicated(true); + AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); +} + +void AGGA_PlayerState::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AGGA_PlayerState::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AGGA_PlayerState::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AGGA_PlayerState::Reset() +{ + TArray ModularComponents; + GetComponents(ModularComponents); + for (UPlayerStateComponent* Component : ModularComponents) + { + Component->Reset(); + } + Super::Reset(); +} + +void AGGA_PlayerState::ClientInitialize(AController* C) +{ + Super::ClientInitialize(C); + ReceiveClientInitialize(C); +} + +UAbilitySystemComponent* AGGA_PlayerState::GetAbilitySystemComponent() const +{ + return AbilitySystemComponent; +} + +void AGGA_PlayerState::CopyProperties(APlayerState* PlayerState) +{ + Super::CopyProperties(PlayerState); + + TInlineComponentArray PlayerStateComponents; + GetComponents(PlayerStateComponents); + for (UPlayerStateComponent* SourcePSComp : PlayerStateComponents) + { + if (UPlayerStateComponent* TargetComp = Cast(static_cast(FindObjectWithOuter(PlayerState, SourcePSComp->GetClass(), SourcePSComp->GetFName())))) + { + SourcePSComp->CopyProperties(TargetComp); + } + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/GenericGameplayAbilities.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GenericGameplayAbilities.cpp new file mode 100644 index 0000000..cd9a1c7 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/GenericGameplayAbilities.cpp @@ -0,0 +1,25 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericGameplayAbilities.h" +#include "Misc/Paths.h" +#include "GameplayTagsManager.h" + +#define LOCTEXT_NAMESPACE "FGGameplayAbilitiesModule" + + +void FGenericGameplayAbilitiesModule::StartupModule() +{ + static FString PluginName = TEXT("GenericGameplayAbilities"); + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + UGameplayTagsManager::Get().AddTagIniSearchPath(FPaths::ProjectPluginsDir() / PluginName / TEXT("Config/Tags")); +} + +void FGenericGameplayAbilitiesModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericGameplayAbilitiesModule, GenericGameplayAbilities) diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_AbilitySystemGlobals.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_AbilitySystemGlobals.cpp new file mode 100644 index 0000000..f250946 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_AbilitySystemGlobals.cpp @@ -0,0 +1,128 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_AbilitySystemGlobals.h" + +#include "GameplayEffect.h" +#include "GGA_GameplayEffectContext.h" +#include "GGA_LogChannels.h" + +void UGGA_AbilitySystemGlobals::GlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) +{ + for (TScriptInterface Receiver : Receivers) + { + Receiver->ReceiveGlobalPreGameplayEffectSpecApply(Spec, AbilitySystemComponent); + } +} + +FGameplayEffectContext* UGGA_AbilitySystemGlobals::AllocGameplayEffectContext() const +{ + return new FGGA_GameplayEffectContext(); +} + +const UAbilitySystemGlobals* UGGA_AbilitySystemGlobals::GetAbilitySystemGlobals() +{ + if (const UAbilitySystemGlobals* ASG = IGameplayAbilitiesModule::Get().GetAbilitySystemGlobals()) + { + return ASG; + } + + return nullptr; +} + +const UAbilitySystemGlobals* UGGA_AbilitySystemGlobals::GetTypedAbilitySystemGloabls(TSubclassOf DesiredClass) +{ + if (UClass* RealClass = DesiredClass) + { + if (const UAbilitySystemGlobals* ASG = IGameplayAbilitiesModule::Get().GetAbilitySystemGlobals()) + { + if (ASG->GetClass()->IsChildOf(RealClass)) + { + return ASG; + } + } + } + return nullptr; +} + +void UGGA_AbilitySystemGlobals::RegisterEventReceiver(TScriptInterface NewReceiver) +{ + UGGA_AbilitySystemGlobals* Globals = dynamic_cast(&Get()); + + if (Globals != nullptr) + { + if (NewReceiver != nullptr && IsValid(NewReceiver.GetObject()) && !Globals->Receivers.Contains(NewReceiver)) + { + Globals->Receivers.Add(NewReceiver); + UE_LOG(LogGGA_AbilitySystem, VeryVerbose, TEXT("RegisterEventReceiver:%s"), *NewReceiver.GetObject()->GetName()); + } + } +} + +void UGGA_AbilitySystemGlobals::UnregisterEventReceiver(TScriptInterface NewReceiver) +{ + UGGA_AbilitySystemGlobals* Globals = dynamic_cast(&Get()); + + if (Globals != nullptr) + { + if (NewReceiver != nullptr && IsValid(NewReceiver.GetObject()) && Globals->Receivers.Contains(NewReceiver)) + { + Globals->Receivers.Remove(NewReceiver); + UE_LOG(LogGGA_AbilitySystem, VeryVerbose, TEXT("UnregisterEventReceiver:%s"), *NewReceiver.GetObject()->GetName()); + } + } +} + +TArray UGGA_AbilitySystemGlobals::GetAttributeDefaultsTables() const +{ + return GlobalAttributeDefaultsTables; +} + +void UGGA_AbilitySystemGlobals::InitAttributeSetDefaults(UAbilitySystemComponent* AbilitySystem, const FGGA_AttributeGroupName& GroupName, int32 Level, bool bInitialInit) const +{ + if (GlobalAttributeSetInitter.IsValid()) + { + if (FAttributeSetInitter* Initter = GetAttributeSetInitter()) + { + if (GroupName.IsValid()) + { + Initter->InitAttributeSetDefaults(AbilitySystem, GroupName.GetName(), Level, bInitialInit); + } + } + } + else + { + UE_LOG(LogGGA_AbilitySystem, Warning, TEXT("You don't have any GlobalAttributeSetDefaultsTableNames configured in your AbilitySystemGlobals setting.")) + } +} + +void UGGA_AbilitySystemGlobals::ApplyAttributeDefault(UAbilitySystemComponent* AbilitySystem, FGameplayAttribute& InAttribute, const FGGA_AttributeGroupName& GroupName, int32 Level) const +{ + if (GlobalAttributeSetInitter.IsValid()) + { + if (FAttributeSetInitter* Initter = GetAttributeSetInitter()) + { + if (GroupName.IsValid()) + { + Initter->ApplyAttributeDefault(AbilitySystem, InAttribute, GroupName.GetName(), Level); + } + } + } + else + { + UE_LOG(LogGGA_AbilitySystem, Warning, TEXT("You don't have any GlobalAttributeSetDefaultsTableNames configured in your AbilitySystemGlobals setting.")) + } +} + + +void IGGA_AbilitySystemGlobalsEventReceiver::ReceiveGlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) +{ + OnGlobalPreGameplayEffectSpecApply(Spec, AbilitySystemComponent); + + FGameplayTagContainer DynamicTags; + Execute_OnGlobalPreGameplayEffectSpecApply_Bp(_getUObject(), Spec, AbilitySystemComponent, DynamicTags); + if (!DynamicTags.IsEmpty()) + { + Spec.AppendDynamicAssetTags(DynamicTags); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayAbilityTargetData_Payload.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayAbilityTargetData_Payload.cpp new file mode 100644 index 0000000..061752e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayAbilityTargetData_Payload.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_GameplayAbilityTargetData_Payload.h" diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayEffectContext.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayEffectContext.cpp new file mode 100644 index 0000000..0a8c214 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Globals/GGA_GameplayEffectContext.cpp @@ -0,0 +1,143 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Globals/GGA_GameplayEffectContext.h" + +FGameplayEffectContext* FGGA_GameplayEffectContext::Duplicate() const +{ + FGGA_GameplayEffectContext* NewContext = new FGGA_GameplayEffectContext(); + *NewContext = *this; + if (GetHitResult()) + { + // Does a deep copy of the hit result + NewContext->AddHitResult(*GetHitResult(), true); + } + return NewContext; +} + +UScriptStruct* FGGA_GameplayEffectContext::GetScriptStruct() const +{ + return StaticStruct(); +} + +//Reference this:https://www.thegames.dev/?p=62 + +bool FGGA_GameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) +{ + bool bCombinedSuccess = FGameplayEffectContext::NetSerialize(Ar, Map, bOutSuccess); + + enum RepFlag + { + REP_ContextPayloads, + REP_MAX + }; + + uint16 RepBits = 0; + if (Ar.IsSaving()) + { + if (Payloads.Num() > 0) + { + RepBits |= 1 << REP_ContextPayloads; + } + } + + Ar.SerializeBits(&RepBits, REP_MAX); + if (RepBits & (1 << REP_ContextPayloads)) + { + // or also check if Ar.IsSaving || Ar.IsLoading + bCombinedSuccess &= SafeNetSerializeTArray_WithNetSerialize<31>(Ar, Payloads, Map); + } + + return bCombinedSuccess; +} + +TArray& FGGA_GameplayEffectContext::GetPayloads() +{ + return Payloads; +} + +void FGGA_GameplayEffectContext::AddOrOverwriteData(const FInstancedStruct& DataInstance) +{ + RemovePayloadByType(DataInstance.GetScriptStruct()); + Payloads.Add(DataInstance); +} + +const FInstancedStruct* FGGA_GameplayEffectContext::FindPayloadByType(const UScriptStruct* PayloadType) const +{ + for (const FInstancedStruct& Payload : Payloads) + { + if (const UScriptStruct* CandidateStruct = Payload.GetScriptStruct()) + { + if (CandidateStruct == PayloadType) + { + return &Payload; + } + } + } + + return nullptr; +} + +FInstancedStruct* FGGA_GameplayEffectContext::FindPayloadByType(const UScriptStruct* PayloadType) +{ + for (FInstancedStruct& Payload : Payloads) + { + if (const UScriptStruct* CandidateStruct = Payload.GetScriptStruct()) + { + if (CandidateStruct == PayloadType) + { + return &Payload; + } + } + } + + return nullptr; +} + +FInstancedStruct* FGGA_GameplayEffectContext::FindOrAddPayloadByType(const UScriptStruct* PayloadType) +{ + if (FInstancedStruct* ExistingData = FindPayloadByType(PayloadType)) + { + return ExistingData; + } + + return AddPayloadByType(PayloadType); +} + +FInstancedStruct* FGGA_GameplayEffectContext::AddPayloadByType(const UScriptStruct* PayloadType) +{ + if (ensure(!FindPayloadByType(PayloadType))) + { + FInstancedStruct Data; + Data.InitializeAs(PayloadType); + AddOrOverwriteData(Data); + return FindPayloadByType(PayloadType); + } + + return nullptr; +} + +bool FGGA_GameplayEffectContext::RemovePayloadByType(const UScriptStruct* PayloadType) +{ + int32 IndexToRemove = -1; + + for (int32 i = 0; i < Payloads.Num() && IndexToRemove < 0; ++i) + { + if (const UScriptStruct* CandidateStruct = Payloads[i].GetScriptStruct()) + { + if (CandidateStruct == PayloadType) + { + IndexToRemove = i; + break; + } + } + } + + if (IndexToRemove >= 0) + { + Payloads.RemoveAt(IndexToRemove); + return true; + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Notifies/GGA_AnimNotify_SendGameplayEvent.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Notifies/GGA_AnimNotify_SendGameplayEvent.cpp new file mode 100644 index 0000000..2de3448 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Notifies/GGA_AnimNotify_SendGameplayEvent.cpp @@ -0,0 +1,49 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Notifies/GGA_AnimNotify_SendGameplayEvent.h" +#include "Components/SkeletalMeshComponent.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" +#include "GameplayTagsManager.h" + +void UGGA_AnimNotify_SendGameplayEvent::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) +{ + if (EventTag.IsValid() && MeshComp->GetOwner()) + { + UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(MeshComp->GetOwner()); + if (ASC == nullptr) + return; + FGameplayEventData EventData; + EventData.Instigator = MeshComp->GetOwner(); + EventData.EventMagnitude = EventReference.GetNotify()->GetTime(); + EventData.EventTag = EventTag; + UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(MeshComp->GetOwner(),EventTag,EventData); + } +} + +FName GetLastTagName(FGameplayTag Tag) +{ + if (!Tag.IsValid()) + { + return FName(TEXT("Invalid Tag")); + } + + TArray TagNames; + + UGameplayTagsManager::Get().SplitGameplayTagFName(Tag, TagNames); + + if (TagNames.IsEmpty()) + { + return FName(TEXT("Invalid Tag")); + } + + return TagNames.Last(); +} + +FString UGGA_AnimNotify_SendGameplayEvent::GetNotifyName_Implementation() const +{ + + FString NotifyName = FString::Format(TEXT("SendEvent:{0}"),{GetLastTagName(EventTag).ToString()}); + return NotifyName; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseAbility.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseAbility.cpp new file mode 100644 index 0000000..90ad9d1 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseAbility.cpp @@ -0,0 +1,69 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Phases/GGA_GamePhaseAbility.h" +#include "AbilitySystemComponent.h" +#include "Phases/GGA_GamePhaseSubsystem.h" +#include "Engine/World.h" + +#if WITH_EDITOR +#include "Misc/DataValidation.h" +#endif + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GGA_GamePhaseAbility) + +#define LOCTEXT_NAMESPACE "UGGA_GamePhaseAbility" + +UGGA_GamePhaseAbility::UGGA_GamePhaseAbility(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateNo; + InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor; + NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerInitiated; + NetSecurityPolicy = EGameplayAbilityNetSecurityPolicy::ServerOnly; +} + +void UGGA_GamePhaseAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) +{ + if (ActorInfo->IsNetAuthority()) + { + UWorld* World = ActorInfo->AbilitySystemComponent->GetWorld(); + UGGA_GamePhaseSubsystem* PhaseSubsystem = UWorld::GetSubsystem(World); + PhaseSubsystem->OnBeginPhase(this, Handle); + } + + Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); +} + +void UGGA_GamePhaseAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + bool bReplicateEndAbility, bool bWasCancelled) +{ + if (ActorInfo->IsNetAuthority()) + { + UWorld* World = ActorInfo->AbilitySystemComponent->GetWorld(); + UGGA_GamePhaseSubsystem* PhaseSubsystem = UWorld::GetSubsystem(World); + PhaseSubsystem->OnEndPhase(this, Handle); + } + + Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); +} + +#if WITH_EDITOR +#if ENGINE_MINOR_VERSION > 2 +EDataValidationResult UGGA_GamePhaseAbility::IsDataValid(FDataValidationContext& Context) const +{ + EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid); + + if (!GamePhaseTag.IsValid()) + { + Result = EDataValidationResult::Invalid; + Context.AddError(LOCTEXT("GamePhaseTagNotSet", "GamePhaseTag must be set to a tag representing the current phase.")); + } + + return Result; +} +#endif +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseLog.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseLog.cpp new file mode 100644 index 0000000..fd3ea74 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseLog.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Phases/GGA_GamePhaseLog.h" + +DEFINE_LOG_CATEGORY(LogGGA_GamePhase) \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseSubsystem.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseSubsystem.cpp new file mode 100644 index 0000000..c4ac883 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Phases/GGA_GamePhaseSubsystem.cpp @@ -0,0 +1,219 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Phases/GGA_GamePhaseSubsystem.h" +#include "Phases/GGA_GamePhaseAbility.h" +#include "GameplayTagsManager.h" +#include "GameFramework/GameState.h" +#include "Engine/World.h" +#include "AbilitySystemComponent.h" +#include "Abilities/GGA_GameplayAbility.h" +#include "GGA_AbilitySystemComponent.h" +#include "Phases/GGA_GamePhaseLog.h" + +////////////////////////////////////////////////////////////////////// +// UGGA_GamePhaseSubsystem + +UGGA_GamePhaseSubsystem::UGGA_GamePhaseSubsystem() +{ +} + +bool UGGA_GamePhaseSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + if (Super::ShouldCreateSubsystem(Outer)) + { + //UWorld* World = Cast(Outer); + //check(World); + + //return World->GetAuthGameMode() != nullptr; + //return nullptr; + return true; + } + + return false; +} + +bool UGGA_GamePhaseSubsystem::DoesSupportWorldType(const EWorldType::Type WorldType) const +{ + return WorldType == EWorldType::Game || WorldType == EWorldType::PIE; +} + +void UGGA_GamePhaseSubsystem::StartPhase(TSubclassOf PhaseAbility, FGGamePhaseDelegate PhaseEndedCallback) +{ + UWorld* World = GetWorld(); + UAbilitySystemComponent* GameState_ASC = World->GetGameState()->FindComponentByClass(); + if (ensure(GameState_ASC)) + { + FGameplayAbilitySpec PhaseSpec(PhaseAbility, 1, 0, this); + FGameplayAbilitySpecHandle SpecHandle = GameState_ASC->GiveAbilityAndActivateOnce(PhaseSpec); + FGameplayAbilitySpec* FoundSpec = GameState_ASC->FindAbilitySpecFromHandle(SpecHandle); + + if (FoundSpec && FoundSpec->IsActive()) + { + FGGamePhaseEntry& Entry = ActivePhaseMap.FindOrAdd(SpecHandle); + Entry.PhaseEndedCallback = PhaseEndedCallback; + } + else + { + PhaseEndedCallback.ExecuteIfBound(nullptr); + } + } +} + +void UGGA_GamePhaseSubsystem::K2_StartPhase(TSubclassOf PhaseAbility, const FGGamePhaseDynamicDelegate& PhaseEndedDelegate) +{ + const FGGamePhaseDelegate EndedDelegate = FGGamePhaseDelegate::CreateWeakLambda(const_cast(PhaseEndedDelegate.GetUObject()), [PhaseEndedDelegate](const UGGA_GamePhaseAbility* PhaseAbility) + { + PhaseEndedDelegate.ExecuteIfBound(PhaseAbility); + }); + + StartPhase(PhaseAbility, EndedDelegate); +} + +void UGGA_GamePhaseSubsystem::K2_WhenPhaseStartsOrIsActive(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, FGGamePhaseTagDynamicDelegate WhenPhaseActive) +{ + const FGGamePhaseTagDelegate ActiveDelegate = FGGamePhaseTagDelegate::CreateWeakLambda(WhenPhaseActive.GetUObject(), [WhenPhaseActive](const FGameplayTag& PhaseTag) + { + WhenPhaseActive.ExecuteIfBound(PhaseTag); + }); + + WhenPhaseStartsOrIsActive(PhaseTag, MatchType, ActiveDelegate); +} + +void UGGA_GamePhaseSubsystem::K2_WhenPhaseEnds(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, FGGamePhaseTagDynamicDelegate WhenPhaseEnd) +{ + const FGGamePhaseTagDelegate EndedDelegate = FGGamePhaseTagDelegate::CreateWeakLambda(WhenPhaseEnd.GetUObject(), [WhenPhaseEnd](const FGameplayTag& PhaseTag) + { + WhenPhaseEnd.ExecuteIfBound(PhaseTag); + }); + + WhenPhaseEnds(PhaseTag, MatchType, EndedDelegate); +} + +void UGGA_GamePhaseSubsystem::WhenPhaseStartsOrIsActive(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, const FGGamePhaseTagDelegate& WhenPhaseActive) +{ + FGPhaseObserver Observer; + Observer.PhaseTag = PhaseTag; + Observer.MatchType = MatchType; + Observer.PhaseCallback = WhenPhaseActive; + PhaseStartObservers.Add(Observer); + + if (IsPhaseActive(PhaseTag)) + { + WhenPhaseActive.ExecuteIfBound(PhaseTag); + } +} + +void UGGA_GamePhaseSubsystem::WhenPhaseEnds(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, const FGGamePhaseTagDelegate& WhenPhaseEnd) +{ + FGPhaseObserver Observer; + Observer.PhaseTag = PhaseTag; + Observer.MatchType = MatchType; + Observer.PhaseCallback = WhenPhaseEnd; + PhaseEndObservers.Add(Observer); +} + +bool UGGA_GamePhaseSubsystem::IsPhaseActive(const FGameplayTag& PhaseTag) const +{ + for (const auto& KVP : ActivePhaseMap) + { + const FGGamePhaseEntry& PhaseEntry = KVP.Value; + if (PhaseEntry.PhaseTag.MatchesTag(PhaseTag)) + { + return true; + } + } + + return false; +} + +void UGGA_GamePhaseSubsystem::OnBeginPhase(const UGGA_GamePhaseAbility* PhaseAbility, const FGameplayAbilitySpecHandle PhaseAbilityHandle) +{ + const FGameplayTag IncomingPhaseTag = PhaseAbility->GetGamePhaseTag(); + const FGameplayTag IncomingPhaseParentTag = UGameplayTagsManager::Get().RequestGameplayTagDirectParent(IncomingPhaseTag); + + UE_LOG(LogGGA_GamePhase, Log, TEXT("Beginning Phase '%s' (%s)"), *IncomingPhaseTag.ToString(), *GetNameSafe(PhaseAbility)); + + const UWorld* World = GetWorld(); + UGGA_AbilitySystemComponent* GameState_ASC = World->GetGameState()->FindComponentByClass(); + if (ensure(GameState_ASC)) + { + TArray ActivePhases; + for (const auto& KVP : ActivePhaseMap) + { + const FGameplayAbilitySpecHandle ActiveAbilityHandle = KVP.Key; + if (FGameplayAbilitySpec* Spec = GameState_ASC->FindAbilitySpecFromHandle(ActiveAbilityHandle)) + { + ActivePhases.Add(Spec); + } + } + + for (const FGameplayAbilitySpec* ActivePhase : ActivePhases) + { + const UGGA_GamePhaseAbility* ActivePhaseAbility = CastChecked(ActivePhase->Ability); + const FGameplayTag ActivePhaseTag = ActivePhaseAbility->GetGamePhaseTag(); + + // So if the active phase currently matches the incoming phase tag, we allow it. + // i.e. multiple gameplay abilities can all be associated with the same phase tag. + // For example, + // You can be in the, Game.Playing, phase, and then start a sub-phase, like Game.Playing.SuddenDeath + // Game.Playing phase will still be active, and if someone were to push another one, like, + // Game.Playing.ActualSuddenDeath, it would end Game.Playing.SuddenDeath phase, but Game.Playing would + // continue. Similarly if we activated Game.GameOver, all the Game.Playing* phases would end. + if (!ActivePhaseTag.MatchesTag(IncomingPhaseTag) && ActivePhaseTag.MatchesTag(IncomingPhaseParentTag)) + { + UE_LOG(LogGGA_GamePhase, Log, TEXT("\tEnding Phase '%s' (%s)"), *ActivePhaseTag.ToString(), *GetNameSafe(ActivePhaseAbility)); + + FGameplayAbilitySpecHandle HandleToEnd = ActivePhase->Handle; + GameState_ASC->CancelAbilitiesByFunc([HandleToEnd](const UGameplayAbility* AbilityToCancel, FGameplayAbilitySpecHandle Handle) + { + return Handle == HandleToEnd; + }, true); + } + } + + FGGamePhaseEntry& Entry = ActivePhaseMap.FindOrAdd(PhaseAbilityHandle); + Entry.PhaseTag = IncomingPhaseTag; + + // Notify all observers of this phase that it has started. + for (int32 i = 0; i < PhaseStartObservers.Num(); i++) + { + if (PhaseStartObservers[i].IsMatch(IncomingPhaseTag)) + { + PhaseStartObservers[i].PhaseCallback.ExecuteIfBound(IncomingPhaseTag); + } + } + } +} + +void UGGA_GamePhaseSubsystem::OnEndPhase(const UGGA_GamePhaseAbility* PhaseAbility, const FGameplayAbilitySpecHandle PhaseAbilityHandle) +{ + const FGameplayTag EndedPhaseTag = PhaseAbility->GetGamePhaseTag(); + UE_LOG(LogGGA_GamePhase, Log, TEXT("Ended Phase '%s' (%s)"), *EndedPhaseTag.ToString(), *GetNameSafe(PhaseAbility)); + + const FGGamePhaseEntry& Entry = ActivePhaseMap.FindChecked(PhaseAbilityHandle); + Entry.PhaseEndedCallback.ExecuteIfBound(PhaseAbility); + + ActivePhaseMap.Remove(PhaseAbilityHandle); + + // Notify all observers of this phase that it has ended. + for (int32 i = 0; i < PhaseEndObservers.Num(); i++) + { + if (PhaseEndObservers[i].IsMatch(EndedPhaseTag)) + { + PhaseEndObservers[i].PhaseCallback.ExecuteIfBound(EndedPhaseTag); + } + } +} + +bool UGGA_GamePhaseSubsystem::FGPhaseObserver::IsMatch(const FGameplayTag& ComparePhaseTag) const +{ + switch (MatchType) + { + case EGGA_PhaseTagMatchType::ExactMatch: + return ComparePhaseTag == PhaseTag; + case EGGA_PhaseTagMatchType::PartialMatch: + return ComparePhaseTag.MatchesTag(PhaseTag); + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_LineTrace.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_LineTrace.cpp new file mode 100644 index 0000000..dc85310 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_LineTrace.cpp @@ -0,0 +1,124 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetActors/GGA_AbilityTargetActor_LineTrace.h" +#include "DrawDebugHelpers.h" +#include "GameFramework/PlayerController.h" + +AGGA_AbilityTargetActor_LineTrace::AGGA_AbilityTargetActor_LineTrace() +{ +} + +void AGGA_AbilityTargetActor_LineTrace::Configure( + const FGameplayAbilityTargetingLocationInfo& InStartLocation, + FGameplayTag InAimingTag, + FGameplayTag InAimingRemovalTag, + FCollisionProfileName InTraceProfile, + FGameplayTargetDataFilterHandle InFilter, + TSubclassOf InReticleClass, + FWorldReticleParameters InReticleParams, + bool bInIgnoreBlockingHits, + bool bInShouldProduceTargetDataOnServer, + bool bInUsePersistentHitResults, + bool bInDebug, + bool bInTraceAffectsAimPitch, + bool bInTraceFromPlayerViewPoint, + bool bInUseAimingSpreadMod, + float InMaxRange, + float InBaseSpread, + float InAimingSpreadMod, + float InTargetingSpreadIncrement, + float InTargetingSpreadMax, + int32 InMaxHitResultsPerTrace, + int32 InNumberOfTraces) +{ + StartLocation = InStartLocation; + AimingTag = InAimingTag; + AimingRemovalTag = InAimingRemovalTag; + TraceProfile = InTraceProfile; + Filter = InFilter; + ReticleClass = InReticleClass; + ReticleParams = InReticleParams; + bIgnoreBlockingHits = bInIgnoreBlockingHits; + ShouldProduceTargetDataOnServer = bInShouldProduceTargetDataOnServer; + bUsePersistentHitResults = bInUsePersistentHitResults; + bDebug = bInDebug; + bTraceAffectsAimPitch = bInTraceAffectsAimPitch; + bTraceFromPlayerViewPoint = bInTraceFromPlayerViewPoint; + bUseAimingSpreadMod = bInUseAimingSpreadMod; + MaxRange = InMaxRange; + BaseSpread = InBaseSpread; + AimingSpreadMod = InAimingSpreadMod; + TargetingSpreadIncrement = InTargetingSpreadIncrement; + TargetingSpreadMax = InTargetingSpreadMax; + MaxHitResultsPerTrace = InMaxHitResultsPerTrace; + NumberOfTraces = InNumberOfTraces; + + if (bUsePersistentHitResults) + { + NumberOfTraces = 1; + } +} + +void AGGA_AbilityTargetActor_LineTrace::DoTrace(TArray& HitResults, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, FName ProfileName, + const FCollisionQueryParams Params) +{ + LineTraceWithFilter(HitResults, World, FilterHandle, Start, End, ProfileName, Params); +} + +void AGGA_AbilityTargetActor_LineTrace::ShowDebugTrace(TArray& HitResults, EDrawDebugTrace::Type DrawDebugType, float Duration) +{ +#if ENABLE_DRAW_DEBUG + if (bDebug) + { + FVector ViewStart = StartLocation.GetTargetingTransform().GetLocation(); + FRotator ViewRot; + if (PrimaryPC && bTraceFromPlayerViewPoint) + { + PrimaryPC->GetPlayerViewPoint(ViewStart, ViewRot); + } + + FVector TraceEnd = HitResults[0].TraceEnd; + if (NumberOfTraces > 1 || bUsePersistentHitResults) + { + TraceEnd = CurrentTraceEnd; + } + + DrawDebugLineTraceMulti(GetWorld(), ViewStart, TraceEnd, DrawDebugType, true, HitResults, FLinearColor::Green, FLinearColor::Red, Duration); + } +#endif +} + +#if ENABLE_DRAW_DEBUG +// Copied from KismetTraceUtils.cpp +void AGGA_AbilityTargetActor_LineTrace::DrawDebugLineTraceMulti(const UWorld* World, const FVector& Start, const FVector& End, EDrawDebugTrace::Type DrawDebugType, bool bHit, const TArray& OutHits, + FLinearColor TraceColor, FLinearColor TraceHitColor, float DrawTime) +{ + if (DrawDebugType != EDrawDebugTrace::None) + { + bool bPersistent = DrawDebugType == EDrawDebugTrace::Persistent; + float LifeTime = (DrawDebugType == EDrawDebugTrace::ForDuration) ? DrawTime : 0.f; + + // @fixme, draw line with thickness = 2.f? + if (bHit && OutHits.Last().bBlockingHit) + { + // Red up to the blocking hit, green thereafter + FVector const BlockingHitPoint = OutHits.Last().ImpactPoint; + ::DrawDebugLine(World, Start, BlockingHitPoint, TraceColor.ToFColor(true), bPersistent, LifeTime); + ::DrawDebugLine(World, BlockingHitPoint, End, TraceHitColor.ToFColor(true), bPersistent, LifeTime); + } + else + { + // no hit means all red + ::DrawDebugLine(World, Start, End, TraceColor.ToFColor(true), bPersistent, LifeTime); + } + + // draw hits + for (int32 HitIdx = 0; HitIdx < OutHits.Num(); ++HitIdx) + { + FHitResult const& Hit = OutHits[HitIdx]; + ::DrawDebugPoint(World, Hit.ImpactPoint, 16.0f, (Hit.bBlockingHit ? TraceColor.ToFColor(true) : TraceHitColor.ToFColor(true)), bPersistent, LifeTime); + } + } +} +#endif // ENABLE_DRAW_DEBUG diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_SphereTrace.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_SphereTrace.cpp new file mode 100644 index 0000000..288e390 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_SphereTrace.cpp @@ -0,0 +1,187 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetActors/GGA_AbilityTargetActor_SphereTrace.h" +#include "WorldCollision.h" +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "DrawDebugHelpers.h" + +AGGA_AbilityTargetActor_SphereTrace::AGGA_AbilityTargetActor_SphereTrace() +{ + TraceSphereRadius = 100.0f; +} + +void AGGA_AbilityTargetActor_SphereTrace::Configure( + const FGameplayAbilityTargetingLocationInfo& InStartLocation, + FGameplayTag InAimingTag, + FGameplayTag InAimingRemovalTag, + FCollisionProfileName InTraceProfile, + FGameplayTargetDataFilterHandle InFilter, + TSubclassOf InReticleClass, + FWorldReticleParameters InReticleParams, + bool bInIgnoreBlockingHits, + bool bInShouldProduceTargetDataOnServer, + bool bInUsePersistentHitResults, + bool bInDebug, + bool bInTraceAffectsAimPitch, + bool bInTraceFromPlayerViewPoint, + bool bInUseAimingSpreadMod, + float InMaxRange, + float InTraceSphereRadius, + float InBaseSpread, + float InAimingSpreadMod, + float InTargetingSpreadIncrement, + float InTargetingSpreadMax, + int32 InMaxHitResultsPerTrace, + int32 InNumberOfTraces) +{ + StartLocation = InStartLocation; + AimingTag = InAimingTag; + AimingRemovalTag = InAimingRemovalTag; + TraceProfile = InTraceProfile; + Filter = InFilter; + ReticleClass = InReticleClass; + ReticleParams = InReticleParams; + bIgnoreBlockingHits = bInIgnoreBlockingHits; + ShouldProduceTargetDataOnServer = bInShouldProduceTargetDataOnServer; + bUsePersistentHitResults = bInUsePersistentHitResults; + bDebug = bInDebug; + bTraceAffectsAimPitch = bInTraceAffectsAimPitch; + bTraceFromPlayerViewPoint = bInTraceFromPlayerViewPoint; + bUseAimingSpreadMod = bInUseAimingSpreadMod; + MaxRange = InMaxRange; + TraceSphereRadius = InTraceSphereRadius; + BaseSpread = InBaseSpread; + AimingSpreadMod = InAimingSpreadMod; + TargetingSpreadIncrement = InTargetingSpreadIncrement; + TargetingSpreadMax = InTargetingSpreadMax; + MaxHitResultsPerTrace = InMaxHitResultsPerTrace; + NumberOfTraces = InNumberOfTraces; + + if (bUsePersistentHitResults) + { + NumberOfTraces = 1; + } +} + +void AGGA_AbilityTargetActor_SphereTrace::SphereTraceWithFilter(TArray& OutHitResults, const UWorld* World, + const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, + const FVector& End, float Radius, FName ProfileName, + const FCollisionQueryParams Params) +{ + check(World); + + TArray HitResults; + World->SweepMultiByProfile(HitResults, Start, End, FQuat::Identity, ProfileName, + FCollisionShape::MakeSphere(Radius), Params); + + TArray FilteredHitResults; + + // Start param could be player ViewPoint. We want HitResult to always display the StartLocation. + FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation(); + + for (int32 HitIdx = 0; HitIdx < HitResults.Num(); ++HitIdx) + { + FHitResult& Hit = HitResults[HitIdx]; + + AActor* HitActor = Hit.GetActor(); + if (!HitActor || FilterHandle.FilterPassesForActor(HitActor)) + { + Hit.TraceStart = TraceStart; + Hit.TraceEnd = End; + + FilteredHitResults.Add(Hit); + } + } + + OutHitResults = FilteredHitResults; + + return; +} + +void AGGA_AbilityTargetActor_SphereTrace::DoTrace(TArray& HitResults, const UWorld* World, + const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, + const FVector& End, FName ProfileName, const FCollisionQueryParams Params) +{ + SphereTraceWithFilter(HitResults, World, FilterHandle, Start, End, TraceSphereRadius, ProfileName, Params); +} + +void AGGA_AbilityTargetActor_SphereTrace::ShowDebugTrace(TArray& HitResults, EDrawDebugTrace::Type DrawDebugType, + float Duration) +{ +#if ENABLE_DRAW_DEBUG + if (bDebug) + { + FVector ViewStart = StartLocation.GetTargetingTransform().GetLocation(); + FRotator ViewRot; + if (PrimaryPC && bTraceFromPlayerViewPoint) + { + PrimaryPC->GetPlayerViewPoint(ViewStart, ViewRot); + } + + FVector TraceEnd = HitResults[0].TraceEnd; + if (NumberOfTraces > 1 || bUsePersistentHitResults) + { + TraceEnd = CurrentTraceEnd; + } + + DrawDebugSphereTraceMulti(GetWorld(), ViewStart, TraceEnd, TraceSphereRadius, DrawDebugType, true, HitResults, + FLinearColor::Green, FLinearColor::Red, Duration); + } +#endif +} + +#if ENABLE_DRAW_DEBUG +// Copied from KismetTraceUtils.cpp +void AGGA_AbilityTargetActor_SphereTrace::DrawDebugSweptSphere(const UWorld* InWorld, FVector const& Start, FVector const& End, + float Radius, FColor const& Color, bool bPersistentLines, float LifeTime, + uint8 DepthPriority) +{ + FVector const TraceVec = End - Start; + float const Dist = TraceVec.Size(); + + FVector const Center = Start + TraceVec * 0.5f; + float const HalfHeight = (Dist * 0.5f) + Radius; + + FQuat const CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat(); + ::DrawDebugCapsule(InWorld, Center, HalfHeight, Radius, CapsuleRot, Color, bPersistentLines, LifeTime, + DepthPriority); +} + +void AGGA_AbilityTargetActor_SphereTrace::DrawDebugSphereTraceMulti(const UWorld* World, const FVector& Start, const FVector& End, + float Radius, EDrawDebugTrace::Type DrawDebugType, bool bHit, + const TArray& OutHits, FLinearColor TraceColor, + FLinearColor TraceHitColor, float DrawTime) +{ + if (DrawDebugType != EDrawDebugTrace::None) + { + bool bPersistent = DrawDebugType == EDrawDebugTrace::Persistent; + float LifeTime = (DrawDebugType == EDrawDebugTrace::ForDuration) ? DrawTime : 0.f; + + if (bHit && OutHits.Last().bBlockingHit) + { + // Red up to the blocking hit, green thereafter + FVector const BlockingHitPoint = OutHits.Last().Location; + DrawDebugSweptSphere(World, Start, BlockingHitPoint, Radius, TraceColor.ToFColor(true), bPersistent, + LifeTime); + DrawDebugSweptSphere(World, BlockingHitPoint, End, Radius, TraceHitColor.ToFColor(true), bPersistent, + LifeTime); + } + else + { + // no hit means all red + DrawDebugSweptSphere(World, Start, End, Radius, TraceColor.ToFColor(true), bPersistent, LifeTime); + } + + // draw hits + for (int32 HitIdx = 0; HitIdx < OutHits.Num(); ++HitIdx) + { + FHitResult const& Hit = OutHits[HitIdx]; + ::DrawDebugPoint(World, Hit.ImpactPoint, 16.0f, + (Hit.bBlockingHit ? TraceColor.ToFColor(true) : TraceHitColor.ToFColor(true)), bPersistent, + LifeTime); + } + } +} +#endif // ENABLE_DRAW_DEBUG diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_Trace.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_Trace.cpp new file mode 100644 index 0000000..0913a9d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetActors/GGA_AbilityTargetActor_Trace.cpp @@ -0,0 +1,622 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetActors/GGA_AbilityTargetActor_Trace.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystemLog.h" +#include "Engine/World.h" +#include "DrawDebugHelpers.h" +#include "GameFramework/PlayerController.h" +#include "GameplayAbilitySpec.h" +#include "Kismet/KismetMathLibrary.h" + +AGGA_AbilityTargetActor_Trace::AGGA_AbilityTargetActor_Trace() +{ + bDestroyOnConfirmation = false; + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickGroup = TG_PostUpdateWork; + MaxHitResultsPerTrace = 1; + NumberOfTraces = 1; + bIgnoreBlockingHits = false; + bTraceAffectsAimPitch = true; + bTraceFromPlayerViewPoint = false; + MaxRange = 999999.0f; + bUseAimingSpreadMod = false; + BaseSpread = 0.0f; + AimingSpreadMod = 0.0f; + TargetingSpreadIncrement = 0.0f; + TargetingSpreadMax = 0.0f; + CurrentTargetingSpread = 0.0f; + bUsePersistentHitResults = false; +} + +void AGGA_AbilityTargetActor_Trace::ResetSpread() +{ + bUseAimingSpreadMod = false; + BaseSpread = 0.0f; + AimingSpreadMod = 0.0f; + TargetingSpreadIncrement = 0.0f; + TargetingSpreadMax = 0.0f; + CurrentTargetingSpread = 0.0f; +} + +float AGGA_AbilityTargetActor_Trace::GetCurrentSpread() const +{ + float FinalSpread = BaseSpread + CurrentTargetingSpread; + + if (bUseAimingSpreadMod && AimingTag.IsValid() && AimingRemovalTag.IsValid()) + { + UAbilitySystemComponent* ASC = OwningAbility->GetCurrentActorInfo()->AbilitySystemComponent.Get(); + if (ASC && (ASC->GetTagCount(AimingTag) > ASC->GetTagCount(AimingRemovalTag))) + { + FinalSpread *= AimingSpreadMod; + } + } + + return FinalSpread; +} + +void AGGA_AbilityTargetActor_Trace::SetStartLocation(const FGameplayAbilityTargetingLocationInfo& InStartLocation) +{ + StartLocation = InStartLocation; +} + +void AGGA_AbilityTargetActor_Trace::SetShouldProduceTargetDataOnServer(bool bInShouldProduceTargetDataOnServer) +{ + ShouldProduceTargetDataOnServer = bInShouldProduceTargetDataOnServer; +} + +void AGGA_AbilityTargetActor_Trace::SetDestroyOnConfirmation(bool bInDestroyOnConfirmation) +{ + bDestroyOnConfirmation = bInDestroyOnConfirmation; +} + +void AGGA_AbilityTargetActor_Trace::StartTargeting(UGameplayAbility* Ability) +{ + // Don't call to Super because we can have more than one Reticle + + SetActorTickEnabled(true); + + OwningAbility = Ability; + SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get(); + + // This is a lazy way of emptying and repopulating the ReticleActors. + // We could come up with a solution that reuses them. + DestroyReticleActors(); + + if (ReticleClass) + { + for (int32 i = 0; i < MaxHitResultsPerTrace * NumberOfTraces; i++) + { + SpawnReticleActor(GetActorLocation(), GetActorRotation()); + } + } + + if (bUsePersistentHitResults) + { + PersistentHitResults.Empty(); + } +} + +void AGGA_AbilityTargetActor_Trace::ConfirmTargetingAndContinue() +{ + check(ShouldProduceTargetData()); + if (SourceActor) + { + TArray HitResults = PerformTrace(SourceActor); + FGameplayAbilityTargetDataHandle Handle = MakeTargetData(HitResults); + TargetDataReadyDelegate.Broadcast(Handle); + +#if ENABLE_DRAW_DEBUG + if (bDebug) + { + ShowDebugTrace(HitResults, EDrawDebugTrace::Type::ForDuration, 2.0f); + } +#endif + } + + if (bUsePersistentHitResults) + { + PersistentHitResults.Empty(); + } +} + +void AGGA_AbilityTargetActor_Trace::CancelTargeting() +{ + const FGameplayAbilityActorInfo* ActorInfo = (OwningAbility ? OwningAbility->GetCurrentActorInfo() : nullptr); + UAbilitySystemComponent* ASC = (ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr); + if (ASC) + { + ASC->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::GenericCancel, + OwningAbility->GetCurrentAbilitySpecHandle(), + OwningAbility->GetCurrentActivationInfo().GetActivationPredictionKey()). + Remove(GenericCancelHandle); + } + else + { + ABILITY_LOG(Warning, TEXT("AGameplayAbilityTargetActor::CancelTargeting called with null ASC! Actor %s"), + *GetName()); + } + + CanceledDelegate.Broadcast(FGameplayAbilityTargetDataHandle()); + + SetActorTickEnabled(false); + + if (bUsePersistentHitResults) + { + PersistentHitResults.Empty(); + } +} + +void AGGA_AbilityTargetActor_Trace::BeginPlay() +{ + Super::BeginPlay(); + // 一开始就禁用Tick。我们会在StartTargeting中启用并在StopTargeting中禁用。 + // 对于瞬间确认,tick永远不会发生,因为我们一次性Start,Confirm,然后马上Stop。 + SetActorTickEnabled(false); +} + +void AGGA_AbilityTargetActor_Trace::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + DestroyReticleActors(); + + Super::EndPlay(EndPlayReason); +} + +void AGGA_AbilityTargetActor_Trace::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + TArray HitResults; + if (bDebug || bUsePersistentHitResults) + { + // Only need to trace on Tick if we're showing debug or if we use persistent hit results, otherwise we just use the confirmation trace + HitResults = PerformTrace(SourceActor); + if (HitResults.Num() > 0) + { + const FVector StartPoint = StartLocation.GetTargetingTransform().GetLocation(); + const FVector EndPoint = HitResults[0].Component.IsValid() ? HitResults[0].ImpactPoint : HitResults[0].TraceEnd; + const FRotator Rotator = UKismetMathLibrary::FindLookAtRotation(StartPoint, EndPoint); + SetActorLocationAndRotation(StartPoint, Rotator); + } + } + +#if ENABLE_DRAW_DEBUG + if (SourceActor && bDebug) + { + ShowDebugTrace(HitResults, EDrawDebugTrace::Type::ForOneFrame); + } +#endif +} + +void AGGA_AbilityTargetActor_Trace::LineTraceWithFilter(TArray& OutHitResults, const UWorld* World, + const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, + const FVector& End, FName ProfileName, const FCollisionQueryParams Params) +{ + check(World); + + TArray HitResults; + World->LineTraceMultiByProfile(HitResults, Start, End, ProfileName, Params); + + TArray FilteredHitResults; + + // Start param could be player ViewPoint. We want HitResult to always display the StartLocation. + FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation(); + + for (int32 HitIdx = 0; HitIdx < HitResults.Num(); ++HitIdx) + { + FHitResult& Hit = HitResults[HitIdx]; + + AActor* HitActor = Hit.GetActor(); + + if (!HitActor || FilterHandle.FilterPassesForActor(HitActor)) + { + Hit.TraceStart = TraceStart; + Hit.TraceEnd = End; + + FilteredHitResults.Add(Hit); + } + } + + OutHitResults = FilteredHitResults; +} + +void AGGA_AbilityTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, + const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) +{ + if (!OwningAbility) // Server and launching client only + { + return; + } + + // Default values in case of AI Controller + FVector ViewStart = TraceStart; + FRotator ViewRot = StartLocation.GetTargetingTransform().GetRotation().Rotator(); + + if (PrimaryPC && bTraceFromPlayerViewPoint) + { + PrimaryPC->GetPlayerViewPoint(ViewStart, ViewRot); + } + + const FVector ViewDir = ViewRot.Vector(); + FVector ViewEnd = ViewStart + (ViewDir * MaxRange); + + ClipCameraRayToAbilityRange(ViewStart, ViewDir, TraceStart, MaxRange, ViewEnd); + + // Use first hit + TArray HitResults; + LineTraceWithFilter(HitResults, InSourceActor->GetWorld(), Filter, ViewStart, ViewEnd, TraceProfile.Name, Params); + + CurrentTargetingSpread = FMath::Min(TargetingSpreadMax, CurrentTargetingSpread + TargetingSpreadIncrement); + + const bool bUseTraceResult = HitResults.Num() > 0 && (FVector::DistSquared(TraceStart, HitResults[0].Location) <= ( + MaxRange * MaxRange)); + + const FVector AdjustedEnd = (bUseTraceResult) ? HitResults[0].Location : ViewEnd; + + FVector AdjustedAimDir = (AdjustedEnd - TraceStart).GetSafeNormal(); + if (AdjustedAimDir.IsZero()) + { + AdjustedAimDir = ViewDir; + } + + if (!bTraceAffectsAimPitch && bUseTraceResult) + { + FVector OriginalAimDir = (ViewEnd - TraceStart).GetSafeNormal(); + + if (!OriginalAimDir.IsZero()) + { + // Convert to angles and use original pitch + const FRotator OriginalAimRot = OriginalAimDir.Rotation(); + + FRotator AdjustedAimRot = AdjustedAimDir.Rotation(); + AdjustedAimRot.Pitch = OriginalAimRot.Pitch; + + AdjustedAimDir = AdjustedAimRot.Vector(); + } + } + + const float CurrentSpread = GetCurrentSpread(); + + const float ConeHalfAngle = FMath::DegreesToRadians(CurrentSpread * 0.5f); + const int32 RandomSeed = FMath::Rand(); + FRandomStream WeaponRandomStream(RandomSeed); + const FVector ShootDir = WeaponRandomStream.VRandCone(AdjustedAimDir, ConeHalfAngle, ConeHalfAngle); + + OutTraceEnd = TraceStart + (ShootDir * MaxRange); +} + +bool AGGA_AbilityTargetActor_Trace::ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, + float AbilityRange, FVector& ClippedPosition) +{ + FVector CameraToCenter = AbilityCenter - CameraLocation; + float DotToCenter = FVector::DotProduct(CameraToCenter, CameraDirection); + if (DotToCenter >= 0) + //If this fails, we're pointed away from the center, but we might be inside the sphere and able to find a good exit point. + { + float DistanceSquared = CameraToCenter.SizeSquared() - (DotToCenter * DotToCenter); + float RadiusSquared = (AbilityRange * AbilityRange); + if (DistanceSquared <= RadiusSquared) + { + float DistanceFromCamera = FMath::Sqrt(RadiusSquared - DistanceSquared); + float DistanceAlongRay = DotToCenter + DistanceFromCamera; + //Subtracting instead of adding will get the other intersection point + ClippedPosition = CameraLocation + (DistanceAlongRay * CameraDirection); + //Cam aim point clipped to range sphere + return true; + } + } + return false; +} + +void AGGA_AbilityTargetActor_Trace::StopTargeting() +{ + SetActorTickEnabled(false); + + DestroyReticleActors(); + + // Clear added callbacks + TargetDataReadyDelegate.Clear(); + CanceledDelegate.Clear(); + + if (GenericDelegateBoundASC) + { + GenericDelegateBoundASC->GenericLocalConfirmCallbacks.RemoveDynamic( + this, &AGameplayAbilityTargetActor::ConfirmTargeting); + GenericDelegateBoundASC->GenericLocalCancelCallbacks.RemoveDynamic( + this, &AGameplayAbilityTargetActor::CancelTargeting); + GenericDelegateBoundASC = nullptr; + } +} + +FGameplayAbilityTargetDataHandle AGGA_AbilityTargetActor_Trace::MakeTargetData(const TArray& HitResults) const +{ + FGameplayAbilityTargetDataHandle ReturnDataHandle; + + for (int32 i = 0; i < HitResults.Num(); i++) + { + /** Note: These are cleaned up by the FGameplayAbilityTargetDataHandle (via an internal TSharedPtr) */ + FGameplayAbilityTargetData_SingleTargetHit* ReturnData = new FGameplayAbilityTargetData_SingleTargetHit(); + ReturnData->HitResult = HitResults[i]; + ReturnDataHandle.Add(ReturnData); + } + + return ReturnDataHandle; +} + +TArray AGGA_AbilityTargetActor_Trace::PerformTrace(AActor* InSourceActor) +{ + bool bTraceComplex = false; + TArray ActorsToIgnore; + + ActorsToIgnore.Add(InSourceActor); + + FCollisionQueryParams Params(SCENE_QUERY_STAT(AGSGATA_LineTrace), bTraceComplex); + Params.bReturnPhysicalMaterial = true; + Params.AddIgnoredActors(ActorsToIgnore); + Params.bIgnoreBlocks = bIgnoreBlockingHits; + + FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation(); + FVector TraceEnd; + + if (PrimaryPC) + { + FVector ViewStart; + FRotator ViewRot; + PrimaryPC->GetPlayerViewPoint(ViewStart, ViewRot); + TraceStart = bTraceFromPlayerViewPoint ? ViewStart : TraceStart; + } + + if (bUsePersistentHitResults) + { + // Clear any blocking hit results, invalid Actors, or actors out of range + //TODO Check for visibility if we add AIPerceptionComponent in the future + for (int32 i = PersistentHitResults.Num() - 1; i >= 0; i--) + { + FHitResult& HitResult = PersistentHitResults[i]; + + AActor* HitActor = HitResult.GetActor(); + + if (HitResult.bBlockingHit || !HitActor || FVector::DistSquared( + TraceStart, HitActor->GetActorLocation()) > (MaxRange * MaxRange)) + { + PersistentHitResults.RemoveAt(i); + } + } + } + + TArray ReturnHitResults; + + for (int32 TraceIndex = 0; TraceIndex < NumberOfTraces; TraceIndex++) + { + AimWithPlayerController(InSourceActor, Params, TraceStart, TraceEnd); + //Effective on server and launching client only + + // ------------------------------------------------------ + + SetActorLocationAndRotation(TraceEnd, SourceActor->GetActorRotation()); + + CurrentTraceEnd = TraceEnd; + + TArray TraceHitResults; + DoTrace(TraceHitResults, InSourceActor->GetWorld(), Filter, TraceStart, TraceEnd, TraceProfile.Name, Params); + + for (int32 j = TraceHitResults.Num() - 1; j >= 0; j--) + { + if (MaxHitResultsPerTrace >= 0 && j + 1 > MaxHitResultsPerTrace) + { + // Trim to MaxHitResultsPerTrace + TraceHitResults.RemoveAt(j); + continue; + } + + FHitResult& HitResult = TraceHitResults[j]; + + // Reminder: if bUsePersistentHitResults, Number of Traces = 1 + if (bUsePersistentHitResults) + { + AActor* HitActor = HitResult.GetActor(); + + // This is looping backwards so that further objects from player are added first to the queue. + // This results in closer actors taking precedence as the further actors will get bumped out of the TArray. + if (HitActor && (!HitResult.bBlockingHit || PersistentHitResults.Num() < 1)) + { + bool bActorAlreadyInPersistentHits = false; + + // Make sure PersistentHitResults doesn't have this hit actor already + for (int32 k = 0; k < PersistentHitResults.Num(); k++) + { + FHitResult& PersistentHitResult = PersistentHitResults[k]; + + AActor* PersistentHitActor = PersistentHitResult.GetActor(); + + if (PersistentHitActor == HitActor) + { + bActorAlreadyInPersistentHits = true; + break; + } + } + + if (bActorAlreadyInPersistentHits) + { + continue; + } + + if (PersistentHitResults.Num() >= MaxHitResultsPerTrace) + { + // Treat PersistentHitResults like a queue, remove first element + PersistentHitResults.RemoveAt(0); + } + + PersistentHitResults.Add(HitResult); + } + } + else + { + // ReticleActors for PersistentHitResults are handled later + int32 ReticleIndex = TraceIndex * MaxHitResultsPerTrace + j; + if (ReticleIndex < ReticleActors.Num()) + { + if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActors[ReticleIndex].Get()) + { + AActor* HitActor = HitResult.GetActor(); + const bool bHitActor = HitActor != nullptr; + if (bHitActor && !HitResult.bBlockingHit) + { + LocalReticleActor->SetActorHiddenInGame(false); + + const FVector ReticleLocation = (bHitActor && LocalReticleActor->bSnapToTargetedActor) + ? HitActor->GetActorLocation() + : HitResult.Location; + + LocalReticleActor->SetActorLocation(ReticleLocation); + LocalReticleActor->SetIsTargetAnActor(bHitActor); + } + else + { + LocalReticleActor->SetActorHiddenInGame(true); + } + } + } + } + } // for TraceHitResults + + if (!bUsePersistentHitResults) + { + if (TraceHitResults.Num() < ReticleActors.Num()) + { + // We have less hit results than ReticleActors, hide the extra ones + for (int32 j = TraceHitResults.Num(); j < ReticleActors.Num(); j++) + { + if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActors[j].Get()) + { + LocalReticleActor->SetIsTargetAnActor(false); + LocalReticleActor->SetActorHiddenInGame(true); + } + } + } + } + + if (TraceHitResults.Num() < 1) + { + // If there were no hits, add a default HitResult at the end of the trace + FHitResult HitResult; + // Start param could be player ViewPoint. We want HitResult to always display the StartLocation. + HitResult.TraceStart = StartLocation.GetTargetingTransform().GetLocation(); + HitResult.TraceEnd = TraceEnd; + HitResult.Location = TraceEnd; + HitResult.ImpactPoint = TraceEnd; + TraceHitResults.Add(HitResult); + + if (bUsePersistentHitResults && PersistentHitResults.Num() < 1) + { + PersistentHitResults.Add(HitResult); + } + } + + ReturnHitResults.Append(TraceHitResults); + } // for NumberOfTraces + + // Reminder: if bUsePersistentHitResults, Number of Traces = 1 + if (bUsePersistentHitResults && MaxHitResultsPerTrace > 0) + { + // Handle ReticleActors + for (int32 PersistentHitResultIndex = 0; PersistentHitResultIndex < PersistentHitResults.Num(); + PersistentHitResultIndex++) + { + FHitResult& HitResult = PersistentHitResults[PersistentHitResultIndex]; + AActor* HitActor = HitResult.GetActor(); + // Update TraceStart because old persistent HitResults will have their original TraceStart and the player could have moved since then + HitResult.TraceStart = StartLocation.GetTargetingTransform().GetLocation(); + if (!ReticleActors.IsValidIndex(PersistentHitResultIndex)) + { + continue; + } + if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActors[PersistentHitResultIndex].Get()) + { + const bool bHitActor = HitActor != nullptr; + + if (bHitActor && !HitResult.bBlockingHit) + { + LocalReticleActor->SetActorHiddenInGame(false); + + const FVector ReticleLocation = (bHitActor && LocalReticleActor->bSnapToTargetedActor) + ? HitActor->GetActorLocation() + : HitResult.Location; + + LocalReticleActor->SetActorLocation(ReticleLocation); + LocalReticleActor->SetIsTargetAnActor(bHitActor); + } + else + { + LocalReticleActor->SetActorHiddenInGame(true); + } + } + } + + if (PersistentHitResults.Num() < ReticleActors.Num()) + { + // We have less hit results than ReticleActors, hide the extra ones + for (int32 PersistentHitResultIndex = PersistentHitResults.Num(); PersistentHitResultIndex < ReticleActors. + Num(); PersistentHitResultIndex++) + { + if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActors[PersistentHitResultIndex].Get()) + { + LocalReticleActor->SetIsTargetAnActor(false); + LocalReticleActor->SetActorHiddenInGame(true); + } + } + } + CurrentHitResults = PersistentHitResults; + return PersistentHitResults; + } + CurrentHitResults = ReturnHitResults; + return ReturnHitResults; +} + +AGameplayAbilityWorldReticle* AGGA_AbilityTargetActor_Trace::SpawnReticleActor(FVector Location, FRotator Rotation) +{ + if (ReticleClass) + { + AGameplayAbilityWorldReticle* SpawnedReticleActor = GetWorld()->SpawnActor( + ReticleClass, Location, Rotation); + if (SpawnedReticleActor) + { + SpawnedReticleActor->InitializeReticle(this, PrimaryPC, ReticleParams); + SpawnedReticleActor->SetActorHiddenInGame(true); + ReticleActors.Add(SpawnedReticleActor); + + // This is to catch cases of playing on a listen server where we are using a replicated reticle actor. + // (In a client controlled player, this would only run on the client and therefor never replicate. If it runs + // on a listen server, the reticle actor may replicate. We want consistancy between client/listen server players. + // Just saying 'make the reticle actor non replicated' isnt a good answer, since we want to mix and match reticle + // actors and there may be other targeting types that want to replicate the same reticle actor class). + if (!ShouldProduceTargetDataOnServer) + { + SpawnedReticleActor->SetReplicates(false); + } + + return SpawnedReticleActor; + } + } + + return nullptr; +} + +void AGGA_AbilityTargetActor_Trace::DestroyReticleActors() +{ + for (int32 i = ReticleActors.Num() - 1; i >= 0; i--) + { + if (ReticleActors[i].IsValid()) + { + ReticleActors[i].Get()->Destroy(); + } + } + + ReticleActors.Empty(); +} + +void AGGA_AbilityTargetActor_Trace::GetCurrentHitResult(TArray& HitResults) +{ + HitResults = CurrentHitResults; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType.cpp new file mode 100644 index 0000000..a9213e0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType.cpp @@ -0,0 +1,8 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetTypes/GGA_TargetType.h" + +void UGGA_TargetType::GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const +{ +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseEventData.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseEventData.cpp new file mode 100644 index 0000000..2e9b9f4 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseEventData.cpp @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetTypes/GGA_TargetType_UseEventData.h" + +#include "AbilitySystemBlueprintLibrary.h" + + +void UGGA_TargetType_UseEventData::GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const +{ + const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult(); + const FHitResult TargetDataHitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(EventData.TargetData, 0); + + if (FoundHitResult) + { + OutHitResults.Add(*FoundHitResult); + } + else if (TargetDataHitResult.IsValidBlockingHit()) + { + OutHitResults.Add(TargetDataHitResult); + } + else if (EventData.Target) + { + const AActor* Actor = EventData.Target; + OutActors.Add(const_cast(Actor)); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseOwner.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseOwner.cpp new file mode 100644 index 0000000..d4b0885 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/TargetTypes/GGA_TargetType_UseOwner.cpp @@ -0,0 +1,10 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "TargetTypes/GGA_TargetType_UseOwner.h" + + +void UGGA_TargetType_UseOwner::GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const +{ + OutActors.Add(TargetingActor); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_AbilitySystemFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_AbilitySystemFunctionLibrary.cpp new file mode 100644 index 0000000..d10a45e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_AbilitySystemFunctionLibrary.cpp @@ -0,0 +1,947 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_AbilitySystemFunctionLibrary.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Logging/LogMacros.h" +#include "AbilitySystemLog.h" +#include "GameplayCueManager.h" +#include "Animation/AnimSequenceBase.h" +#include "Animation/AnimMontage.h" + +bool UGGA_AbilitySystemFunctionLibrary::FindAbilitySystemComponent(AActor* Actor, TSubclassOf DesiredClass, UAbilitySystemComponent*& ASC) +{ + if (UAbilitySystemComponent* Instance = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Actor)) + { + if (Instance->GetClass()->IsChildOf(DesiredClass)) + { + ASC = Instance; + return true; + } + } + return false; +} + +UAbilitySystemComponent* UGGA_AbilitySystemFunctionLibrary::GetAbilitySystemComponent(AActor* Actor, TSubclassOf DesiredClass) +{ + if (UAbilitySystemComponent* Instance = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Actor)) + { + if (Instance->GetClass()->IsChildOf(DesiredClass)) + { + return Instance; + } + } + return nullptr; +} + +void UGGA_AbilitySystemFunctionLibrary::InitAbilityActorInfo(UAbilitySystemComponent* AbilitySystem, AActor* InOwnerActor, AActor* InAvatarActor) +{ + if (!IsValid(AbilitySystem) || !IsValid(InOwnerActor) || !IsValid(InAvatarActor)) + { + return; + } + AbilitySystem->InitAbilityActorInfo(InOwnerActor, InAvatarActor); +} + +AActor* UGGA_AbilitySystemFunctionLibrary::GetOwnerActor(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return nullptr; + } + return AbilitySystem->GetOwnerActor(); +} + +AActor* UGGA_AbilitySystemFunctionLibrary::GetAvatarActor(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return nullptr; + } + return AbilitySystem->GetAvatarActor_Direct(); +} + +int32 UGGA_AbilitySystemFunctionLibrary::HandleGameplayEvent(UAbilitySystemComponent* AbilitySystem, FGameplayTag EventTag, const FGameplayEventData& Payload) +{ + if (!IsValid(AbilitySystem) || !EventTag.IsValid()) + { + return false; + } + + return AbilitySystem->HandleGameplayEvent(EventTag, &Payload); +} + +bool UGGA_AbilitySystemFunctionLibrary::TryActivateAbilities(UAbilitySystemComponent* AbilitySystem, TArray AbilitiesToActivate, bool bAllowRemoteActivation, + bool bFirstOnly) +{ + if (!IsValid(AbilitySystem) || AbilitiesToActivate.IsEmpty()) + { + return false; + } + int32 Num = 0; + for (int32 i = 0; i < AbilitiesToActivate.Num(); i++) + { + if (AbilitySystem->TryActivateAbility(AbilitiesToActivate[i], bAllowRemoteActivation)) + { + Num++; + if (bFirstOnly) + { + return true; + } + } + } + return Num == AbilitiesToActivate.Num(); +} + +bool UGGA_AbilitySystemFunctionLibrary::HasActivatableTriggeredAbility(UAbilitySystemComponent* AbilitySystem, FGameplayTag Tag) +{ + if (!IsValid(AbilitySystem) || !Tag.IsValid()) + { + return false; + } + return AbilitySystem->HasActivatableTriggeredAbility(Tag); +} + +void UGGA_AbilitySystemFunctionLibrary::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, + TArray& MatchingGameplayAbilities, + bool bOnlyAbilitiesThatSatisfyTagRequirements) +{ + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return; + } + MatchingGameplayAbilities.Empty(); + TArray AbilitiesToActivate; + + AbilitySystem->GetActivatableGameplayAbilitySpecsByAllMatchingTags(Tags, AbilitiesToActivate, bOnlyAbilitiesThatSatisfyTagRequirements); + + for (FGameplayAbilitySpec* GameplayAbilitySpec : AbilitiesToActivate) + { + MatchingGameplayAbilities.Add(GameplayAbilitySpec->Handle); + } +} + +void UGGA_AbilitySystemFunctionLibrary::GetActivatableGameplayAbilitySpecs(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, const UObject* SourceObject, + TArray& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements) +{ + if (SourceObject == nullptr) + { + GetActivatableGameplayAbilitySpecsByAllMatchingTags(AbilitySystem, Tags, MatchingGameplayAbilities, bOnlyAbilitiesThatSatisfyTagRequirements); + return; + } + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return; + } + + TArray AbilitiesToActivate; + + for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities()) + { + if (Spec.Ability && Spec.Ability->GetAssetTags().HasAll(Tags) && Spec.SourceObject == SourceObject) + { + // Consider abilities that are blocked by tags currently if we're supposed to (default behavior). + // That way, we can use the blocking to find an appropriate ability based on tags when we have more than + // one ability that match the GameplayTagContainer. + if (!bOnlyAbilitiesThatSatisfyTagRequirements || Spec.Ability->DoesAbilitySatisfyTagRequirements(*AbilitySystem)) + { + MatchingGameplayAbilities.Add(Spec.Handle); + } + } + } +} + +bool UGGA_AbilitySystemFunctionLibrary::GetFirstActivatableAbility(const UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer Tags, FGameplayAbilitySpecHandle& MatchingGameplayAbility, + const UObject* SourceObject, bool bOnlyAbilitiesThatSatisfyTagRequirements) +{ + if (SourceObject == nullptr) + { + return GetFirstActivatableAbilityByAllMatchingTags(AbilitySystem, Tags, MatchingGameplayAbility, bOnlyAbilitiesThatSatisfyTagRequirements); + } + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return false; + } + + for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities()) + { + if (Spec.Ability && Spec.Ability->GetAssetTags().HasAll(Tags) && SourceObject == Spec.SourceObject) + { + // Consider abilities that are blocked by tags currently if we're supposed to (default behavior). + // That way, we can use the blocking to find an appropriate ability based on tags when we have more than + // one ability that match the GameplayTagContainer. + if (!bOnlyAbilitiesThatSatisfyTagRequirements || Spec.Ability->DoesAbilitySatisfyTagRequirements(*AbilitySystem)) + { + MatchingGameplayAbility = Spec.Handle; + return true; + } + } + } + return false; +} + +bool UGGA_AbilitySystemFunctionLibrary::GetFirstActivatableAbilityByAllMatchingTags(const UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer Tags, + FGameplayAbilitySpecHandle& MatchingGameplayAbility, bool bOnlyAbilitiesThatSatisfyTagRequirements) +{ + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return false; + } + + for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities()) + { + if (Spec.Ability && Spec.Ability->GetAssetTags().HasAll(Tags)) + { + // Consider abilities that are blocked by tags currently if we're supposed to (default behavior). + // That way, we can use the blocking to find an appropriate ability based on tags when we have more than + // one ability that match the GameplayTagContainer. + if (!bOnlyAbilitiesThatSatisfyTagRequirements || Spec.Ability->DoesAbilitySatisfyTagRequirements(*AbilitySystem)) + { + MatchingGameplayAbility = Spec.Handle; + return true; + } + } + } + return false; +} + +void UGGA_AbilitySystemFunctionLibrary::GetActiveAbilityInstancesWithTags(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, + TArray& MatchingAbilityInstances) +{ + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return; + } + + TArray AbilitiesToActivate; + AbilitySystem->GetActivatableGameplayAbilitySpecsByAllMatchingTags(Tags, AbilitiesToActivate, false); + + // Iterate the list of all ability specs + for (FGameplayAbilitySpec* Spec : AbilitiesToActivate) + { + // Iterate all instances on this ability spec + TArray AbilityInstances = Spec->GetAbilityInstances(); + + for (UGameplayAbility* ActiveAbility : AbilityInstances) + { + if (ActiveAbility->IsActive()) + { + MatchingAbilityInstances.Add(ActiveAbility); + } + } + } +} + +bool UGGA_AbilitySystemFunctionLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) +{ + if (IsValid(Actor)) + { + UAbilitySystemComponent* AbilitySystemComponent = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor); + if (IsValid(AbilitySystemComponent)) + { + FScopedPredictionWindow NewScopedWindow(AbilitySystemComponent, true); + if (AbilitySystemComponent->HandleGameplayEvent(EventTag, &Payload) > 0) + { + return true; + } + } + else + { + ABILITY_LOG(Error, TEXT("UAbilitySystemBPLibrary::SendGameplayEventToActor: Invalid ability system component retrieved from Actor %s. EventTag was %s"), *Actor->GetName(), + *EventTag.ToString()); + } + } + return false; +} + +void UGGA_AbilitySystemFunctionLibrary::BreakAbilityEndedData(const FAbilityEndedData& AbilityEndedData, UGameplayAbility*& AbilityThatEnded, FGameplayAbilitySpecHandle& AbilitySpecHandle, + bool& bReplicateEndAbility, bool& bWasCancelled) +{ + AbilityThatEnded = AbilityEndedData.AbilityThatEnded; + AbilitySpecHandle = AbilityEndedData.AbilitySpecHandle; + bReplicateEndAbility = AbilityEndedData.bReplicateEndAbility; + bWasCancelled = AbilityEndedData.bWasCancelled; +} + +bool UGGA_AbilitySystemFunctionLibrary::FindAllAbilitiesWithTagsInOrder(const UAbilitySystemComponent* AbilitySystem, TArray Tags, TArray& OutAbilityHandles, + bool bExactMatch) +{ + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return false; + } + // ensure the output array is empty + OutAbilityHandles.Empty(); + + for (const FGameplayTag& Tag : Tags) + { + TArray Handles; + AbilitySystem->FindAllAbilitiesWithTags(Handles, Tag.GetSingleTagContainer(), bExactMatch); + OutAbilityHandles.Append(Handles); + } + + return !OutAbilityHandles.IsEmpty(); +} + +bool UGGA_AbilitySystemFunctionLibrary::FindAbilityMatchingQuery(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, FGameplayTagQuery Query, + const UObject* SourceObject) +{ + if (!IsValid(AbilitySystem) || Query.IsEmpty()) + { + return false; + } + + // iterate through all Ability Specs + for (const FGameplayAbilitySpec& CurrentSpec : AbilitySystem->GetActivatableAbilities()) + { + // try to get the ability instance + UGameplayAbility* AbilityInstance = CurrentSpec.GetPrimaryInstance(); + + // default to the CDO if we can't + if (!AbilityInstance) + { + AbilityInstance = CurrentSpec.Ability; + } + + if (SourceObject && CurrentSpec.SourceObject != SourceObject) + { + continue; + } + + // ensure the ability instance is valid + if (IsValid(AbilityInstance) && AbilityInstance->GetAssetTags().MatchesQuery(Query)) + { + OutAbilityHandle = CurrentSpec.Handle; + return true; + } + } + return false; +} + +FGameplayAbilitySpec* UGGA_AbilitySystemFunctionLibrary::FindAbilitySpecFromClass(const UAbilitySystemComponent* AbilitySystem, TSubclassOf AbilityClass, const UObject* SourceObject) +{ + if (!IsValid(AbilitySystem) || AbilityClass == nullptr) + { + return nullptr; + } + for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities()) + { + if (Spec.Ability == nullptr) + { + continue; + } + + if (SourceObject && Spec.SourceObject != SourceObject) + { + continue; + } + + if (Spec.Ability->GetClass() == AbilityClass) + { + return const_cast(&Spec); + } + } + + return nullptr; +} + +bool UGGA_AbilitySystemFunctionLibrary::FindAbilityFromClass(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, TSubclassOf AbilityClass, + const UObject* SourceObject) +{ + if (!IsValid(AbilitySystem) || AbilityClass == nullptr) + { + return false; + } + for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities()) + { + if (Spec.Ability == nullptr) + { + continue; + } + + if (SourceObject && Spec.SourceObject != SourceObject) + { + continue; + } + + if (Spec.Ability->GetClass() == AbilityClass) + { + OutAbilityHandle = Spec.Handle; + return true; + } + } + + return false; +} + +bool UGGA_AbilitySystemFunctionLibrary::FindAbilityWithTags(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, FGameplayTagContainer Tags, bool bExactMatch, + const UObject* SourceObject) +{ + if (!IsValid(AbilitySystem) || Tags.IsEmpty()) + { + return false; + } + // iterate through all Ability Specs + for (const FGameplayAbilitySpec& CurrentSpec : AbilitySystem->GetActivatableAbilities()) + { + // try to get the ability instance + UGameplayAbility* AbilityInstance = CurrentSpec.GetPrimaryInstance(); + + // default to the CDO if we can't + if (!AbilityInstance) + { + AbilityInstance = CurrentSpec.Ability; + } + + if (SourceObject && CurrentSpec.SourceObject != SourceObject) + { + continue; + } + + // ensure the ability instance is valid + if (IsValid(AbilityInstance)) + { + // do we want an exact match? + if (bExactMatch) + { + // check if we match all tags + if (AbilityInstance->GetAssetTags().HasAll(Tags)) + { + OutAbilityHandle = CurrentSpec.Handle; + return true; + } + } + else + { + // check if we match any tags + if (AbilityInstance->GetAssetTags().HasAny(Tags)) + { + OutAbilityHandle = CurrentSpec.Handle; + return true; + } + } + } + } + return false; +} + +void UGGA_AbilitySystemFunctionLibrary::AddLooseGameplayTag(UAbilitySystemComponent* AbilitySystem, const FGameplayTag& GameplayTag, int32 Count) +{ + if (!IsValid(AbilitySystem) || !GameplayTag.IsValid()) + { + return; + } + AbilitySystem->AddLooseGameplayTag(GameplayTag, Count); +} + +void UGGA_AbilitySystemFunctionLibrary::AddLooseGameplayTags(UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& GameplayTags, int32 Count) +{ + if (!IsValid(AbilitySystem) || GameplayTags.IsEmpty()) + { + return; + } + AbilitySystem->AddLooseGameplayTags(GameplayTags, Count); +} + +void UGGA_AbilitySystemFunctionLibrary::RemoveLooseGameplayTag(UAbilitySystemComponent* AbilitySystem, const FGameplayTag& GameplayTag, int32 Count) +{ + if (!IsValid(AbilitySystem) || !GameplayTag.IsValid()) + { + return; + } + AbilitySystem->RemoveLooseGameplayTag(GameplayTag, Count); +} + +void UGGA_AbilitySystemFunctionLibrary::RemoveLooseGameplayTags(UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& GameplayTags, int32 Count) +{ + if (!IsValid(AbilitySystem) || GameplayTags.IsEmpty()) + { + return; + } + AbilitySystem->RemoveLooseGameplayTags(GameplayTags, Count); +} + +void UGGA_AbilitySystemFunctionLibrary::RemoveAllGameplayCues(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return; + } + AbilitySystem->RemoveAllGameplayCues(); +} + +float UGGA_AbilitySystemFunctionLibrary::PlayMontage(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* AnimatingAbility, FGameplayAbilityActivationInfo ActivationInfo, UAnimMontage* Montage, + float InPlayRate, FName StartSectionName, + float StartTimeSeconds) +{ + if (!IsValid(AbilitySystem) || !Montage || !AnimatingAbility) + { + return -1.f;; + } + return AbilitySystem->PlayMontage(AnimatingAbility, ActivationInfo, Montage, InPlayRate, StartSectionName, StartTimeSeconds); +} + +void UGGA_AbilitySystemFunctionLibrary::CurrentMontageStop(UAbilitySystemComponent* AbilitySystem, float OverrideBlendOutTime) +{ + if (!IsValid(AbilitySystem)) + { + return; + } + AbilitySystem->CurrentMontageStop(OverrideBlendOutTime); +} + +void UGGA_AbilitySystemFunctionLibrary::StopMontageIfCurrent(UAbilitySystemComponent* AbilitySystem, const UAnimMontage* Montage, float OverrideBlendOutTime) +{ + if (!IsValid(AbilitySystem) || !Montage) + { + return; + } + AbilitySystem->StopMontageIfCurrent(*Montage, OverrideBlendOutTime); +} + +void UGGA_AbilitySystemFunctionLibrary::ClearAnimatingAbility(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* Ability) +{ + if (!IsValid(AbilitySystem) || !Ability) + { + return; + } + AbilitySystem->ClearAnimatingAbility(Ability); +} + +void UGGA_AbilitySystemFunctionLibrary::CurrentMontageJumpToSection(UAbilitySystemComponent* AbilitySystem, FName SectionName) +{ + if (!IsValid(AbilitySystem) || SectionName == NAME_None) + { + return; + } + AbilitySystem->CurrentMontageJumpToSection(SectionName); +} + +void UGGA_AbilitySystemFunctionLibrary::CurrentMontageSetNextSectionName(UAbilitySystemComponent* AbilitySystem, FName FromSectionName, FName ToSectionName) +{ + if (!IsValid(AbilitySystem) || FromSectionName == NAME_None || ToSectionName == NAME_None) + return; + AbilitySystem->CurrentMontageSetNextSectionName(FromSectionName, ToSectionName); +} + +void UGGA_AbilitySystemFunctionLibrary::CurrentMontageSetPlayRate(UAbilitySystemComponent* AbilitySystem, float InPlayRate) +{ + if (!IsValid(AbilitySystem) || InPlayRate <= 0.f) + { + return; + } + AbilitySystem->CurrentMontageSetPlayRate(InPlayRate); +} + +UGameplayAbility* UGGA_AbilitySystemFunctionLibrary::GetAnimatingAbility(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return nullptr; + } + return AbilitySystem->GetAnimatingAbility(); +} + +bool UGGA_AbilitySystemFunctionLibrary::IsAnimatingAbility(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* Ability) +{ + if (!IsValid(AbilitySystem)) + { + return false; + } + return AbilitySystem->IsAnimatingAbility(Ability); +} + +UGameplayAbility* UGGA_AbilitySystemFunctionLibrary::GetAnimatingAbilityFromActor(AActor* Actor, UAnimSequenceBase* Animation) +{ + if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor)) + { + if (ASC->GetCurrentMontage() == Animation) + { + if (UGameplayAbility* Ability = ASC->GetAnimatingAbility()) + { + return Ability; + } + } + } + return nullptr; +} + +bool UGGA_AbilitySystemFunctionLibrary::FindAnimatingAbilityFromActor(AActor* Actor, UAnimSequenceBase* Animation, TSubclassOf DesiredClass, UGameplayAbility*& AbilityInstance) +{ + UGameplayAbility* Ability = GetAnimatingAbilityFromActor(Actor, Animation); + if (UClass* ActualClass = DesiredClass) + { + if (Ability && Ability->GetClass()->IsChildOf(ActualClass)) + { + AbilityInstance = Ability; + return true; + } + } + return false; +} + +UAnimMontage* UGGA_AbilitySystemFunctionLibrary::GetCurrentMontage(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return nullptr; + } + return AbilitySystem->GetCurrentMontage(); +} + +int32 UGGA_AbilitySystemFunctionLibrary::GetCurrentMontageSectionID(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return INDEX_NONE; + } + return AbilitySystem->GetCurrentMontageSectionID(); +} + +FName UGGA_AbilitySystemFunctionLibrary::GetCurrentMontageSectionName(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return NAME_None; + } + return AbilitySystem->GetCurrentMontageSectionName(); +} + +float UGGA_AbilitySystemFunctionLibrary::GetCurrentMontageSectionLength(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return 0.0f; + } + return AbilitySystem->GetCurrentMontageSectionLength(); +} + +float UGGA_AbilitySystemFunctionLibrary::GetCurrentMontageSectionTimeLeft(UAbilitySystemComponent* AbilitySystem) +{ + if (!IsValid(AbilitySystem)) + { + return 0.0f; + } + return AbilitySystem->GetCurrentMontageSectionTimeLeft(); +} + +void UGGA_AbilitySystemFunctionLibrary::SetAbilityInputPressed(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return; + } + + FScopedAbilityListLock ActiveScopeLock(*AbilitySystem); + if (FGameplayAbilitySpec* Spec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + if (Spec->Ability) + { + Spec->InputPressed = true; + if (Spec->IsActive()) + { + if (Spec->Ability->bReplicateInputDirectly && AbilitySystem->IsOwnerActorAuthoritative() == false) + { + AbilitySystem->ServerSetInputPressed(Spec->Handle); + } + + AbilitySystem->AbilitySpecInputPressed(*Spec); + + PRAGMA_DISABLE_DEPRECATION_WARNINGS +#if ENGINE_MINOR_VERSION > 4 + // Fixing this up to use the instance activation, but this function should be deprecated as it cannot work with InstancedPerExecution + UE_CLOG(Spec->Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerExecution, LogAbilitySystem, Warning, + TEXT("%hs: %s is InstancedPerExecution. This is unreliable for Input as you may only interact with the latest spawned Instance"), __func__, *GetNameSafe(Spec->Ability)); + TArray Instances = Spec->GetAbilityInstances(); + const FGameplayAbilityActivationInfo& ActivationInfo = Instances.IsEmpty() ? Spec->ActivationInfo : Instances.Last()->GetCurrentActivationInfoRef(); + // Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server. + AbilitySystem->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec->Handle, ActivationInfo.GetActivationPredictionKey()); +#else + // Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server. + AbilitySystem->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec->Handle, Spec->ActivationInfo.GetActivationPredictionKey()); +#endif + PRAGMA_ENABLE_DEPRECATION_WARNINGS + } + else + { + // Ability is not active, so try to activate it + AbilitySystem->TryActivateAbility(Spec->Handle); + } + } + } +} + +void UGGA_AbilitySystemFunctionLibrary::SetAbilityInputReleased(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return; + } + + FScopedAbilityListLock ActiveScopeLock(*AbilitySystem); + + if (FGameplayAbilitySpec* Spec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + Spec->InputPressed = false; + if (Spec->Ability && Spec->IsActive()) + { + if (Spec->Ability->bReplicateInputDirectly && AbilitySystem->IsOwnerActorAuthoritative() == false) + { + AbilitySystem->ServerSetInputReleased(Spec->Handle); + } + + AbilitySystem->AbilitySpecInputReleased(*Spec); + + + PRAGMA_DISABLE_DEPRECATION_WARNINGS +#if ENGINE_MINOR_VERSION > 4 + // Fixing this up to use the instance activation, but this function should be deprecated as it cannot work with InstancedPerExecution + UE_CLOG(Spec->Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerExecution, LogAbilitySystem, Warning, + TEXT("%hs: %s is InstancedPerExecution. This is unreliable for Input as you may only interact with the latest spawned Instance"), __func__, *GetNameSafe(Spec->Ability)); + TArray Instances = Spec->GetAbilityInstances(); + const FGameplayAbilityActivationInfo& ActivationInfo = Instances.IsEmpty() ? Spec->ActivationInfo : Instances.Last()->GetCurrentActivationInfoRef(); + AbilitySystem->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, Spec->Handle, ActivationInfo.GetActivationPredictionKey()); +#else + AbilitySystem->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, Spec->Handle, Spec->ActivationInfo.GetActivationPredictionKey()); +#endif + PRAGMA_ENABLE_DEPRECATION_WARNINGS + } + } +} + +bool UGGA_AbilitySystemFunctionLibrary::CanActivateAbility(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle AbilityToActivate, FGameplayTagContainer& RelevantTags) +{ + if (!IsValid(AbilitySystem) || !AbilityToActivate.IsValid()) + { + return false; + } + + FGameplayTagContainer FailureTags; + FGameplayAbilitySpec* Spec = AbilitySystem->FindAbilitySpecFromHandle(AbilityToActivate); + if (!Spec) + { + ABILITY_LOG(Warning, TEXT("CanActivateAbility called with invalid Handle")); + return false; + } + + // Lock ability list so our Spec doesn't get destroyed while activating + + const FGameplayAbilityActorInfo* ActorInfo = AbilitySystem->AbilityActorInfo.Get(); + + UGameplayAbility* AbilityCDO = Spec->Ability; + + if (!AbilityCDO) + { + ABILITY_LOG(Warning, TEXT("CanActivateAbility called with ability with invalid definition")); + return false; + } + + // If it's instance once the instanced ability will be set, otherwise it will be null + UGameplayAbility* InstancedAbility = Spec->GetPrimaryInstance(); + + // If we have an instanced ability, call CanActivateAbility on it. + // Otherwise we always do a non instanced CanActivateAbility check using the CDO of the Ability. + UGameplayAbility* const CanActivateAbilitySource = InstancedAbility ? InstancedAbility : AbilityCDO; + + return CanActivateAbilitySource->CanActivateAbility(AbilityToActivate, ActorInfo, nullptr, nullptr, &RelevantTags); +} + +bool UGGA_AbilitySystemFunctionLibrary::SelectFirstCanActivateAbility(const UAbilitySystemComponent* AbilitySystem, TArray Abilities, + FGameplayAbilitySpecHandle& OutAbilityHandle) +{ + for (int32 i = 0; i < Abilities.Num(); i++) + { + static FGameplayTagContainer DummyTags; + DummyTags.Reset(); + if (CanActivateAbility(AbilitySystem, Abilities[i], DummyTags)) + { + OutAbilityHandle = Abilities[i]; + return true; + } + } + return false; +} + +void UGGA_AbilitySystemFunctionLibrary::CancelAbility(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return; + } + AbilitySystem->CancelAbilityHandle(Ability); +} + +void UGGA_AbilitySystemFunctionLibrary::CancelAbilities(UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags) +{ + if (!IsValid(AbilitySystem)) + { + return; + } + AbilitySystem->CancelAbilities(&WithTags, &WithoutTags); +} + +bool UGGA_AbilitySystemFunctionLibrary::IsAbilityInstanceActive(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return false; + } + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + if (UGameplayAbility* AbilityInstance = AbilitySpec->GetPrimaryInstance()) + { + return AbilityInstance->IsActive(); + } + } + return false; +} + +bool UGGA_AbilitySystemFunctionLibrary::IsAbilityActive(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return false; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + return AbilitySpec->IsActive(); + } + + return false; +} + +TArray UGGA_AbilitySystemFunctionLibrary::GetAbilityInstances(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + TArray Abilities; + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return Abilities; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + Abilities = AbilitySpec->GetAbilityInstances(); + return Abilities; + } + + return Abilities; +} + +const UGameplayAbility* UGGA_AbilitySystemFunctionLibrary::GetAbilityCDO(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return nullptr; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + return AbilitySpec->Ability; + } + return nullptr; +} + +int32 UGGA_AbilitySystemFunctionLibrary::GetAbilityLevel(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return 0; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + return AbilitySpec->Level; + } + return 0; +} + +int32 UGGA_AbilitySystemFunctionLibrary::GetAbilityInputId(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return -1; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + return AbilitySpec->InputID; + } + return -1; +} + +UObject* UGGA_AbilitySystemFunctionLibrary::GetAbilitySourceObject(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return nullptr; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + if (AbilitySpec->SourceObject.IsValid()) + { + return AbilitySpec->SourceObject.Get(); + } + } + return nullptr; +} + +FGameplayTagContainer UGGA_AbilitySystemFunctionLibrary::GetAbilityDynamicTags(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return FGameplayTagContainer::EmptyContainer; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { +#if ENGINE_MINOR_VERSION > 4 + return AbilitySpec->GetDynamicSpecSourceTags(); +#else + return AbilitySpec->DynamicAbilityTags; +#endif + } + return FGameplayTagContainer::EmptyContainer; +} + +UGameplayAbility* UGGA_AbilitySystemFunctionLibrary::GetAbilityPrimaryInstance(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability) +{ + if (!IsValid(AbilitySystem) || !Ability.IsValid()) + { + return nullptr; + } + + if (FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(Ability)) + { + return AbilitySpec->GetPrimaryInstance(); + } + + return nullptr; +} + +UAttributeSet* UGGA_AbilitySystemFunctionLibrary::GetAttributeSetByClass(const UAbilitySystemComponent* AbilitySystem, const TSubclassOf AttributeSetClass) +{ + if (!IsValid(AbilitySystem) || !AttributeSetClass) + { + return nullptr; + } + + for (UAttributeSet* SpawnedAttribute : AbilitySystem->GetSpawnedAttributes()) + { + if (SpawnedAttribute && SpawnedAttribute->IsA(AttributeSetClass)) + { + return SpawnedAttribute; + } + } + + return nullptr; +} + +float UGGA_AbilitySystemFunctionLibrary::GetValueAtLevel(const FScalableFloat& ScalableFloat, float Level, FString ContextString) +{ + return ScalableFloat.GetValueAtLevel(Level, &ContextString); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityFunctionLibrary.cpp new file mode 100644 index 0000000..377edac --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityFunctionLibrary.cpp @@ -0,0 +1,71 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayAbilityFunctionLibrary.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Abilities/GameplayAbility.h" + +bool UGGA_GameplayAbilityFunctionLibrary::IsAbilitySpecHandleValid(FGameplayAbilitySpecHandle Handle) +{ + return Handle.IsValid(); +} + +const UGameplayAbility* UGGA_GameplayAbilityFunctionLibrary::GetAbilityCDOFromClass(TSubclassOf AbilityClass) +{ + if (IsValid(AbilityClass)) + { + return AbilityClass->GetDefaultObject(); + } + return nullptr; +} + +FGameplayAbilitySpecHandle UGGA_GameplayAbilityFunctionLibrary::GetCurrentAbilitySpecHandle(const UGameplayAbility* Ability) +{ + return IsValid(Ability) ? Ability->GetCurrentAbilitySpecHandle() : FGameplayAbilitySpecHandle(); +} + +bool UGGA_GameplayAbilityFunctionLibrary::IsAbilityActive(const UGameplayAbility* Ability) +{ + return IsValid(Ability) ? Ability->IsActive() : false; +} + +EGameplayAbilityReplicationPolicy::Type UGGA_GameplayAbilityFunctionLibrary::GetReplicationPolicy(const UGameplayAbility* Ability) +{ + return IsValid(Ability) ? Ability->GetReplicationPolicy() : EGameplayAbilityReplicationPolicy::ReplicateNo; +} + +EGameplayAbilityInstancingPolicy::Type UGGA_GameplayAbilityFunctionLibrary::GetInstancingPolicy(const UGameplayAbility* Ability) +{ + PRAGMA_DISABLE_DEPRECATION_WARNINGS + return IsValid(Ability) ? Ability->GetInstancingPolicy() : EGameplayAbilityInstancingPolicy::NonInstanced; + PRAGMA_ENABLE_DEPRECATION_WARNINGS +} + +FGameplayTagContainer UGGA_GameplayAbilityFunctionLibrary::GetAbilityTags(const UGameplayAbility* Ability) +{ +#if ENGINE_MINOR_VERSION > 4 + return IsValid(Ability) ? Ability->GetAssetTags() : FGameplayTagContainer::EmptyContainer; +#else + return IsValid(Ability) ? Ability->AbilityTags : FGameplayTagContainer::EmptyContainer; +#endif +} + +bool UGGA_GameplayAbilityFunctionLibrary::IsPredictingClient(const UGameplayAbility* Ability) +{ + return IsValid(Ability) ? Ability->IsPredictingClient() : false; +} + +bool UGGA_GameplayAbilityFunctionLibrary::IsForRemoteClient(const UGameplayAbility* Ability) +{ + return IsValid(Ability) ? Ability->IsForRemoteClient() : false; +} + +bool UGGA_GameplayAbilityFunctionLibrary::HasAuthorityOrPredictionKey(const UGameplayAbility* Ability) +{ + if (IsValid(Ability)) + { + const FGameplayAbilityActivationInfo& ActivationInfo = Ability->GetCurrentActivationInfo(); + return Ability->HasAuthorityOrPredictionKey(Ability->GetCurrentActorInfo(), &ActivationInfo); + } + return IsValid(Ability) ? Ability->IsForRemoteClient() : false; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.cpp new file mode 100644 index 0000000..fa6a1d4 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.cpp @@ -0,0 +1,75 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GGA_GameplayAbilityTargetData_Payload.h" + +FGameplayAbilityTargetDataHandle UGGA_GameplayAbilityTargetDataFunctionLibrary::AbilityTargetDataFromPayload(const FInstancedStruct& Payload) +{ + // Construct TargetData + FGGA_GameplayAbilityTargetData_Payload* TargetData = new FGGA_GameplayAbilityTargetData_Payload(Payload); + + // Give it a handle and return + FGameplayAbilityTargetDataHandle Handle; + Handle.Data.Add(TSharedPtr(TargetData)); + + return Handle; +} + +FInstancedStruct UGGA_GameplayAbilityTargetDataFunctionLibrary::GetPayloadFromTargetData(const FGameplayAbilityTargetDataHandle& TargetData, int32 Index) +{ + if (TargetData.Data.IsValidIndex(Index)) + { + if (TargetData.Data[Index]->GetScriptStruct() == FGGA_GameplayAbilityTargetData_Payload::StaticStruct()) + { + if (FGGA_GameplayAbilityTargetData_Payload* Data = static_cast(TargetData.Data[Index].Get())) + { + return Data->Payload; + } + } + } + + return FInstancedStruct(); +} + +FGameplayAbilityTargetDataHandle UGGA_GameplayAbilityTargetDataFunctionLibrary::AbilityTargetDataFromHitResults(const TArray& HitResults, bool OneTargetPerHandle) +{ + // Construct TargetData + if (OneTargetPerHandle) + { + FGameplayAbilityTargetDataHandle Handle; + for (int32 i = 0; i < HitResults.Num(); ++i) + { + if (::IsValid(HitResults[i].GetActor())) + { + FGameplayAbilityTargetDataHandle TempHandle = UAbilitySystemBlueprintLibrary::AbilityTargetDataFromHitResult(HitResults[i]); + Handle.Append(TempHandle); + } + } + return Handle; + } + else + { + FGameplayAbilityTargetDataHandle Handle; + + for (const FHitResult& HitResult : HitResults) + { + FGameplayAbilityTargetData_SingleTargetHit* NewData = new FGameplayAbilityTargetData_SingleTargetHit(HitResult); + Handle.Add(NewData); + } + + return Handle; + } +} + +void UGGA_GameplayAbilityTargetDataFunctionLibrary::AddTargetDataToContext(FGameplayAbilityTargetDataHandle TargetData, FGameplayEffectContextHandle EffectContext) +{ + for (auto Data : TargetData.Data) + { + if (Data.IsValid()) + { + Data->AddTargetDataToContext(EffectContext, true); + } + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayCueFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayCueFunctionLibrary.cpp new file mode 100644 index 0000000..b4c2d25 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayCueFunctionLibrary.cpp @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayCueFunctionLibrary.h" + +#include "AbilitySystemGlobals.h" +#include "GameplayCueManager.h" + +void UGGA_GameplayCueFunctionLibrary::ExecuteGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters) +{ + UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue( + Actor, GameplayCueTag, EGameplayCueEvent::Type::Executed, + GameplayCueParameters); +} + +void UGGA_GameplayCueFunctionLibrary::AddGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters) +{ + UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(Actor, GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters); + UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(Actor, GameplayCueTag, EGameplayCueEvent::Type::WhileActive, GameplayCueParameters); +} + +void UGGA_GameplayCueFunctionLibrary::RemoveGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters) +{ + UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue( + Actor, GameplayCueTag, EGameplayCueEvent::Type::Removed, + GameplayCueParameters); +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.cpp new file mode 100644 index 0000000..89024fc --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.cpp @@ -0,0 +1,172 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayEffectCalculationFunctionLibrary.h" + +#include "AbilitySystemComponent.h" + +const FGameplayEffectSpec& UGGA_GameplayEffectCalculationFunctionLibrary::GetOwningSpec(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetOwningSpec(); +} + +const FGameplayTagContainer& UGGA_GameplayEffectCalculationFunctionLibrary::GetPassedInTags(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetPassedInTags(); +} + +FGameplayEffectContextHandle UGGA_GameplayEffectCalculationFunctionLibrary::GetEffectContext(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetOwningSpec().GetEffectContext(); +} + +float UGGA_GameplayEffectCalculationFunctionLibrary::GetSetByCallerMagnitudeByTag(const FGameplayEffectCustomExecutionParameters& InParams, const FGameplayTag& Tag, bool WarnIfNotFound, + float DefaultIfNotFound) +{ + return InParams.GetOwningSpec().GetSetByCallerMagnitude(Tag, WarnIfNotFound, DefaultIfNotFound); +} + +float UGGA_GameplayEffectCalculationFunctionLibrary::GetSetByCallerMagnitudeByName(const FGameplayEffectCustomExecutionParameters& InParams, const FName& MagnitudeName, bool WarnIfNotFound, + float DefaultIfNotFound) +{ + return InParams.GetOwningSpec().GetSetByCallerMagnitude(MagnitudeName, WarnIfNotFound, DefaultIfNotFound); +} + + +FGameplayTagContainer UGGA_GameplayEffectCalculationFunctionLibrary::GetSourceAggregatedTags(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return *InParams.GetOwningSpec().CapturedSourceTags.GetAggregatedTags(); +} + +FGameplayTagContainer UGGA_GameplayEffectCalculationFunctionLibrary::GetTargetAggregatedTags(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return *InParams.GetOwningSpec().CapturedTargetTags.GetAggregatedTags(); +} + +UAbilitySystemComponent* UGGA_GameplayEffectCalculationFunctionLibrary::GetTargetASC(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetTargetAbilitySystemComponent(); +} + +AActor* UGGA_GameplayEffectCalculationFunctionLibrary::GetTargetActor(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetTargetAbilitySystemComponent()->GetAvatarActor(); +} + +UAbilitySystemComponent* UGGA_GameplayEffectCalculationFunctionLibrary::GetSourceASC(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetSourceAbilitySystemComponent(); +} + +AActor* UGGA_GameplayEffectCalculationFunctionLibrary::GetSourceActor(const FGameplayEffectCustomExecutionParameters& InParams) +{ + return InParams.GetSourceAbilitySystemComponent()->GetAvatarActor(); +} + +bool UGGA_GameplayEffectCalculationFunctionLibrary::AttemptCalculateCapturedAttributeMagnitude(const FGameplayEffectCustomExecutionParameters& InParams, + TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, float& OutMagnitude) +{ + FAggregatorEvaluateParameters EvaluationParams; + const FGameplayEffectSpec& EffectSpec = InParams.GetOwningSpec(); + EvaluationParams.SourceTags = EffectSpec.CapturedSourceTags.GetAggregatedTags(); + EvaluationParams.TargetTags = EffectSpec.CapturedTargetTags.GetAggregatedTags(); + + for (const FGameplayEffectAttributeCaptureDefinition& AttributeCaptureDefinition : InAttributeCaptureDefinitions) + { + if (AttributeCaptureDefinition.AttributeToCapture == InAttribute) + { + return InParams.AttemptCalculateCapturedAttributeMagnitude(AttributeCaptureDefinition, EvaluationParams, OutMagnitude); + } + } + return false; +} + +bool UGGA_GameplayEffectCalculationFunctionLibrary::AttemptCalculateCapturedAttributeMagnitudeExt(const FGameplayEffectCustomExecutionParameters& InParams, const FGameplayTagContainer& SourceTags, + const FGameplayTagContainer& TargetTags, + TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, float& OutMagnitude) +{ + FAggregatorEvaluateParameters EvaluationParams; + EvaluationParams.SourceTags = &SourceTags; + EvaluationParams.TargetTags = &TargetTags; + + for (const FGameplayEffectAttributeCaptureDefinition& AttributeCaptureDefinition : InAttributeCaptureDefinitions) + { + if (AttributeCaptureDefinition.AttributeToCapture == InAttribute) + { + return InParams.AttemptCalculateCapturedAttributeMagnitude(AttributeCaptureDefinition, EvaluationParams, OutMagnitude); + } + } + return false; +} + +bool UGGA_GameplayEffectCalculationFunctionLibrary::AttemptCalculateCapturedAttributeMagnitudeWithBase(const FGameplayEffectCustomExecutionParameters& InParams, + TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, float InBaseValue, float& OutMagnitude) +{ + FAggregatorEvaluateParameters EvaluationParams; + const FGameplayEffectSpec& EffectSpec = InParams.GetOwningSpec(); + EvaluationParams.SourceTags = EffectSpec.CapturedSourceTags.GetAggregatedTags(); + EvaluationParams.TargetTags = EffectSpec.CapturedTargetTags.GetAggregatedTags(); + + for (const FGameplayEffectAttributeCaptureDefinition& AttributeCaptureDefinition : InAttributeCaptureDefinitions) + { + if (AttributeCaptureDefinition.AttributeToCapture == InAttribute) + { + return InParams.AttemptCalculateCapturedAttributeMagnitudeWithBase(AttributeCaptureDefinition, EvaluationParams, InBaseValue, OutMagnitude); + } + } + return false; +} + +FGameplayEffectCustomExecutionOutput UGGA_GameplayEffectCalculationFunctionLibrary::AddOutputModifier(FGameplayEffectCustomExecutionOutput& InExecutionOutput, FGameplayAttribute InAttribute, + EGameplayModOp::Type InModifierOp, float InMagnitude) +{ + if (InAttribute.IsValid()) + { + FGameplayModifierEvaluatedData Data; + Data.Attribute = InAttribute; + Data.ModifierOp = InModifierOp; + Data.Magnitude = InMagnitude; + InExecutionOutput.AddOutputModifier(Data); + } + return InExecutionOutput; +} + + +void UGGA_GameplayEffectCalculationFunctionLibrary::MarkConditionalGameplayEffectsToTrigger(FGameplayEffectCustomExecutionOutput& InExecutionOutput) +{ + InExecutionOutput.MarkConditionalGameplayEffectsToTrigger(); +} + +void UGGA_GameplayEffectCalculationFunctionLibrary::MarkGameplayCuesHandledManually(FGameplayEffectCustomExecutionOutput& InExecutionOutput) +{ + InExecutionOutput.MarkGameplayCuesHandledManually(); +} + +void UGGA_GameplayEffectCalculationFunctionLibrary::MarkStackCountHandledManually(FGameplayEffectCustomExecutionOutput& InExecutionOutput) +{ + InExecutionOutput.MarkStackCountHandledManually(); +} + +FGameplayEffectContextHandle UGGA_GameplayEffectCalculationFunctionLibrary::GetEffectContextFromSpec(const FGameplayEffectSpec& EffectSpec) +{ + return EffectSpec.GetEffectContext(); +} + +void UGGA_GameplayEffectCalculationFunctionLibrary::AddAssetTagForPreMod(const FGameplayEffectCustomExecutionParameters& InParams, FGameplayTag NewGameplayTag) +{ + if (FGameplayEffectSpec* Spec = InParams.GetOwningSpecForPreExecuteMod()) + { + Spec->AddDynamicAssetTag(NewGameplayTag); + } +} + +void UGGA_GameplayEffectCalculationFunctionLibrary::AddAssetTagsForPreMod(const FGameplayEffectCustomExecutionParameters& InParams, FGameplayTagContainer NewGameplayTags) +{ + if (FGameplayEffectSpec* Spec = InParams.GetOwningSpecForPreExecuteMod()) + { + Spec->AppendDynamicAssetTags(NewGameplayTags); + } +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectContainerFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectContainerFunctionLibrary.cpp new file mode 100644 index 0000000..cc19c6e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectContainerFunctionLibrary.cpp @@ -0,0 +1,158 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayEffectContainerFunctionLibrary.h" + +#include "AbilitySystemBlueprintLibrary.h" +#include "AbilitySystemComponent.h" +#include "GameplayAbilitySpec.h" +#include "AbilitySystemGlobals.h" +#include "GGA_LogChannels.h" +#include "TargetingSystem/TargetingSubsystem.h" + +bool UGGA_GameplayEffectContainerFunctionLibrary::IsValidContainer(const FGGA_GameplayEffectContainer& Container) +{ + return !Container.TargetGameplayEffectClasses.IsEmpty() && Container.TargetingPreset != nullptr; +} + +bool UGGA_GameplayEffectContainerFunctionLibrary::HasValidEffects(const FGGA_GameplayEffectContainerSpec& ContainerSpec) +{ + return ContainerSpec.HasValidEffects(); +} + +bool UGGA_GameplayEffectContainerFunctionLibrary::HasValidTargets(const FGGA_GameplayEffectContainerSpec& ContainerSpec) +{ + return ContainerSpec.HasValidTargets(); +} + +FGGA_GameplayEffectContainerSpec UGGA_GameplayEffectContainerFunctionLibrary::AddTargets(const FGGA_GameplayEffectContainerSpec& ContainerSpec, const TArray& HitResults, + const TArray& TargetActors) +{ + FGGA_GameplayEffectContainerSpec NewSpec = ContainerSpec; + NewSpec.AddTargets(HitResults, TargetActors); + return NewSpec; +} + +FGGA_GameplayEffectContainerSpec UGGA_GameplayEffectContainerFunctionLibrary::MakeEffectContainerSpec(const FGGA_GameplayEffectContainer& Container, + const FGameplayEventData& EventData, int32 OverrideGameplayLevel, UGameplayAbility* SourceAbility) +{ + FGGA_GameplayEffectContainerSpec ReturnSpec; + + if (SourceAbility== nullptr && EventData.Instigator == nullptr) + { + UE_LOG(LogGGA_AbilitySystem, Warning, TEXT("Missing instigator within EventData, It's required to look for Source AbilitySystemComponent!")) + return ReturnSpec; + } + // First figure out our actor info + + UAbilitySystemComponent* SourceASC = SourceAbility?SourceAbility->GetAbilitySystemComponentFromActorInfo():UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(EventData.Instigator); + + if (SourceASC && SourceASC->GetAvatarActor()) + { + if (Container.TargetingPreset.Get()) + { + if (UTargetingSubsystem* TargetingSubsystem = UTargetingSubsystem::Get(SourceASC->GetWorld())) + { + FTargetingSourceContext SourceContext; + SourceContext.SourceActor = SourceASC->GetAvatarActor(); + + FTargetingRequestHandle TargetingHandle = UTargetingSubsystem::MakeTargetRequestHandle(Container.TargetingPreset, SourceContext); + + FTargetingRequestDelegate Delegate = FTargetingRequestDelegate::CreateLambda([&](FTargetingRequestHandle InTargetingHandle) + { + TArray HitResults; + TArray TargetActors; + TargetingSubsystem->GetTargetingResults(InTargetingHandle, HitResults); + ReturnSpec.AddTargets(HitResults, TargetActors); + }); + + UE_LOG(LogGGA_AbilitySystem, VeryVerbose, TEXT("Starting immediate targeting task for EffectContainer.")); + + FTargetingImmediateTaskData& ImmediateTaskData = FTargetingImmediateTaskData::FindOrAdd(TargetingHandle); + ImmediateTaskData.bReleaseOnCompletion = true; + + TargetingSubsystem->ExecuteTargetingRequestWithHandle(TargetingHandle, Delegate); + } + } + + int32 Level = OverrideGameplayLevel >= 0 ? OverrideGameplayLevel : EventData.EventMagnitude; + + // Build GameplayEffectSpecs for each applied effect + for (const TSubclassOf& EffectClass : Container.TargetGameplayEffectClasses) + { + FGameplayEffectSpecHandle SpecHandle = SourceAbility?SourceAbility->MakeOutgoingGameplayEffectSpec(EffectClass, Level):SourceASC->MakeOutgoingSpec(EffectClass, Level, SourceASC->MakeEffectContext()); + FGameplayEffectContextHandle ContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(SpecHandle); + + if (SourceAbility==nullptr && EventData.OptionalObject) + { + ContextHandle.AddSourceObject(EventData.OptionalObject); + } + + ReturnSpec.TargetGameplayEffectSpecs.Add(SpecHandle); + } + + if (EventData.TargetData.Num() > 0) + { + ReturnSpec.TargetData.Append(EventData.TargetData); + } + } + return ReturnSpec; +} + +TArray UGGA_GameplayEffectContainerFunctionLibrary::ApplyEffectContainerSpec(UGameplayAbility* ExecutingAbility, const FGGA_GameplayEffectContainerSpec& ContainerSpec) +{ + TArray AllEffects; + + if (!IsValid(ExecutingAbility) || !ExecutingAbility->IsInstantiated()) + { + UE_LOG(LogGGA_Ability, Error, TEXT("Requires \"Executing ability\" to apply effect container spec.")) + return AllEffects; + } + + const FGameplayAbilityActorInfo* ActorInfo = ExecutingAbility->GetCurrentActorInfo(); + const FGameplayAbilityActivationInfo& ActivationInfo = ExecutingAbility->GetCurrentActivationInfoRef(); + + // Iterate list of effect specs and apply them to their target data + for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs) + { + TArray EffectHandles; + + if (SpecHandle.IsValid() && ExecutingAbility->HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo)) + { + FScopedTargetListLock ActiveScopeLock(*ActorInfo->AbilitySystemComponent, *ExecutingAbility); + + for (TSharedPtr Data : ContainerSpec.TargetData.Data) + { + if (Data.IsValid()) + { + AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get(), ActorInfo->AbilitySystemComponent->GetPredictionKeyForNewAction())); + } + else + { + UE_LOG(LogGGA_Ability, Warning, TEXT("ApplyGameplayEffectSpecToTarget invalid target data passed in. Ability: %s"), *ExecutingAbility->GetPathName()); + } + } + } + } + + return AllEffects; +} + +TArray UGGA_GameplayEffectContainerFunctionLibrary::ApplyExternalEffectContainerSpec(const FGGA_GameplayEffectContainerSpec& ContainerSpec) +{ + TArray AllEffects; + + // Iterate list of gameplay effects + for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs) + { + if (SpecHandle.IsValid()) + { + // If effect is valid, iterate list of targets and apply to all + for (TSharedPtr Data : ContainerSpec.TargetData.Data) + { + AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get())); + } + } + } + return AllEffects; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectFunctionLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectFunctionLibrary.cpp new file mode 100644 index 0000000..f3a3e92 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Private/Utilities/GGA_GameplayEffectFunctionLibrary.cpp @@ -0,0 +1,284 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGA_GameplayEffectFunctionLibrary.h" +#include "GameplayEffect.h" +#include "Blueprint/BlueprintExceptionInfo.h" +#include "UObject/EnumProperty.h" +#include "GGA_GameplayEffectContext.h" +#include "GGA_LogChannels.h" + +#define LOCTEXT_NAMESPACE "UGGA_GameplayEffectFunctionLibrary" + +// UGameplayEffect* UGGA_GameplayEffectFunctionLibrary::MakeRuntimeGameplayEffect(FString UniqueName, EGameplayEffectDurationType DurationPolicy, TArray AttributeModifiers) +// { +// UGameplayEffect* GameplayEffect = NewObject(GetTransientPackage(), FName("RuntimeGE_" + GetNameSafe(this) + UniqueName)); +// GameplayEffect->DurationPolicy = DurationPolicy; +// GameplayEffect->Modifiers = AttributeModifiers; +// return GameplayEffect; +// } + +float UGGA_GameplayEffectFunctionLibrary::GetSetByCallerMagnitudeByTag(FGameplayEffectSpecHandle SpecHandle, FGameplayTag DataTag, bool WarnIfNotFound, float DefaultIfNotFound) +{ + FGameplayEffectSpec* Spec = SpecHandle.Data.Get(); + if (Spec) + { + return Spec->GetSetByCallerMagnitude(DataTag, WarnIfNotFound, DefaultIfNotFound); + } + return 0.0f; +} + +float UGGA_GameplayEffectFunctionLibrary::GetSetByCallerMagnitudeByTagFromSpec(const FGameplayEffectSpec& EffectSpec, FGameplayTag DataTag, bool WarnIfNotFound, float DefaultIfNotFound) +{ + return EffectSpec.GetSetByCallerMagnitude(DataTag, WarnIfNotFound, DefaultIfNotFound); +} + +float UGGA_GameplayEffectFunctionLibrary::GetSetByCallerMagnitudeByName(FGameplayEffectSpecHandle SpecHandle, FName DataName) +{ + FGameplayEffectSpec* Spec = SpecHandle.Data.Get(); + if (Spec) + { + return Spec->GetSetByCallerMagnitude(DataName, false); + } + + return 0.0f; +} + +bool UGGA_GameplayEffectFunctionLibrary::IsActiveGameplayEffectHandleValid(FActiveGameplayEffectHandle Handle) +{ + return Handle.IsValid(); +} + +void UGGA_GameplayEffectFunctionLibrary::GetOwnedGameplayTags(FGameplayEffectContextHandle EffectContext, FGameplayTagContainer& ActorTagContainer, FGameplayTagContainer& SpecTagContainer) +{ + return EffectContext.GetOwnedGameplayTags(ActorTagContainer, SpecTagContainer); +} + +void UGGA_GameplayEffectFunctionLibrary::AddInstigator(FGameplayEffectContextHandle EffectContext, AActor* InInstigator, AActor* InEffectCauser) +{ + EffectContext.AddInstigator(InInstigator, InEffectCauser); +} + +void UGGA_GameplayEffectFunctionLibrary::SetEffectCauser(FGameplayEffectContextHandle EffectContext, AActor* InEffectCauser) +{ + EffectContext.AddInstigator(EffectContext.GetInstigator(), InEffectCauser); +} + +void UGGA_GameplayEffectFunctionLibrary::SetAbility(FGameplayEffectContextHandle EffectContext, const UGameplayAbility* InGameplayAbility) +{ + EffectContext.SetAbility(InGameplayAbility); +} + +const UGameplayAbility* UGGA_GameplayEffectFunctionLibrary::GetAbilityCDO(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.GetAbility(); +} + +const UGameplayAbility* UGGA_GameplayEffectFunctionLibrary::GetAbilityInstance(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.GetAbilityInstance_NotReplicated(); +} + +int32 UGGA_GameplayEffectFunctionLibrary::GetAbilityLevel(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.GetAbilityLevel(); +} + +void UGGA_GameplayEffectFunctionLibrary::AddSourceObject(FGameplayEffectContextHandle EffectContext, const UObject* NewSourceObject) +{ + if (NewSourceObject) + { + EffectContext.AddSourceObject(NewSourceObject); + } +} + +bool UGGA_GameplayEffectFunctionLibrary::HasOrigin(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.HasOrigin(); +} + +FGGA_GameplayEffectContext* UGGA_GameplayEffectFunctionLibrary::GetEffectContextPtr(FGameplayEffectContextHandle EffectContext) +{ + if (!EffectContext.IsValid()) + { + GGA_LOG(Warning, "Try access invalid effect context!") + return nullptr; + } + if (!EffectContext.Get()->GetScriptStruct()->IsChildOf(FGGA_GameplayEffectContext::StaticStruct())) + { + GGA_LOG(Warning, "The GameplayEffectContext type is not FGGA_GameplayEffectContext! " + "Make sure you are setting AbilitySystemGlobalsClassName as GGA_AbilitySystemGlobals in Gameplay Abilities Settings Under Project Settings! ") + return nullptr; + } + return static_cast(EffectContext.Get()); +} + +UAbilitySystemComponent* UGGA_GameplayEffectFunctionLibrary::GetInstigatorAbilitySystemComponent(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.GetInstigatorAbilitySystemComponent(); +} + +UAbilitySystemComponent* UGGA_GameplayEffectFunctionLibrary::GetOriginalInstigatorAbilitySystemComponent(FGameplayEffectContextHandle EffectContext) +{ + return EffectContext.GetOriginalInstigatorAbilitySystemComponent(); +} + +bool UGGA_GameplayEffectFunctionLibrary::HasContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType) +{ + if (const FGGA_GameplayEffectContext* Context = GetEffectContextPtr(EffectContext)) + { + return Context->FindPayloadByType(PayloadType) != nullptr; + } + return false; +} + +bool UGGA_GameplayEffectFunctionLibrary::GetContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, FInstancedStruct& OutPayload) +{ + if (FGGA_GameplayEffectContext* Context = GetEffectContextPtr(EffectContext)) + { + if (FInstancedStruct* Found = Context->FindPayloadByType(PayloadType)) + { + OutPayload = *Found; + return true; + } + } + + return false; +} + +FInstancedStruct UGGA_GameplayEffectFunctionLibrary::GetValidContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, bool& bValid) +{ + bValid = false; + if (FGGA_GameplayEffectContext* Context = GetEffectContextPtr(EffectContext)) + { + if (FInstancedStruct* Found = Context->FindPayloadByType(PayloadType)) + { + bValid = true; + return *Found; + } + } + return FInstancedStruct(); +} + +void UGGA_GameplayEffectFunctionLibrary::GetContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, EGGA_ContextPayloadResult& ExecResult, int32& Value) +{ + // We should never hit this! stubs to avoid NoExport on the class. + checkNoEntry(); +} + +DEFINE_FUNCTION(UGGA_GameplayEffectFunctionLibrary::execGetContextPayload) +{ + P_GET_STRUCT_REF(FGameplayEffectContextHandle, EffectContext); + P_GET_OBJECT(const UScriptStruct, PayloadType); + P_GET_ENUM_REF(EGGA_ContextPayloadResult, ExecResult); + + // Read wildcard Value input + Stack.MostRecentPropertyAddress = nullptr; + Stack.MostRecentPropertyContainer = nullptr; + Stack.StepCompiledIn(nullptr); + + const FStructProperty* ValueProp = CastField(Stack.MostRecentProperty); + void* ValuePtr = Stack.MostRecentPropertyAddress; + + P_FINISH; + + P_NATIVE_BEGIN; + + ExecResult = EGGA_ContextPayloadResult::NotValid; + + if (!ValueProp || !ValuePtr || !PayloadType) + { + FBlueprintExceptionInfo ExceptionInfo( + EBlueprintExceptionType::AbortExecution, + LOCTEXT("InstancedStruct_GetInvalidValueWarning", "Failed to resolve the Value or PayloadType for Get Context Payload") + ); + FBlueprintCoreDelegates::ThrowScriptException(P_THIS, Stack, ExceptionInfo); + } + else + { + FInstancedStruct OutPayload; + if (GetContextPayload(EffectContext, PayloadType, OutPayload)) + { + if (OutPayload.IsValid() && OutPayload.GetScriptStruct()->IsChildOf(ValueProp->Struct)) + { + // Copy the struct data to the output Value + ValueProp->Struct->CopyScriptStruct(ValuePtr, OutPayload.GetMemory()); + ExecResult = EGGA_ContextPayloadResult::Valid; + } + else + { + ExecResult = EGGA_ContextPayloadResult::NotValid; + } + } + else + { + ExecResult = EGGA_ContextPayloadResult::NotValid; + } + } + + P_NATIVE_END; +} + + +void UGGA_GameplayEffectFunctionLibrary::SetContextPayload(FGameplayEffectContextHandle EffectContext, EGGA_ContextPayloadResult& ExecResult, const int32& Value) +{ + // We should never hit this! stubs to avoid NoExport on the class. + checkNoEntry(); +} + +DEFINE_FUNCTION(UGGA_GameplayEffectFunctionLibrary::execSetContextPayload) +{ + P_GET_STRUCT_REF(FGameplayEffectContextHandle, EffectContext); + P_GET_ENUM_REF(EGGA_ContextPayloadResult, ExecResult); + + // Read wildcard Value input + Stack.MostRecentPropertyAddress = nullptr; + Stack.MostRecentPropertyContainer = nullptr; + Stack.StepCompiledIn(nullptr); + + const FStructProperty* ValueProp = CastField(Stack.MostRecentProperty); + const void* ValuePtr = Stack.MostRecentPropertyAddress; + + P_FINISH; + + P_NATIVE_BEGIN; + + ExecResult = EGGA_ContextPayloadResult::NotValid; + + if (!ValueProp || !ValuePtr || !EffectContext.IsValid()) + { + FBlueprintExceptionInfo ExceptionInfo( + EBlueprintExceptionType::AbortExecution, + LOCTEXT("InstancedStruct_SetInvalidValueWarning", "Failed to resolve Value or EffectContext for Set Instanced Struct Value") + ); + FBlueprintCoreDelegates::ThrowScriptException(P_THIS, Stack, ExceptionInfo); + } + else + { + // Create an FInstancedStruct from the input struct + FInstancedStruct Payload; + Payload.InitializeAs(ValueProp->Struct, static_cast(ValuePtr)); + + if (Payload.IsValid()) + { + // Call the existing SetPayload function + if (FGGA_GameplayEffectContext* ContextPtr = GetEffectContextPtr(EffectContext)) + { + ContextPtr->AddOrOverwriteData(Payload); + ExecResult = EGGA_ContextPayloadResult::Valid; + } + } + else + { + FBlueprintExceptionInfo ExceptionInfo( + EBlueprintExceptionType::AbortExecution, + LOCTEXT("SetGameplayEffectContextPayload", "Failed to create valid InstancedStruct from Value") + ); + FBlueprintCoreDelegates::ThrowScriptException(P_THIS, Stack, ExceptionInfo); + } + } + + P_NATIVE_END; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilityCost.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilityCost.h new file mode 100644 index 0000000..0d9515c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilityCost.h @@ -0,0 +1,87 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayAbilitySpec.h" +#include "Abilities/GameplayAbility.h" +#include "GGA_AbilityCost.generated.h" + +/** + * Base class for defining costs associated with a gameplay ability (e.g., ammo, charges). + * 定义与游戏技能相关成本的基类(例如弹药、次数)。 + */ +UCLASS(Blueprintable, DefaultToInstanced, EditInlineNew, Abstract) +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityCost : public UObject +{ + GENERATED_BODY() + +public: + /** + * Constructor for the ability cost. + * 技能成本构造函数。 + */ + UGGA_AbilityCost() + { + } + + /** + * Checks if the ability cost can be afforded. + * 检查是否能支付技能成本。 + * @param Ability The gameplay ability. 游戏技能。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param OptionalRelevantTags Tags for failure reasons (optional, output). 失败原因标签(可选,输出)。 + * @return True if the cost can be paid, false otherwise. 如果成本可支付则返回true,否则返回false。 + */ + virtual bool CheckCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, FGameplayTagContainer* OptionalRelevantTags) const; + + /** + * Applies the ability cost to the target. + * 将技能成本应用于目标。 + * @param Ability The gameplay ability. 游戏技能。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + virtual void ApplyCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo); + + /** + * Checks if the cost should only be applied on a successful hit. + * 检查成本是否仅在成功命中时应用。 + * @return True if the cost applies only on hit, false otherwise. 如果成本仅在命中时应用则返回true,否则返回false。 + */ + bool ShouldOnlyApplyCostOnHit() const { return bOnlyApplyCostOnHit; } + +protected: + /** + * Blueprint event for checking the ability cost. + * 检查技能成本的蓝图事件。 + * @param Ability The gameplay ability. 游戏技能。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param OptionalRelevantTags Tags for failure reasons. 失败原因标签。 + * @return True if the cost can be paid, false otherwise. 如果成本可支付则返回true,否则返回false。 + */ + UFUNCTION(BlueprintImplementableEvent, Category=Costs, meta=(DisplayName="Check Cost")) + bool BlueprintCheckCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo, + const FGameplayTagContainer& OptionalRelevantTags) const; + + /** + * Blueprint event for applying the ability cost. + * 应用技能成本的蓝图事件。 + * @param Ability The gameplay ability. 游戏技能。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + UFUNCTION(BlueprintImplementableEvent, Category=Costs, meta=(DisplayName="Apply Cost")) + void BlueprintApplyCost(const UGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo, const FGameplayAbilityActivationInfo& ActivationInfo); + + /** + * Determines if the cost applies only on a successful hit. + * 确定成本是否仅在成功命中时应用。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Costs) + bool bOnlyApplyCostOnHit = false; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilitySet.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilitySet.h new file mode 100644 index 0000000..9063673 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_AbilitySet.h @@ -0,0 +1,321 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "ActiveGameplayEffectHandle.h" +#include "Engine/DataAsset.h" +#include "AttributeSet.h" +#include "GameplayTagContainer.h" +#include "GameplayAbilitySpecHandle.h" +#include "GGA_AbilitySet.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogGGA_AbilitySet, Log, All) + +class UGameplayEffect; +class UGameplayAbility; +class UGAbilitySystemComponent; +class UObject; + +/** + * Struct for granting gameplay abilities in an ability set. + * 用于在技能集赋予游戏技能的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_AbilitySet_GameplayAbility +{ + GENERATED_BODY() + + /** + * The gameplay ability class to grant. + * 要赋予的技能类。 + */ + UPROPERTY(EditAnywhere, Category = "GGA", BlueprintReadWrite, meta=(AllowAbstract=false)) + TSoftClassPtr Ability = nullptr; + + /** + * The level of the ability to grant. + * 要赋予的技能等级。 + */ + UPROPERTY(EditAnywhere, Category = "GGA", BlueprintReadWrite) + int32 AbilityLevel = 1; + + /** + * The input ID for activating/cancelling the ability. + * 用于激活/取消技能的输入ID。 + */ + UPROPERTY(EditAnywhere, Category = "GGA", BlueprintReadWrite) + int32 InputID = -1; + + /** + * Dynamic tags to add to the ability. + * 添加到技能的动态标签。 + */ + UPROPERTY(EditAnywhere, Category = "GGA", BlueprintReadWrite) + FGameplayTagContainer DynamicTags; + +#if WITH_EDITORONLY_DATA + /** + * Generates an editor-friendly name. + * 生成编辑器友好的名称。 + */ + void MakeEditorFriendlyName(); + + /** + * Editor-friendly name for the ability. + * 技能的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; + + /** + * Toggles whether the ability is granted (editor-only, for debugging). + * 切换是否赋予技能(仅编辑器,用于调试)。 + */ + UPROPERTY(EditAnywhere, Category = "GGA") + bool bAbilityEnabled{true}; +#endif +}; + +/** + * Struct for granting gameplay effects in an ability set. + * 用于在技能集赋予游戏效果的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_AbilitySet_GameplayEffect +{ + GENERATED_BODY() + + /** + * The gameplay effect class to grant. + * 要赋予的效果类。 + */ + UPROPERTY(EditDefaultsOnly, Category = "GGA") + TSoftClassPtr GameplayEffect = nullptr; + + /** + * The level of the gameplay effect to grant. + * 要赋予的效果等级。 + */ + UPROPERTY(EditDefaultsOnly, Category = "GGA") + float EffectLevel = 1.0f; + +#if WITH_EDITORONLY_DATA + /** + * Generates an editor-friendly name. + * 生成编辑器友好的名称。 + */ + void MakeEditorFriendlyName(); + + /** + * Editor-friendly name for the effect. + * 效果的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; + + /** + * Toggles whether the effect is granted (editor-only, for debugging). + * 切换是否赋予效果(仅编辑器,用于调试)。 + */ + UPROPERTY(EditAnywhere, Category = "GGA") + bool bEffectEnabled{true}; +#endif +}; + +/** + * Struct for granting attribute sets in an ability set. + * 用于在技能集赋予属性集的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_AbilitySet_AttributeSet +{ + GENERATED_BODY() + + /** + * The attribute set class to grant. + * 要赋予的属性集类。 + */ + UPROPERTY(EditDefaultsOnly, Category = "GGA") + TSoftClassPtr AttributeSet; + +#if WITH_EDITORONLY_DATA + /** + * Generates an editor-friendly name. + * 生成编辑器友好的名称。 + */ + void MakeEditorFriendlyName(); + + /** + * Editor-friendly name for the attribute set. + * 属性集的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; + + /** + * Toggles whether the attribute set is granted (editor-only, for debugging). + * 切换是否赋予属性集(仅编辑器,用于调试)。 + */ + UPROPERTY(EditAnywhere, Category = "GGA") + bool bAttributeSetEnabled{true}; +#endif +}; + +/** + * Struct for storing handles to granted abilities, effects, and attribute sets. + * 存储已赋予的技能、效果和属性集句柄的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_AbilitySet_GrantedHandles +{ + GENERATED_BODY() + + /** + * Adds an ability spec handle. + * 添加技能句柄。 + * @param Handle The ability spec handle. 技能句柄。 + */ + void AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle); + + /** + * Adds a gameplay effect handle. + * 添加游戏效果句柄。 + * @param Handle The gameplay effect handle. 游戏效果句柄。 + */ + void AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle); + + /** + * Adds an attribute set. + * 添加属性集。 + * @param Set The attribute set to add. 要添加的属性集。 + */ + void AddAttributeSet(UAttributeSet* Set); + + /** + * Removes granted items from an ability system component. + * 从技能系统组件移除已赋予的项。 + * @param ASC The ability system component. 技能系统组件。 + */ + void TakeFromAbilitySystem(UAbilitySystemComponent* ASC); + +protected: + /** + * Handles to granted abilities. + * 已赋予的技能句柄。 + */ + UPROPERTY(BlueprintReadOnly, Category="GGA") + TArray AbilitySpecHandles; + + /** + * Handles to granted gameplay effects. + * 已赋予的游戏效果句柄。 + */ + UPROPERTY(BlueprintReadOnly, Category="GGA") + TArray GameplayEffectHandles; + + /** + * Pointers to granted attribute sets. + * 已赋予的属性集指针。 + */ + UPROPERTY() + TArray> GrantedAttributeSets; +}; + +/** + * Data asset for granting gameplay abilities, effects, and attribute sets. + * 用于赋予游戏技能、效果和属性集的数据资产。 + */ +UCLASS(BlueprintType, Const) +class GENERICGAMEPLAYABILITIES_API UGGA_AbilitySet : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /** + * Constructor for the ability set. + * 技能集构造函数。 + */ + UGGA_AbilitySet(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Grants the ability set to an ability system component. + * 将技能集赋予技能系统组件。 + * @param ASC The ability system component. 技能系统组件。 + * @param OutGrantedHandles Handles for granted items (output). 已赋予项的句柄(输出)。 + * @param SourceObject Optional source object. 可选的源对象。 + * @param OverrideLevel Optional level override. 可选的等级覆盖。 + */ + void GiveToAbilitySystem(UAbilitySystemComponent* ASC, FGGA_AbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject = nullptr, int32 OverrideLevel = -1) const; + + /** + * Grants an ability set to an ability system component (static). + * 将技能集赋予技能系统组件(静态)。 + * @param AbilitySet The ability set to grant. 要赋予的技能集。 + * @param ASC The ability system component. 技能系统组件。 + * @param SourceObject Optional source object. 可选的源对象。 + * @param OverrideLevel Optional level override. 可选的等级覆盖。 + * @return Handles for granted items. 已赋予项的句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySet", BlueprintAuthorityOnly) + static FGGA_AbilitySet_GrantedHandles GiveAbilitySetToAbilitySystem(TSoftObjectPtr AbilitySet, UAbilitySystemComponent* ASC, UObject* SourceObject, int32 OverrideLevel = -1); + + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySet", BlueprintAuthorityOnly) + static TArray GiveAbilitySetsToAbilitySystem(TArray> AbilitySets, UAbilitySystemComponent* ASC, UObject* SourceObject, + int32 OverrideLevel = -1); + + /** + * Removes granted ability sets from an ability system component. + * 从技能系统组件移除已赋予的技能集。 + * @param GrantedHandles Handles for granted items. 已赋予项的句柄。 + * @param ASC The ability system component. 技能系统组件。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySet", BlueprintAuthorityOnly) + static void TakeAbilitySetFromAbilitySystem(UPARAM(ref) + FGGA_AbilitySet_GrantedHandles& GrantedHandles, UAbilitySystemComponent* ASC); + + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySet", BlueprintAuthorityOnly) + static void TakeAbilitySetsFromAbilitySystem(UPARAM(ref) + TArray& GrantedHandles, UAbilitySystemComponent* ASC); + +protected: + /** + * Gameplay abilities to grant. + * 要赋予的游戏技能。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GGA|AbilitySet", meta=(TitleProperty=EditorFriendlyName, NoElementDuplicate)) + TArray GrantedGameplayAbilities; + + /** + * Gameplay effects to grant. + * 要赋予的游戏效果。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GGA|AbilitySet", meta=(TitleProperty=EditorFriendlyName, NoElementDuplicate)) + TArray GrantedGameplayEffects; + + /** + * Attribute sets to grant. + * 要赋予的属性集。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GGA|AbilitySet", meta=(TitleProperty=EditorFriendlyName, NoElementDuplicate)) + TArray GrantedAttributes; + +#if WITH_EDITOR + /** + * Pre-save processing for editor. + * 编辑器预保存处理。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Handles property changes in the editor. + * 处理编辑器中的属性更改。 + */ + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + + /** + * Handles chain property changes in the editor. + * 处理编辑器中的链式属性更改。 + */ + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbility.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbility.h new file mode 100644 index 0000000..495785b --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbility.h @@ -0,0 +1,452 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemEnumLibrary.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "GGA_GameplayAbilityInterface.h" +#include "Tickable.h" +#include "Abilities/GameplayAbility.h" +#include "GGA_GameplayAbility.generated.h" + +class UGGA_AbilityCost; + +DECLARE_STATS_GROUP(TEXT("GameplayAbility"), STATGROUP_GameplayAbility, STATCAT_Advanced) + +/** + * Extended gameplay ability class for custom functionality. + * 扩展的游戏技能类,提供自定义功能。 + */ +UCLASS(Abstract) +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayAbility : public UGameplayAbility, public FTickableGameObject, public IGGA_GameplayAbilityInterface +{ + GENERATED_BODY() + + friend class UGGA_AbilitySystemComponent; + +public: + /** + * Constructor for the gameplay ability. + * 游戏技能构造函数。 + */ + UGGA_GameplayAbility(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Updates the ability each frame. + * 每帧更新技能逻辑。 + * @param DeltaTime Time since last frame. 自上一帧以来的时间。 + */ + virtual void Tick(float DeltaTime) override; + + /** + * Retrieves the stat ID for performance tracking. + * 获取用于性能跟踪的统计ID。 + * @return The stat ID. 统计ID。 + */ + virtual TStatId GetStatId() const override; + + /** + * Checks if the ability can be ticked. + * 检查技能是否可被Tick。 + * @return True if tickable, false otherwise. 如果可Tick则返回true,否则返回false。 + */ + virtual bool IsTickable() const override; + + /** + * Blueprint event for per-frame ability updates. + * 每帧技能更新的蓝图事件。 + * @param DeltaTime Time since last frame. 自上一帧以来的时间。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GGA|Ability") + void AbilityTick(float DeltaTime); + virtual void AbilityTick_Implementation(float DeltaTime); + + /** + * Checks if the input bound to this ability is pressed. + * 检查绑定到此技能的输入是否被按下。 + * @return True if the input is pressed, false otherwise. 如果输入被按下则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Ability|Input") + virtual bool IsInputPressed() const; + + /** + * Retrieves the controller associated with this ability. + * 获取与此技能相关的控制器。 + * @return The controller. 控制器。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Ability") + virtual AController* GetControllerFromActorInfo() const; + + /** + * Retrieves the activation group for this ability. + * 获取此技能的激活组。 + * @return The activation group. 激活组。 + */ + virtual EGGA_AbilityActivationGroup GetActivationGroup() const override { return ActivationGroup; } + + /** + * Sets the activation group for this ability. + * 设置此技能的激活组。 + * @param NewGroup The new activation group. 新激活组。 + */ + virtual void SetActivationGroup(EGGA_AbilityActivationGroup NewGroup) override; + + /** + * Attempts to activate the ability on spawn. + * 尝试在生成时激活技能。 + * @param ActorInfo The actor info. Actor信息。 + * @param Spec The ability spec. 技能规格。 + */ + virtual void TryActivateAbilityOnSpawn(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) const override; + + /** + * Handles ability activation failure. + * 处理技能激活失败。 + * @param FailedReason The reason for failure. 失败原因。 + */ + virtual void HandleActivationFailed(const FGameplayTagContainer& FailedReason) const override; + + /** + * Checks if an effect container exists for a given tag. + * 检查是否存在指定标签的效果容器。 + * @param ContainerTag The container tag to check. 要检查的容器标签。 + * @return True if the container exists, false otherwise. 如果容器存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Ability|EffectContainer") + virtual bool HasEffectContainer(FGameplayTag ContainerTag); + + /** + * Creates an effect container spec from the effect container map. + * 从效果容器映射创建效果容器规格。 + * @param ContainerTag The container tag. 容器标签。 + * @param EventData The event data. 事件数据。 + * @param OverrideGameplayLevel Optional gameplay level override. 可选的游戏等级覆盖。 + * @return The effect container spec. 效果容器规格。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Ability|EffectContainer", meta = (AutoCreateRefTerm = "EventData")) + virtual FGGA_GameplayEffectContainerSpec MakeEffectContainerSpec(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1); + + /** + * Applies an effect container by creating and applying its spec. + * 通过创建并应用规格来应用效果容器。 + * @param ContainerTag The container tag. 容器标签。 + * @param EventData The event data. 事件数据。 + * @param OverrideGameplayLevel Optional gameplay level override. 可选的游戏等级覆盖。 + * @return Array of active gameplay effect handles. 激活的游戏效果句柄数组。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Ability|EffectContainer", meta = (AutoCreateRefTerm = "EventData")) + virtual TArray ApplyEffectContainer(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1); + +protected: + /** + * Prepares the ability for activation. + * 为技能激活做准备。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + * @param OnGameplayAbilityEndedDelegate Delegate for ability end. 技能结束委托。 + * @param TriggerEventData Optional trigger event data. 可选的触发事件数据。 + */ + virtual void PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData = nullptr) override; + + /** + * Ends the ability. + * 结束技能。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + * @param bReplicateEndAbility Whether to replicate ability end. 是否复制技能结束。 + * @param bWasCancelled Whether the ability was cancelled. 技能是否被取消。 + */ + virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, + bool bWasCancelled) override; + + /** + * Blueprint event for handling ability activation failure. + * 处理技能激活失败的蓝图事件。 + * @param FailedReason The reason for failure. 失败原因。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Ability") + void OnActivationFailed(const FGameplayTagContainer& FailedReason) const; + + /** + * Checks if the ability can be activated. + * 检查技能是否可以激活。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param SourceTags Optional source tags. 可选的源标签。 + * @param TargetTags Optional target tags. 可选的目标标签。 + * @param OptionalRelevantTags Optional relevant tags (output). 可选的相关标签(输出)。 + * @return True if the ability can be activated, false otherwise. 如果技能可激活则返回true,否则返回false。 + */ + virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, + const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const override; + + /** + * Sets whether the ability can be canceled. + * 设置技能是否可被取消。 + * @param bCanBeCanceled Whether the ability can be canceled. 技能是否可取消。 + */ + virtual void SetCanBeCanceled(bool bCanBeCanceled) override; + + /** + * Called when the ability is granted to the ability system component. + * 技能被授予技能系统组件时调用。 + * @param ActorInfo The actor info. Actor信息。 + * @param Spec The ability spec. 技能规格。 + */ + virtual void OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override; + + /** + * Blueprint event for when the ability is granted. + * 技能被授予时的蓝图事件。 + */ + UFUNCTION(BlueprintImplementableEvent, Category = "Ability", DisplayName = "On Give Ability") + void K2_OnGiveAbility(); + + /** + * Called when the ability is removed from the ability system component. + * 技能从技能系统组件移除时调用。 + * @param ActorInfo The actor info. Actor信息。 + * @param Spec The ability spec. 技能规格。 + */ + virtual void OnRemoveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override; + + /** + * Blueprint event for when the ability is removed. + * 技能移除时的蓝图事件。 + */ + UFUNCTION(BlueprintImplementableEvent, Category = "Ability", DisplayName = "On Remove Ability") + void K2_OnRemoveAbility(); + + /** + * Called when the avatar is set for the ability. + * 技能的化身设置时调用。 + * @param ActorInfo The actor info. Actor信息。 + * @param Spec The ability spec. 技能规格。 + */ + virtual void OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override; + + /** + * Blueprint event for when the avatar is set. + * 化身设置时的蓝图事件。 + */ + UFUNCTION(BlueprintImplementableEvent, Category = "Ability", DisplayName = "On Avatar Set") + void K2_OnAvatarSet(); + + /** + * Checks if the ability should activate for a given network role. + * 检查技能是否应为特定网络角色激活。 + * @param Role The network role. 网络角色。 + * @return True if the ability should activate, false otherwise. 如果技能应激活则返回true,否则返回false。 + */ + virtual bool ShouldActivateAbility(ENetRole Role) const override; + + /** + * Blueprint event for checking if the ability should activate for a network role. + * 检查技能是否应为网络角色激活的蓝图事件。 + * @param Role The network role. 网络角色。 + * @return True if the ability should activate, false otherwise. 如果技能应激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Ability", DisplayName = "Should Activate Ability") + bool K2_ShouldActivateAbility(ENetRole Role) const; + + /** + * Handles input press for the ability. + * 处理技能的输入按下。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + virtual void InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override; + + /** + * Blueprint event for handling input press. + * 处理输入按下的蓝图事件。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + UFUNCTION(BlueprintImplementableEvent, Category = "Ability|Input", meta=(DisplayName="On Input Pressed")) + void K2_OnInputPressed(const FGameplayAbilitySpecHandle Handle, FGameplayAbilityActorInfo ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo); + + /** + * Handles input release for the ability. + * 处理技能的输入释放。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + virtual void InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override; + + /** + * Blueprint event for handling input release. + * 处理输入释放的蓝图事件。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + UFUNCTION(BlueprintImplementableEvent, Category = "Ability|Input", meta=(DisplayName="On Input Released")) + void K2_OnInputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo); + + /** + * Checks if the ability cost can be paid. + * 检查是否能支付技能成本。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param OptionalRelevantTags Optional relevant tags (output). 可选的相关标签(输出)。 + * @return True if the cost can be paid, false otherwise. 如果成本可支付则返回true,否则返回false。 + */ + virtual bool CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const override; + + /** + * Blueprint event for checking the ability cost. + * 检查技能成本的蓝图事件。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @return True if the cost can be paid, false otherwise. 如果成本可支付则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Ability|Cost", meta=(DisplayName="On Check Cost")) + bool K2_OnCheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo) const; + virtual bool K2_OnCheckCost_Implementation(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo) const; + + /** + * Applies the ability cost. + * 应用技能成本。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + virtual void ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const override; + + /** + * Retrieves the gameplay effect for the ability cost. + * 获取技能成本的游戏效果。 + * @return The gameplay effect class. 游戏效果类。 + */ + virtual UGameplayEffect* GetCostGameplayEffect() const override; + + /** + * Blueprint event for retrieving the cost gameplay effect. + * 获取成本游戏效果的蓝图事件。 + * @return The gameplay effect class. 游戏效果类。 + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Ability|Cost", meta=(DisplayName="Get Cost Gameplay Effect")) + TSubclassOf K2_GetCostGameplayEffect() const; + + /** + * Blueprint event for applying additional ability costs. + * 应用额外技能成本的蓝图事件。 + * @param Handle The ability spec handle. 技能句柄。 + * @param ActorInfo The actor info. Actor信息。 + * @param ActivationInfo The activation info. 激活信息。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Ability|Cost", meta=(DisplayName="On Apply Cost")) + void K2_OnApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const; + virtual void K2_OnApplyCost_Implementation(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo& ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const; + + /** + * Applies ability tags to a gameplay effect spec. + * 将技能标签应用于游戏效果规格。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySpec The ability spec. 技能规格。 + */ + virtual void ApplyAbilityTagsToGameplayEffectSpec(FGameplayEffectSpec& Spec, FGameplayAbilitySpec* AbilitySpec) const override; + + /** + * Checks if the ability satisfies tag requirements. + * 检查技能是否满足标签要求。 + * @param AbilitySystemComponent The ability system component. 技能系统组件。 + * @param SourceTags Optional source tags. 可选的源标签。 + * @param TargetTags Optional target tags. 可选的目标标签。 + * @param OptionalRelevantTags Optional relevant tags (output). 可选的相关标签(输出)。 + * @return True if requirements are satisfied, false otherwise. 如果满足要求则返回true,否则返回false。 + */ + virtual bool DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, + FGameplayTagContainer* OptionalRelevantTags) const override; + +protected: + /** + * Defines the activation group for the ability. + * 定义技能的激活组。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ability") + EGGA_AbilityActivationGroup ActivationGroup; + + /** + * Additional costs required to activate the ability. + * 激活技能所需的额外成本。 + */ + UPROPERTY(EditDefaultsOnly, Instanced, Category = "Costs") + TArray> AdditionalCosts; + + /** + * Loose tags applied to the owner while the ability is active. + * 技能激活时应用于所有者的松散标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Tags") + TArray ActivationOwnedLooseTags; + + /** + * Map of gameplay effect containers, each associated with a tag. + * 游戏效果容器映射,每个容器与一个标签关联。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GameplayEffects", meta=(ForceInlineRow)) + TMap EffectContainerMap; + + /** + * Enables ticking for instanced-per-actor abilities when active. + * 为每演员实例化的技能启用Tick,仅在技能激活时有效。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Ability") + bool bEnableTick; + +#pragma region Net + +public: + /** + * Attempts to activate the ability with batched RPCs. + * 尝试使用批处理RPC激活技能。 + * @param InAbilityHandle The ability handle. 技能句柄。 + * @param EndAbilityImmediately Whether to end the ability immediately. 是否立即结束技能。 + * @return True if activation was successful, false otherwise. 如果激活成功则返回true,否则返回false。 + */ + virtual bool BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately) override; + + /** + * Ends the ability externally. + * 外部结束技能。 + */ + virtual void ExternalEndAbility() override; + + /** + * Sends target data to the server from the predicting client. + * 从预测客户端向服务器发送目标数据。 + * @note This call will create a prediction key. 此调用会创建一个预测Key。 + * @param TargetData The target data to send. 要发送的目标数据。 + */ + UFUNCTION(BlueprintCallable, Category = "Ability|Net") + virtual void SendTargetDataToServer(const FGameplayAbilityTargetDataHandle& TargetData); + +#pragma endregion + +#pragma region DataValidation +#if WITH_EDITOR + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; + + /** + * Pre-save processing for editor. + * 编辑器预保存处理。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbilityInterface.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbilityInterface.h new file mode 100644 index 0000000..81aab46 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Abilities/GGA_GameplayAbilityInterface.h @@ -0,0 +1,77 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemEnumLibrary.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "UObject/Interface.h" +#include "GGA_GameplayAbilityInterface.generated.h" + +/** + * Interface for gameplay abilities to integrate with GGA_AbilitySystemComponent. + * 与GGA_AbilitySystemComponent集成的游戏技能接口。 + */ +UINTERFACE(MinimalAPI, BlueprintType, meta=(CannotImplementInterfaceInBlueprint)) +class UGGA_GameplayAbilityInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Implementation class for gameplay ability interface. + * 游戏技能接口的实现类。 + */ +class GENERICGAMEPLAYABILITIES_API IGGA_GameplayAbilityInterface +{ + GENERATED_BODY() + +public: + /** + * Retrieves the activation group for the ability. + * 获取技能的激活组。 + * @return The activation group. 激活组。 + */ + UFUNCTION(BlueprintCallable, Category="Ability") + virtual EGGA_AbilityActivationGroup GetActivationGroup() const = 0; + + /** + * Sets the activation group for the ability. + * 设置技能的激活组。 + * @param NewGroup The new activation group. 新激活组。 + */ + UFUNCTION(BlueprintCallable, Category="Ability") + virtual void SetActivationGroup(EGGA_AbilityActivationGroup NewGroup) = 0; + + /** + * Attempts to activate the ability on spawn. + * 尝试在生成时激活技能。 + * @param ActorInfo The actor info. Actor信息。 + * @param Spec The ability spec. 技能规格。 + */ + virtual void TryActivateAbilityOnSpawn(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) const = 0; + + /** + * Handles ability activation failure. + * 处理技能激活失败。 + * @param FailureReason The reason for failure. 失败原因。 + */ + virtual void HandleActivationFailed(const FGameplayTagContainer& FailureReason) const = 0; + + /** + * Attempts to activate the ability with batched RPCs. + * 尝试使用批处理RPC激活技能。 + * @param InAbilityHandle The ability handle. 技能句柄。 + * @param EndAbilityImmediately Whether to end the ability immediately. 是否立即结束技能。 + * @return True if activation was successful, false otherwise. 如果激活成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "Ability|Net") + virtual bool BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately) = 0; + + /** + * Ends the ability externally. + * 外部结束技能。 + */ + UFUNCTION(BlueprintCallable, Category="Ability") + virtual void ExternalEndAbility() = 0; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.h new file mode 100644 index 0000000..f2a46a7 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_NetworkSyncPoint.h @@ -0,0 +1,46 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask_NetworkSyncPoint.h" +#include "GGA_AbilityTask_NetworkSyncPoint.generated.h" + + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGGA_SyncTimeOutDelegate); + + +/** + * + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_NetworkSyncPoint : public UAbilityTask_NetworkSyncPoint +{ + GENERATED_BODY() + + /** + * + * Synchronize execution flow between Client and Server. Depending on SyncType, the Client and Server will wait for the other to reach this node or another WaitNetSync node in the ability before continuing execution. + * + * BothWait - Both Client and Server will wait until the other reaches the node. (Whoever gets their first, waits for the other before continueing). + * OnlyServerWait - Only server will wait for the client signal. Client will signal and immediately continue without waiting to hear from Server. + * OnlyClientWait - Only client will wait for the server signal. Server will signal and immediately continue without waiting to hear from Client. + * + * Note that this is "ability instance wide". These sync points never affect sync points in other abilities. + * + * In most cases you will have both client and server execution paths connected to the same WaitNetSync node. However it is possible to use separate nodes + * for cleanliness of the graph. The "signal" is "ability instance wide". + * + */ + UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AbilityTask_NetworkSyncPoint* WaitNetSyncWithTimeout(UGameplayAbility* OwningAbility, EAbilityTaskNetSyncType SyncType, float WaitTime = 0.2f); + + virtual void Activate() override; + +protected: + void OnTimeFinish(); + + + float Time; + float TimeStarted; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.h new file mode 100644 index 0000000..84be468 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_PlayMontageAndWaitForEvent.h @@ -0,0 +1,223 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimInstance.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "GGA_AbilityTask_PlayMontageAndWaitForEvent.generated.h" + +USTRUCT(BlueprintType) +struct FGGA_PlayMontageAndWaitForEventTaskParams +{ + GENERATED_BODY() + + /** + * Set to override the name of this task, for later querying + * 为此任务设置重载名,以便以后查询 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + FName TaskInstanceName; + + /** + * The montage to play on the character + * 角色要播放的蒙太奇。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + TObjectPtr MontageToPlay = nullptr; + + /** + * Any gameplay events matching this tag will activate the EventReceived callback. If empty, all events will trigger callback + * 任何与此标签匹配的游戏事件都将激活 EventReceived 回调。如果为空,所有事件都将触发回调 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + FGameplayTagContainer EventTags; + + /** + * Change to play the montage faster or slower + * 更改蒙太奇的播放速度快慢 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + float Rate = 1.f; + + /** + * If not empty, named montage section to start from + * 如果不为空,则从命名的蒙太奇部分开始播放 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + FName StartSection = NAME_None; + + /** + * If true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled + * 如果为 "true",那么Ability正常结束时,蒙太奇会中止。当该Ability被明确取消时,蒙太奇总是会停止。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + bool bStopWhenAbilityEnds = true; + + /** + * Change to modify size of root motion or set to 0 to block it entirely + * 更改以修改根运动的大小,或设置为 0 以完全阻止它 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + float AnimRootMotionTranslationScale = 1.f; + + /** + * Starting time offset in montage, this will be overridden by StartSection if that is also set + * 蒙太奇中的起始时间偏移,如果也设置了 StartSection,则会被 StartSection 覆盖 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + float StartTimeSeconds = 0.f; + + /** + * If true, you can receive OnInterrupted after an OnBlendOut started (otherwise OnInterrupted will not fire when interrupted, but you will not get OnComplete). + * 如果为 "true",则可以在 OnBlendOut 开始后接收 OnInterrupted(否则,被中断时不会触发 OnInterrupted,但也不会收到 OnComplete)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + bool bAllowInterruptAfterBlendOut = false; +}; + +/** + * This task combines PlayMontageAndWait and WaitForEvent into one task, so you can wait for multiple types of activations such as from a melee combo + * Much of this code is copied from one of those two ability tasks + * This is a good task to look at as an example when creating game-specific tasks + * It is expected that each game will have a set of game-specific tasks to do what they want + * 此任务将 PlayMontageAndWait 和 WaitForEvent 合并为一个任务,因此您可以等待多种类型的激活,例如来自近战连击的激活。 + * 本任务的大部分代码都是从这两个能力任务中的一个复制过来的 + * 在创建特定游戏任务时,这是一个很好的任务示例 + * 预计每个游戏都会有一套特定于游戏的任务来完成他们想要做的事情 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_PlayMontageAndWaitForEvent : public UAbilityTask +{ + GENERATED_BODY() + +public: + // Constructor and overrides + UGGA_AbilityTask_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer); + + /** Delegate type used, EventTag and Payload may be empty if it came from the montage callbacks */ + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPlayMontageAndWaitForEventDelegate, FGameplayTag, EventTag, FGameplayEventData, EventData); + + /** The montage completely finished playing */ + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate OnCompleted; + + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate OnBlendedIn; + + /** The montage started blending out */ + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate OnBlendOut; + + /** The montage was interrupted */ + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate OnInterrupted; + + /** The ability task was explicitly cancelled by another ability */ + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate OnCancelled; + + /** One of the triggering gameplay events happened */ + UPROPERTY(BlueprintAssignable) + FPlayMontageAndWaitForEventDelegate EventReceived; + + UFUNCTION() + void OnMontageBlendedIn(UAnimMontage* Montage); + + UFUNCTION() + void OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted); + + /** Callback function for when the owning Gameplay Ability is cancelled */ + UFUNCTION() + void OnGameplayAbilityCancelled(); + + void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted); + + void OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload); + + /** + * See PlayMontageAndWaitForEventExt for details。 + * 查看 PlayMontageAndWaitForEventExt 以获得更多注释。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AbilityTask_PlayMontageAndWaitForEvent* PlayMontageAndWaitForEvent( + UGameplayAbility* OwningAbility, + FName TaskInstanceName, + UAnimMontage* MontageToPlay, + FGameplayTagContainer EventTags, + float Rate = 1.f, + FName StartSection = NAME_None, + bool bStopWhenAbilityEnds = true, + float AnimRootMotionTranslationScale = 1.f, + float StartTimeSeconds = 0.f, + bool bAllowInterruptAfterBlendOut = false); + + /** + * Play a montage and wait for it end. If a gameplay event happens that matches EventTags (or EventTags is empty), the EventReceived delegate will fire with a tag and event data. + * If StopWhenAbilityEnds is true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled. + * On normal execution, OnBlendOut is called when the montage is blending out, and OnCompleted when it is completely done playing + * OnInterrupted is called if another montage overwrites this, and OnCancelled is called if the ability or task is cancelled + * 播放蒙太奇并等待结束。如果发生了符合 EventTags 的游戏事件(或 EventTags 为空),EventReceived 委托就会触发,并带有标签和事件数据。 + * 如果 StopWhenAbilityEnds 为 true,那么如果能力正常结束,蒙太奇将会中止。当能力被明确取消时,蒙太奇始终会停止。 + * 在正常执行时,蒙太奇混合结束时会调用 OnBlendOut,完全结束时会调用 OnCompleted。 + * 如果其他蒙太奇覆盖了该功能,则调用 OnInterrupted,如果取消了该功能或任务,则调用 OnCancelled。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AbilityTask_PlayMontageAndWaitForEvent* PlayMontageAndWaitForEventExt(UGameplayAbility* OwningAbility, FGGA_PlayMontageAndWaitForEventTaskParams Params); + + /** + * The Blueprint node for this task, PlayMontageAndWaitForEvent, has some black magic from the plugin that automagically calls Activate() + * inside of K2Node_LatentAbilityCall as stated in the AbilityTask.h. Ability logic written in C++ probably needs to call Activate() itself manually. + */ + virtual void Activate() override; + + /** Called when the ability is asked to cancel from an outside node. What this means depends on the individual task. By default, this does nothing other than ending the task. */ + virtual void ExternalCancel() override; + + virtual FString GetDebugString() const override; + + UFUNCTION(BlueprintCallable, Category="GGA|Tasks") + void EndTaskByOwner(); + +protected: + virtual void OnDestroy(bool AbilityEnded) override; + + /** Checks if the ability is playing a montage and stops that montage, returns true if a montage was stopped, false if not. */ + bool StopPlayingMontage(); + + FOnMontageBlendedInEnded BlendedInDelegate; + FOnMontageBlendingOutStarted BlendingOutDelegate; + FOnMontageEnded MontageEndedDelegate; + FDelegateHandle InterruptedHandle; + FDelegateHandle EventHandle; + + /** Montage that is playing */ + UPROPERTY() + TObjectPtr MontageToPlay; + + /** List of tags to match against gameplay events */ + UPROPERTY() + FGameplayTagContainer EventTags; + + /** Playback rate */ + UPROPERTY() + float Rate; + + /** Section to start montage from */ + UPROPERTY() + FName StartSection; + + /** Modifies how root motion movement to apply */ + UPROPERTY() + float AnimRootMotionTranslationScale; + + UPROPERTY() + float StartTimeSeconds; + + /** Rather montage should be aborted if ability ends */ + UPROPERTY() + bool bStopWhenAbilityEnds; + + UPROPERTY() + bool bAllowInterruptAfterBlendOut; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.h new file mode 100644 index 0000000..3f59f2c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_RunCustomAbilityTask.h @@ -0,0 +1,48 @@ +//// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +//#pragma once +// +//#include "CoreMinimal.h" +//#include "Abilities/Tasks/AbilityTask.h" +//#include "Runtime/Launch/Resources/Version.h" +//#if ENGINE_MINOR_VERSION < 5 +//#include "InstancedStruct.h" +//#else +//#include "StructUtils/InstancedStruct.h" +//#endif +//#include "GGA_AbilityTask_RunCustomAbilityTask.generated.h" +// +// +//class UGGA_CustomAbilityTask; +//DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGGA_CustomAbilityTaskDelegate, const FGameplayTag, EventTag, const FInstancedStruct&, Payload); +// +///** +// * A special ability task runs GGA_CustomAbilityTask internally. +// */ +//UCLASS() +//class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_RunCustomAbilityTask : public UAbilityTask +//{ +// GENERATED_BODY() +// +//public: +// UGGA_AbilityTask_RunCustomAbilityTask(); +// +// UPROPERTY(BlueprintAssignable) +// FGGA_CustomAbilityTaskDelegate OnEvent; +// +// virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; +// +// // Like WaitDelay but only delays one frame (tick). +// UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) +// static UGGA_AbilityTask_RunCustomAbilityTask* RunCustomAbilityTask(UGameplayAbility* OwningAbility, TSoftClassPtr AbilityTaskClass); +// +// virtual void Activate() override; +// virtual void TickTask(float DeltaTime) override; +// virtual void OnDestroy(bool bInOwnerFinished) override; +// +// virtual void InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent) override; +// +//private: +// UPROPERTY(Replicated) +// TObjectPtr TaskInstance{nullptr}; +//}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.h new file mode 100644 index 0000000..7ffb740 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_ServerWaitForClientTargetData.h @@ -0,0 +1,37 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "Abilities/Tasks/AbilityTask_WaitTargetData.h" +#include "GGA_AbilityTask_ServerWaitForClientTargetData.generated.h" + + +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_ServerWaitForClientTargetData : public UAbilityTask +{ + + GENERATED_UCLASS_BODY() + UPROPERTY(BlueprintAssignable) + FWaitTargetDataDelegate ValidData; + + /** + * The server side waits for target data from the client. + * Client execution of this node will return directly from ActivatePin and continue execution + * 服务端等待客户端的目标数据。 + * 客户端执行这个节点会直接从ActivatePin返回并继续执行 + */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true", HideSpawnParms = "Instigator"), Category = "GGA|Tasks") + static UGGA_AbilityTask_ServerWaitForClientTargetData* ServerWaitForClientTargetData(UGameplayAbility* OwningAbility, FName TaskInstanceName, bool TriggerOnce); + + virtual void Activate() override; + + UFUNCTION() + void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& Data, FGameplayTag ActivationTag); + +protected: + virtual void OnDestroy(bool AbilityEnded) override; + + bool bTriggerOnce; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.h new file mode 100644 index 0000000..edb8eae --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitDelayOneFrame.h @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "GGA_AbilityTask_WaitDelayOneFrame.generated.h" + + +/** + * Like WaitDelay but only delays one frame (tick). + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_WaitDelayOneFrame : public UAbilityTask +{ + GENERATED_UCLASS_BODY() + + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FWaitDelayOneFrameDelegate); + UPROPERTY(BlueprintAssignable) + FWaitDelayOneFrameDelegate OnFinish; + + virtual void Activate() override; + + // Like WaitDelay but only delays one frame (tick). + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AbilityTask_WaitDelayOneFrame* WaitDelayOneFrame(UGameplayAbility* OwningAbility); + +private: + void OnDelayFinish(); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.h new file mode 100644 index 0000000..65de1a0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitGameplayEvents.h @@ -0,0 +1,52 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "GameplayTagContainer.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "GGA_AbilityTask_WaitGameplayEvents.generated.h" + +class UAbilitySystemComponent; + + +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_WaitGameplayEvents : public UAbilityTask +{ + GENERATED_UCLASS_BODY() + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FWaitGameplayEventsDelegate, FGameplayTag, EventTag, FGameplayEventData, EventData); + + UPROPERTY(BlueprintAssignable) + FWaitGameplayEventsDelegate EventReceived; + + /** + * Wait until the specified gameplay tags event is triggered. By default this will look at the owner of this ability. OptionalExternalTarget can be set to make this look at another actor's tags for changes + * It will keep listening as long as OnlyTriggerOnce = false + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AbilityTask_WaitGameplayEvents* WaitGameplayEvents(UGameplayAbility* OwningAbility, FGameplayTagContainer EventTags, AActor* OptionalExternalTarget = nullptr, + bool OnlyTriggerOnce = false); + + void SetExternalTarget(AActor* Actor); + + UAbilitySystemComponent* GetTargetASC(); + + virtual void Activate() override; + + virtual void GameplayEventContainerCallback(FGameplayTag MatchingTag, const FGameplayEventData* Payload); + + void OnDestroy(bool AbilityEnding) override; + + /** List of tags to match against gameplay events */ + UPROPERTY() + FGameplayTagContainer EventTags; + + UPROPERTY() + TObjectPtr OptionalExternalTarget; + + bool UseExternalTarget; + bool OnlyTriggerOnce; + + FDelegateHandle MyHandle; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.h new file mode 100644 index 0000000..85601af --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitInputPressWithTags.h @@ -0,0 +1,68 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "GGA_AbilityTask_WaitInputPressWithTags.generated.h" + + +/** + * Waits until the input is pressed from activating an ability and the ASC has the required tags and not the ignored tags. + * This should be true immediately upon starting the ability, since the key was pressed to activate it. We expect server to + * execute this task in parallel and keep its own time. + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_WaitInputPressWithTags : public UAbilityTask +{ + GENERATED_UCLASS_BODY() + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInputPressWithTagsDelegate, float, TimeWaited); + + UPROPERTY(BlueprintAssignable) + FInputPressWithTagsDelegate OnPress; + + virtual void Activate() override; + + UFUNCTION() + void OnPressCallback(); + + /** + * Wait until the user presses the input button for this ability's activation. Returns time this node spent waiting for the press. Will return 0 if input was already down. + * This is hardcoded for GA_InteractPassive to not fire when State.Interacting TagCount is > State.InteractingRemoval TagCount. + * //TODO Ideally the RequiredTags, IgnoredTags, and State.Interacting TagCount would get moved into a subclass of FGameplayTagQuery and then we'd only expose that as one + * parameter and rename the task to WaitInputPress_Query. + * + * @param RequiredTags Ability Owner must have all of these tags otherwise the input is ignored. + * @param IgnoredTags Ability Owner cannot have any of these tags otherwise the input is ignored. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", + meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE", DeprecatedFunction, DeprecationMessage="Use WaitInputPressWithTagQuery")) + static UGGA_AbilityTask_WaitInputPressWithTags* WaitInputPressWithTags(UGameplayAbility* OwningAbility, FGameplayTagContainer RequiredTags, FGameplayTagContainer IgnoredTags, + bool bTestAlreadyPressed = false); + + /** + * Wait until the user presses the input button for this ability's activation. Returns time this node spent waiting for the press. Will return 0 if input was already down. + * @param OwningAbility The ability owning this task. + * @param TagQuery Ability Owner must match this tag query otherwise the input is ignored. + * @param bTestAlreadyPressed Test if already pressed. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", + meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE", DeprecatedFunction, DeprecationMessage="Use WaitInputPressWithQuery")) + static UGGA_AbilityTask_WaitInputPressWithTags* WaitInputPressWithTagQuery(UGameplayAbility* OwningAbility, const FGameplayTagQuery& TagQuery, bool bTestAlreadyPressed = false); + +protected: + float StartTime; + bool bTestInitialState; + FDelegateHandle DelegateHandle; + + FGameplayTagQuery TagQuery; + + virtual void OnDestroy(bool AbilityEnded) override; + + /** + * We can only listen for one input pressed event. I think it's because + * UAbilitySystemComponent::InvokeReplicatedEvent sets ReplicatedData->GenericEvents[(uint8)EventType].bTriggered = true; + * So if we want to keep listening for more input events, we just clear the delegate handle and bind again. + */ + virtual void Reset(); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.h new file mode 100644 index 0000000..f0bb3d0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AbilityTasks/GGA_AbilityTask_WaitTargetDataUsingActor.h @@ -0,0 +1,86 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTargetActor.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "GGA_AbilityTask_WaitTargetDataUsingActor.generated.h" + + +/** + * 从一个已经生成的TargetActor中等待TargetData,当接收到有效数据后,并不销毁这个TargetActor。 + * 是原版本的WaitTargetData的重写,并添加了bCreateKeyIfNotValidForMorePredicting的功能。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTask_WaitTargetDataUsingActor : public UAbilityTask +{ + GENERATED_BODY() + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWaitTargetDataUsingActorDelegate, const FGameplayAbilityTargetDataHandle&,Data); + + UPROPERTY(BlueprintAssignable) + FWaitTargetDataUsingActorDelegate ValidData; + + UPROPERTY(BlueprintAssignable) + FWaitTargetDataUsingActorDelegate Cancelled; + + /** 传入一个已经生成的TargetActor并等待返回有效数据或者取消,这个TargetActor在使用后不会被销毁。 + * + * @param bCreateKeyIfNotValidForMorePredicting Will create a new scoped prediction key if the current scoped prediction key is not valid for more predicting. + * If false, it will always create a new scoped prediction key. We would want to set this to true if we want to use a potentially existing valid scoped prediction + * key like the ability's activation key in a batched ability. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta=(HidePin = "OwningAbility", DefaultToSelf = + "OwningAbility", + BlueprintInternalUseOnly= + "true", HideSpawnParms="Instigator")) + static UGGA_AbilityTask_WaitTargetDataUsingActor* WaitTargetDataWithReusableActor( + UGameplayAbility* OwningAbility, FName TaskInstanceName, + TEnumAsByte ConfirmationType, + AGameplayAbilityTargetActor* InTargetActor, bool bCreateKeyIfNotValidForMorePrediction = false); + + virtual void Activate() override; + + /** server处理TargetDataSet事件 */ + UFUNCTION() + virtual void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& Data, + FGameplayTag ActivationTag); + /** server处理TargetDataCancelled事件 */ + UFUNCTION() + virtual void OnTargetDataReplicatedCancelledCallback(); + + /** 玩家Confirm目标后触发(TargetActor->ConfirmTargeting) */ + UFUNCTION() + virtual void OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data); + + /** 玩家Cancel目标后触发(TargetActor->CancelTargeting) */ + UFUNCTION() + virtual void OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data); + + // Called when the ability is asked to confirm from an outside node. What this means depends on the individual task. By default, this does nothing other than ending if bEndTask is true. + virtual void ExternalConfirm(bool bEndTask) override; + + // Called when the ability is asked to cancel from an outside node. What this means depends on the individual task. By default, this does nothing other than ending the task. + virtual void ExternalCancel() override; + +protected: + UPROPERTY() + TObjectPtr TargetActor; + + bool bCreateKeyIfNotValidForMorePrediction; + + TEnumAsByte ConfirmationType; + + virtual void InitializeTargetActor() const; + virtual void RegisterTargetDataCallbacks(); + virtual void FinalizeTargetActor() const; + + void OnDestroy(bool AbilityEnded) override; + + /** + * 如果是客户端且传入的TargetActor不能在服务端产生TargetData则返回真 + */ + virtual bool ShouldReplicateDataToServer() const; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_AttributeChanged.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_AttributeChanged.h new file mode 100644 index 0000000..d9b94d2 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_AttributeChanged.h @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemComponent.h" +#include "Abilities/Async/AbilityAsync.h" +#include "GGA_AsyncTask_AttributeChanged.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue); + +/** + * 蓝图节点,用于监听AbilitySystemComponent上的属性变化。 + * 一般用于UI。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AsyncTask_AttributeChanged : public UAbilityAsync +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FOnAttributeChanged OnAttributeChanged; + + // Listens for an attribute changing. + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (BlueprintInternalUseOnly = "true")) + static UGGA_AsyncTask_AttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute); + + // Listens for an attribute changing. + // Version that takes in an array of Attributes. Check the Attribute output for which Attribute changed. + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (BlueprintInternalUseOnly = "true")) + static UGGA_AsyncTask_AttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray Attributes); + + // You must call this function manually when you want the AsyncTask to end. + // For UMG Widgets, you would call it in the Widget's Destruct event. + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta=(DeprecatedFunction, DeprecationMessage="Use EndAction")) + void EndTask(); + +protected: + virtual void Activate() override; + virtual void EndAction() override; + + FGameplayAttribute AttributeToListenFor; + TArray AttributesToListenFor; + + void AttributeChanged(const FOnAttributeChangeData& Data); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.h new file mode 100644 index 0000000..4fb5b1a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_GameplayTagAddedRemoved.h @@ -0,0 +1,46 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemComponent.h" +#include "GameplayTagContainer.h" +#include "Abilities/Async/AbilityAsync.h" +#include "GGA_AsyncTask_GameplayTagAddedRemoved.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameplayTagAddedRemoved, FGameplayTag, Tag); + +/** + * 蓝图节点,用于监听AbilitySystemComponent上的标记添加/移除变化。 + * 一般用于蓝图/UMG + */ +UCLASS(BlueprintType, meta = (ExposedAsyncProxy = AsyncTask)) +class GENERICGAMEPLAYABILITIES_API UGGA_AsyncTask_GameplayTagAddedRemoved : public UAbilityAsync +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FOnGameplayTagAddedRemoved OnTagAdded; + + UPROPERTY(BlueprintAssignable) + FOnGameplayTagAddedRemoved OnTagRemoved; + + // Listens for FGameplayTags added and removed. + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (BlueprintInternalUseOnly = "true")) + static UGGA_AsyncTask_GameplayTagAddedRemoved* ListenForGameplayTagAddedOrRemoved(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer Tags); + + /** + * You must call this function manually when you want the AsyncTask to end. For UMG Widgets, you would call it in the Widget's Destruct event. + * 要结束 AsyncTask 时,必须手动调用该函数。对于 UMG Widget,您可以在 Widget 的 Destruct 事件中调用该函数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta=(DeprecatedFunction, DeprecationMessage="Use EndAction")) + void EndTask(); + +protected: + virtual void EndAction() override; + + FGameplayTagContainer Tags; + + virtual void TagChanged(const FGameplayTag Tag, int32 NewCount); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.h new file mode 100644 index 0000000..fa26416 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityActivated.h @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Async/AbilityAsync.h" +#include "GGA_AsyncTask_WaitGameplayAbilityActivated.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGGA_AbilityActivatedDelegate, const UGameplayAbility*, Ability); + +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AsyncTask_WaitGameplayAbilityActivated : public UAbilityAsync +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (DefaultToSelf = "TargetActor", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AsyncTask_WaitGameplayAbilityActivated* WaitGameplayAbilityActivated(AActor* TargetActor); + + void HandleAbilityActivated(UGameplayAbility* Ability); + + UPROPERTY(BlueprintAssignable) + FGGA_AbilityActivatedDelegate OnAbilityActivated; + +protected: + virtual bool ShouldBroadcastDelegates() const override; + virtual void Activate() override; + virtual void EndAction() override; + + FDelegateHandle DelegateHandle; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.h new file mode 100644 index 0000000..de5f0f1 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/AsyncTasks/GGA_AsyncTask_WaitGameplayAbilityEnded.h @@ -0,0 +1,39 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Async/AbilityAsync.h" +#include "GGA_AsyncTask_WaitGameplayAbilityEnded.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGGA_AbilityEndedDelegate, const FAbilityEndedData&, AbilityEndedData); + +/** + * Given an Actor, OnAbilityEnded is triggered whenever the AbilityQuery's skill end is satisfied. + * 给定一个Actor,只要满足AbilityQuery的技能结束,就会触发OnAbilityEnded。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AsyncTask_WaitGameplayAbilityEnded : public UAbilityAsync +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (DefaultToSelf = "TargetActor", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AsyncTask_WaitGameplayAbilityEnded* WaitGameplayAbilityEnded(AActor* TargetActor, FGameplayTagQuery AbilityQuery); + + UFUNCTION(BlueprintCallable, Category = "GGA|Tasks", meta = (DefaultToSelf = "TargetActor", BlueprintInternalUseOnly = "TRUE")) + static UGGA_AsyncTask_WaitGameplayAbilityEnded* WaitAbilitySpecHandleEnded(AActor* TargetActor, FGameplayAbilitySpecHandle AbilitySpecHandle); + + void HandleAbilityEnded(const FAbilityEndedData& Data); + + UPROPERTY(BlueprintAssignable) + FGGA_AbilityEndedDelegate OnAbilityEnded; + +protected: + virtual void Activate() override; + virtual void EndAction() override; + + FGameplayTagQuery AbilityQuery; + FGameplayAbilitySpecHandle AbilitySpecHandle; + FDelegateHandle DelegateHandle; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Attributes/GGA_AttributeSet.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Attributes/GGA_AttributeSet.h new file mode 100644 index 0000000..7440d69 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Attributes/GGA_AttributeSet.h @@ -0,0 +1,65 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "GGA_AttributeSet.generated.h" + + +class UGGA_AbilitySystemComponent; +struct FGameplayEffectSpec; + +/** + * This macro defines a set of helper functions for accessing and initializing attributes. + * + * The following example of the macro: + * ATTRIBUTE_ACCESSORS(ULyraHealthSet, Health) + * will create the following functions: + * static FGameplayAttribute GetHealthAttribute(); + * float GetHealth() const; + * void SetHealth(float NewVal); + * void InitHealth(float NewVal); + */ +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +/** + * Delegate used to broadcast attribute events, some of these parameters may be null on clients: + * @param EffectInstigator The original instigating actor for this event + * @param EffectCauser The physical actor that caused the change + * @param EffectSpec The full effect spec for this change + * @param EffectMagnitude The raw magnitude, this is before clamping + * @param OldValue The value of the attribute before it was changed + * @param NewValue The value after it was changed +*/ +DECLARE_MULTICAST_DELEGATE_SixParams(FGGA_AttributeEvent, AActor* /*EffectInstigator*/, AActor* /*EffectCauser*/, const FGameplayEffectSpec* /*EffectSpec*/, float /*EffectMagnitude*/, + float /*OldValue*/, float /*NewValue*/); + + +// typedef is specific to the FGameplayAttribute() signature, but TStaticFunPtr is generic to any signature chosen +//typedef TBaseStaticDelegateInstance::FFuncPtr FAttributeFuncPtr; +template +using TStaticFuncPtr = typename TBaseStaticDelegateInstance::FFuncPtr; + +/** + * UGGA_AttributeSet + * + * Base attribute set class for the project. + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AttributeSet : public UAttributeSet +{ + GENERATED_BODY() + +public: + UGGA_AttributeSet(); + + UWorld* GetWorld() const override; + + UGGA_AbilitySystemComponent* GetGGA_AbilitySystemComponent() const; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemComponent.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemComponent.h new file mode 100644 index 0000000..fd3403a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemComponent.h @@ -0,0 +1,498 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "AbilitySystemComponent.h" +#include "GGA_AbilitySet.h" +#include "GGA_AbilitySystemEnumLibrary.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "GGA_AbilitySystemComponent.generated.h" + +class UGGA_AbilityTagRelationshipMapping; +class UGGA_GameplayAbility; +class UGGA_AbilitySet; + +/** + * Delegate for when the ability system is initialized. + * 技能系统初始化时的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGGA_AbilitySystemInitializedSignature); + +/** + * Delegate for when the ability system is uninitialized. + * 技能系统取消初始化时的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGGA_AbilitySystemUninitializedSignature); + +/** + * Delegate for when an ability is activated. + * 技能激活时的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGGA_OnAbilityActivatedSignature, const FGameplayAbilitySpecHandle, Handle, const UGameplayAbility*, Ability); + +/** + * Delegate for when an ability ends. + * 技能结束时的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGGA_OnAbilityEndedSignature, FGameplayAbilitySpecHandle, Handle, UGameplayAbility*, Ability, bool, bWasCancelled); + +/** + * Delegate for when an ability fails to activate. + * 技能激活失败时的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGGA_OnAbilityFailedSignature, const UGameplayAbility*, Ability, const FGameplayTagContainer&, ReasonTags); + +/** + * Extended ability system component with custom functionality. + * 扩展的技能系统组件,包含自定义功能。 + */ +UCLASS(ClassGroup=GGA, meta=(BlueprintSpawnableComponent)) +class GENERICGAMEPLAYABILITIES_API UGGA_AbilitySystemComponent : public UAbilitySystemComponent +{ + GENERATED_BODY() + +#pragma region Common + +public: + /** + * Constructor for the ability system component. + * 技能系统组件构造函数。 + */ + UGGA_AbilitySystemComponent(const FObjectInitializer& ObjectInitializer); + + /** + * Initializes the ability system with owner and avatar actors. + * 使用拥有者和化身演员初始化技能系统。 + * @param InOwnerActor The owner actor. 拥有者演员。 + * @param InAvatarActor The avatar actor. 化身演员。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Abilities") + virtual void InitializeAbilitySystem(AActor* InOwnerActor, AActor* InAvatarActor); + + /** + * Uninitializes the ability system. + * 取消初始化技能系统。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Abilities") + virtual void UninitializeAbilitySystem(); + + /** + * Initializes actor info for the ability system. + * 初始化技能系统的Actor信息。 + * @param InOwnerActor The owner actor. 拥有者演员。 + * @param InAvatarActor The avatar actor. 化身演员。 + */ + virtual void InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor) override; + + /** + * Delegate triggered when the ability system is initialized. + * 技能系统初始化时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGGA_AbilitySystemInitializedSignature OnAbilitySystemInitialized; + + /** + * Delegate triggered when the ability system is uninitialized. + * 技能系统取消初始化时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGGA_AbilitySystemUninitializedSignature OnAbilitySystemUninitialized; + + /** + * Called when the component ends play. + * 组件结束播放时调用。 + * @param EndPlayReason The reason for ending play. 结束播放的原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Initializes ability sets for the ability system. + * 为技能系统初始化技能集。 + * @param InOwnerActor The owner actor. 拥有者演员。 + * @param InAvatarActor The avatar actor. 化身演员。 + */ + virtual void InitializeAbilitySets(AActor* InOwnerActor, AActor* InAvatarActor); + + /** + * Initializes attributes for the ability system. + * 为技能系统初始化属性。 + * @param GroupName The attribute group name. 属性组名称。 + * @param Level The level to initialize. 初始化等级。 + * @param bInitialInit Whether this is the initial initialization. 是否为初始初始化。 + */ + virtual void InitializeAttributes(FGGA_AttributeGroupName GroupName, int32 Level, bool bInitialInit); + + /** + * Sends a replicated gameplay event to an actor. + * 向演员发送复制的游戏事件。 + * @param Actor The target actor. 目标演员。 + * @param EventTag The event tag. 事件标签。 + * @param Payload The event data payload. 事件数据负载。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Abilities", meta=(DisplayName="Send Gameplay Event To Actor (Replicated)")) + void SendGameplayEventToActor_Replicated(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload); + +private: + /** + * Server RPC for sending gameplay events. + * 发送游戏事件的服务器RPC。 + * @param Actor The target actor. 目标演员。 + * @param EventTag The event tag. 事件标签。 + * @param Payload The event data payload. 事件数据负载。 + */ + UFUNCTION(Server, Reliable, WithValidation, Category = "GGA|Abilities") + void ServerSendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload); + + /** + * Multicast RPC for sending gameplay events. + * 发送游戏事件的多播RPC。 + * @param Actor The target actor. 目标演员。 + * @param EventTag The event tag. 事件标签。 + * @param Payload The event data payload. 事件数据负载。 + */ + UFUNCTION(NetMulticast, Reliable, Category = "GGA|Abilities") + void MulticastSendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload); + +protected: + /** + * Post-initialization property setup. + * 初始化后属性设置。 + */ + virtual void PostInitProperties() override; + + /** + * Registers the component with the global ability system. + * 将组件注册到全局技能系统。 + */ + void RegisterToGlobalAbilitySystem(); + + /** + * Unregisters the component from the global ability system. + * 从全局技能系统取消注册组件。 + */ + void UnregisterToGlobalAbilitySystem(); + + /** + * Default ability sets to grant on initialization. + * 初始化时赋予的默认技能集。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|Abilities") + TArray> DefaultAbilitySets; + + /** + * Attribute group name for initializing gameplay attributes. + * 用于初始化游戏属性的属性组名称。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|Attributes") + FGGA_AttributeGroupName AttributeSetInitializeGroupName; + + /** + * Level for initializing attributes. + * 初始化属性的等级。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|Attributes", meta=(ClampMin=1)) + int32 AttributeSetInitializeLevel{1}; + +private: + /** + * Tracks if the ability system is initialized. + * 跟踪技能系统是否已初始化。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "GGA|Abilities") + bool bAbilitySystemInitialized{false}; + + /** + * Handles for granted default ability sets. + * 已赋予默认技能集的句柄。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "GGA|Abilities") + TArray DefaultAbilitySet_GrantedHandles; + + /** + * Tracks if the component is registered with the global ability system. + * 跟踪组件是否注册到全局技能系统。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "GGA|Abilities") + bool bRegisteredToGlobalAbilitySystem{false}; + +#pragma endregion + +#pragma region AbilitiesActivation + +public: + /** + * Checks if an activation group is blocked. + * 检查激活组是否被阻挡。 + * @param Group The activation group to check. 要检查的激活组。 + * @return True if blocked, false otherwise. 如果被阻挡则返回true,否则返回false。 + */ + bool IsActivationGroupBlocked(EGGA_AbilityActivationGroup Group) const; + + /** + * Adds an ability to an activation group. + * 将技能添加到激活组。 + * @param Group The activation group. 激活组。 + * @param Ability The ability to add. 要添加的技能。 + */ + void AddAbilityToActivationGroup(EGGA_AbilityActivationGroup Group, UGameplayAbility* Ability); + + /** + * Removes an ability from an activation group. + * 从激活组移除技能。 + * @param Group The activation group. 激活组。 + * @param Ability The ability to remove. 要移除的技能。 + */ + void RemoveAbilityFromActivationGroup(EGGA_AbilityActivationGroup Group, UGameplayAbility* Ability); + + /** + * Checks if an ability can change its activation group. + * 检查技能是否可以更改激活组。 + * @param NewGroup The new activation group. 新激活组。 + * @param Ability The ability to check. 要检查的技能。 + * @return True if the change is allowed, false otherwise. 如果允许更改则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|Ability", Meta = (ExpandBoolAsExecs = "ReturnValue")) + bool CanChangeActivationGroup(EGGA_AbilityActivationGroup NewGroup, UGameplayAbility* Ability) const; + + /** + * Changes the activation group of an ability. + * 更改技能的激活组。 + * @param NewGroup The new activation group. 新激活组。 + * @param Ability The ability to change. 要更改的技能。 + * @return True if the change was successful, false otherwise. 如果更改成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|Ability", Meta = (ExpandBoolAsExecs = "ReturnValue")) + bool ChangeActivationGroup(EGGA_AbilityActivationGroup NewGroup, UGameplayAbility* Ability); + + /** + * Delegate triggered when an ability is activated. + * 技能激活时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGGA_OnAbilityActivatedSignature OnAbilityActivated; + + /** + * Delegate triggered when an ability fails to activate. + * 技能激活失败时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGGA_OnAbilityFailedSignature OnAbilityActivationFailed; + +protected: + /** + * Notifies when an ability is activated. + * 技能激活时通知。 + * @param Handle The ability spec handle. 技能规格句柄。 + * @param Ability The activated ability. 激活的技能。 + */ + virtual void NotifyAbilityActivated(const FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability) override; + + /** + * Notifies when an ability fails to activate. + * 技能激活失败时通知。 + * @param Handle The ability spec handle. 技能规格句柄。 + * @param Ability The ability that failed. 失败的技能。 + * @param FailureReason The reason for failure. 失败原因。 + */ + virtual void NotifyAbilityFailed(const FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason) override; + + /** + * Client RPC for notifying ability activation failure. + * 通知技能激活失败的客户端RPC。 + * @param Ability The ability that failed. 失败的技能。 + * @param FailureReason The reason for failure. 失败原因。 + */ + UFUNCTION(Client, Unreliable) + void ClientNotifyAbilityActivationFailed(const UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason); + + /** + * Implementation of client notification for ability activation failure. + * 技能激活失败客户端通知的实现。 + */ + void ClientNotifyAbilityActivationFailed_Implementation(const UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason); + + /** + * Handles ability activation failure. + * 处理技能激活失败。 + * @param Ability The ability that failed. 失败的技能。 + * @param FailureReason The reason for failure. 失败原因。 + */ + virtual void HandleAbilityActivationFailed(const UGameplayAbility* Ability, const FGameplayTagContainer& FailureReason); + + /** + * Tracks the number of abilities in each activation group. + * 跟踪每个激活组中的技能数量。 + */ + int32 ActivationGroupCounts[(uint8)EGGA_AbilityActivationGroup::MAX]; + +#pragma endregion + +public: + /** + * Function type for determining whether to cancel an ability. + * 确定是否取消技能的函数类型。 + */ + typedef TFunctionRef TShouldCancelAbilityFunc; + + /** + * Cancels abilities based on a provided function. + * 根据提供的函数取消技能。 + * @param ShouldCancelFunc Function to determine cancellation. 确定取消的函数。 + * @param bReplicateCancelAbility Whether to replicate cancellation. 是否复制取消。 + */ + void CancelAbilitiesByFunc(TShouldCancelAbilityFunc ShouldCancelFunc, bool bReplicateCancelAbility); + + /** + * Cancels abilities in a specific activation group. + * 取消特定激活组中的技能。 + * @param Group The activation group. 激活组。 + * @param IgnoreAbility Ability to ignore. 要忽略的技能。 + * @param bReplicateCancelAbility Whether to replicate cancellation. 是否复制取消。 + */ + void CancelActivationGroupAbilities(EGGA_AbilityActivationGroup Group, UGameplayAbility* IgnoreAbility, bool bReplicateCancelAbility); + + /** + * Delegate triggered when an ability ends. + * 技能结束时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGGA_OnAbilityEndedSignature AbilityEndedEvent; + +protected: + /** + * Notifies when an ability ends. + * 技能结束时通知。 + * @param Handle The ability spec handle. 技能规格句柄。 + * @param Ability The ability that ended. 结束的技能。 + * @param bWasCancelled Whether the ability was cancelled. 技能是否被取消。 + */ + virtual void NotifyAbilityEnded(FGameplayAbilitySpecHandle Handle, UGameplayAbility* Ability, bool bWasCancelled) override; + + /** + * Handles changes to whether an ability can be canceled. + * 处理技能是否可取消的更改。 + * @param AbilityTags The ability tags. 技能标签。 + * @param RequestingAbility The requesting ability. 请求的技能。 + * @param bCanBeCanceled Whether the ability can be canceled. 技能是否可取消。 + */ + virtual void HandleChangeAbilityCanBeCanceled(const FGameplayTagContainer& AbilityTags, UGameplayAbility* RequestingAbility, bool bCanBeCanceled) override; + +public: + /** + * Retrieves cooldown information for specified tags. + * 获取指定标签的冷却信息。 + * @param CooldownTags The cooldown tags. 冷却标签。 + * @param TimeRemaining Remaining cooldown time (output). 剩余冷却时间(输出)。 + * @param CooldownDuration Total cooldown duration (output). 总冷却持续时间(输出)。 + * @return True if active cooldowns found, false otherwise. 如果找到激活的冷却则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Abilities") + bool GetCooldownRemainingForTags(FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration); + + /** + * Determines if server ability RPC batching is enabled. + * 确定是否启用服务器技能RPC批处理。 + * @return True if batching is enabled. 如果启用批处理则返回true。 + */ + virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; } + + /** + * Attempts to activate an ability with batched RPCs. + * 尝试使用批处理RPC激活技能。 + * @param InAbilityHandle The ability handle. 技能句柄。 + * @param EndAbilityImmediately Whether to end the ability immediately. 是否立即结束技能。 + * @return True if activation was successful, false otherwise. 如果激活成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Abilities") + virtual bool BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately); + +protected: + /** + * Gameplay effect replication mode. + * 游戏效果复制模式。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|GameplayEffects") + EGameplayEffectReplicationMode AbilitySystemReplicationMode; + +public: + /** + * Retrieves owned gameplay tags. + * 获取拥有的游戏标签。 + * @param TagContainer The container for owned tags (output). 拥有的标签容器(输出)。 + */ + virtual void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const override; + + /** + * Retrieves owned gameplay tags as a string. + * 以字符串形式获取拥有的游戏标签。 + * @return The owned tags as a string. 拥有的标签字符串。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayTags") + FString GetOwnedGameplayTagsString(); + + /** + * Retrieves additional required and blocked activation tags. + * 获取额外的所需和阻止的激活标签。 + * @param AbilityTags The ability tags. 技能标签。 + * @param OutActivationRequired Required activation tags (output). 所需激活标签(输出)。 + * @param OutActivationBlocked Blocked activation tags (output). 阻止激活标签(输出)。 + */ + virtual void GetAdditionalActivationTagRequirements(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer& OutActivationRequired, FGameplayTagContainer& OutActivationBlocked) const; + + /** + * Applies ability block and cancel tags. + * 应用技能阻挡和取消标签。 + * @param AbilityTags The ability tags. 技能标签。 + * @param RequestingAbility The requesting ability. 请求的技能。 + * @param bEnableBlockTags Whether to enable block tags. 是否启用阻挡标签。 + * @param BlockTags The block tags. 阻挡标签。 + * @param bExecuteCancelTags Whether to execute cancel tags. 是否执行取消标签。 + * @param CancelTags The cancel tags. 取消标签。 + */ + virtual void ApplyAbilityBlockAndCancelTags(const FGameplayTagContainer& AbilityTags, UGameplayAbility* RequestingAbility, bool bEnableBlockTags, const FGameplayTagContainer& BlockTags, + bool bExecuteCancelTags, const FGameplayTagContainer& CancelTags) override; + + /** + * Sets the tag relationship mapping. + * 设置标签关系映射。 + * @param NewMapping The new tag relationship mapping. 新标签关系映射。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Gameplay Tags") + void SetTagRelationshipMapping(UGGA_AbilityTagRelationshipMapping* NewMapping); + +protected: + /** + * Tag relationship mapping for ability activation and cancellation. + * 用于技能激活和取消的标签关系映射。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|Gameplay Tags") + TObjectPtr TagRelationshipMapping; + +public: + /** + * Retrieves owned attribute sets as a string. + * 以字符串形式获取拥有的属性集。 + * @return The owned attribute sets as a string. 拥有的属性集字符串。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Attributes") + FString GetOwnedGameplayAttributeSetString(); + +#pragma region TargetData + +public: + /** + * Retrieves ability target data for a given ability handle and activation info. + * 获取指定技能句柄和激活信息的技能目标数据。 + * @param AbilityHandle The ability handle. 技能句柄。 + * @param ActivationInfo The activation info. 激活信息。 + * @param OutTargetDataHandle The target data handle (output). 目标数据句柄(输出)。 + */ + void GetAbilityTargetData(const FGameplayAbilitySpecHandle AbilityHandle, FGameplayAbilityActivationInfo ActivationInfo, FGameplayAbilityTargetDataHandle& OutTargetDataHandle); +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemEnumLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemEnumLibrary.h new file mode 100644 index 0000000..3708b14 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemEnumLibrary.h @@ -0,0 +1,38 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemEnumLibrary.generated.h" + +/** + * Enum defining how an ability activates relative to other abilities. + * 定义技能相对于其他技能的激活方式的枚举。 + */ +UENUM(BlueprintType) +enum class EGGA_AbilityActivationGroup : uint8 +{ + /** + * Ability runs independently of other abilities. + * 技能独立于其他技能运行。 + */ + Independent, + + /** + * Ability is canceled and replaced by other exclusive abilities. + * 技能被其他独占技能取消并替换。 + */ + Exclusive_Replaceable, + + /** + * Ability blocks all other exclusive abilities from activating. + * 技能阻止其他独占技能激活。 + */ + Exclusive_Blocking, + + /** + * Maximum value (hidden). + * 最大值(隐藏)。 + */ + MAX UMETA(Hidden) +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemStructLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemStructLibrary.h new file mode 100644 index 0000000..3766ab8 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilitySystemStructLibrary.h @@ -0,0 +1,214 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "GGA_AbilitySystemStructLibrary.generated.h" + +class UTargetingPreset; +class UGameplayEffect; + +/** + * Struct for defining an attribute group name. + * 定义属性组名称的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_AttributeGroupName +{ + GENERATED_BODY() + +public: + /** + * Main name of the attribute group. + * 属性组的主名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + FName MainName{NAME_None}; + + /** + * Sub-name of the attribute group. + * 属性组的子名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGA") + FName SubName{NAME_None}; + + /** + * Checks if the group name is valid. + * 检查组名称是否有效。 + * @return True if valid, false otherwise. 如果有效则返回true,否则返回false。 + */ + bool IsValid() const { return !MainName.IsNone(); } + + /** + * Retrieves the combined group name. + * 获取组合的组名称。 + * @return The combined name. 组合名称。 + */ + FName GetName() const + { + FName Ref = MainName; + if (!SubName.IsNone()) + { + Ref = FName(*FString::Printf(TEXT("%s.%s"), *Ref.ToString(), *SubName.ToString())); + } + return Ref; + } +}; + +/** + * Struct for tracking gameplay tag counts. + * 跟踪游戏标签计数的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayTagCount +{ + GENERATED_BODY() + + /** + * The gameplay tag. + * 游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayTag Tag; + + /** + * The count of the tag. + * 标签的计数。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + int32 Count{0}; +}; + +/** + * Struct for storing an array of gameplay tags. + * 存储游戏标签数组的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayTagArray +{ + GENERATED_BODY() + + /** + * Array of gameplay tags. + * 游戏标签数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TArray GameplayTags; + + /** + * Converts the array to a gameplay tag container. + * 将数组转换为游戏标签容器。 + * @return The gameplay tag container. 游戏标签容器。 + */ + FORCEINLINE FGameplayTagContainer GetGameplayTagContainer() const + { + return FGameplayTagContainer::CreateFromArray(GameplayTags); + } + + /** + * Creates a tag array from a gameplay tag container. + * 从游戏标签容器创建标签数组。 + * @param Container The gameplay tag container. 游戏标签容器。 + * @return The created tag array. 创建的标签数组。 + */ + FORCEINLINE static FGGA_GameplayTagArray CreateFromContainer(const FGameplayTagContainer& Container) + { + FGGA_GameplayTagArray TagArray; + Container.GetGameplayTagArray(TagArray.GameplayTags); + return TagArray; + } +}; + +/** + * Struct for storing an array of gameplay effects. + * 存储游戏效果数组的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayEffectArray +{ + GENERATED_BODY() + + /** + * Array of gameplay effect classes. + * 游戏效果类数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TArray> GameplayEffects; +}; + +/** + * Struct defining a gameplay effect container with targeting info. + * 定义带有目标信息的游戏效果容器的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayEffectContainer +{ + GENERATED_BODY() + +public: + FGGA_GameplayEffectContainer() {} + + /** + * Targeting preset for the effect container. + * 效果容器的目标预设。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGA") + TObjectPtr TargetingPreset; + + /** + * List of gameplay effect classes to apply. + * 要应用的游戏效果类列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGA") + TArray> TargetGameplayEffectClasses; +}; + +/** + * Processed gameplay effect container spec for application. + * 用于应用的处理过的游戏效果容器规格。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayEffectContainerSpec +{ + GENERATED_BODY() + +public: + FGGA_GameplayEffectContainerSpec() {} + + /** + * Computed target data for the effect. + * 效果的计算目标数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayAbilityTargetDataHandle TargetData; + + /** + * List of gameplay effect specs to apply. + * 要应用的游戏效果规格列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TArray TargetGameplayEffectSpecs; + + /** + * Checks if the spec has valid effects. + * 检查规格是否具有有效效果。 + * @return True if valid effects exist, false otherwise. 如果存在有效效果则返回true,否则返回false。 + */ + bool HasValidEffects() const; + + /** + * Checks if the spec has valid targets. + * 检查规格是否具有有效目标。 + * @return True if valid targets exist, false otherwise. 如果存在有效目标则返回true,否则返回false。 + */ + bool HasValidTargets() const; + + /** + * Adds targets to the target data. + * 将目标添加到目标数据。 + * @param HitResults Array of hit results. 命中结果数组。 + * @param TargetActors Array of target actors. 目标演员数组。 + */ + void AddTargets(const TArray& HitResults, const TArray& TargetActors); +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilityTagRelationshipMapping.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilityTagRelationshipMapping.h new file mode 100644 index 0000000..9735c84 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_AbilityTagRelationshipMapping.h @@ -0,0 +1,184 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/DataAsset.h" +#include "GameplayTagContainer.h" +#include "GGA_AbilityTagRelationshipMapping.generated.h" + +/** + * Struct defining the relationship between different ability tags. + * 定义不同技能标签之间关系的结构体。 + */ +USTRUCT() +struct GENERICGAMEPLAYABILITIES_API FGGA_AbilityTagRelationship +{ + GENERATED_BODY() + + /** + * The ability tag these rules apply to. + * 这些规则适用的技能标签。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTag AbilityTag; + + /** + * Tags of abilities blocked while this ability is active. + * 此技能激活时被阻挡的技能标签。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTagContainer AbilityTagsToBlock; + + /** + * Tags of abilities cancelled when this ability is executed. + * 此技能执行时被取消的技能标签。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTagContainer AbilityTagsToCancel; + + /** + * Tags required on the activating actor/component to activate this ability. + * 激活此技能所需的演员/组件标签。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTagContainer ActivationRequiredTags; + + /** + * Tags that block activation if present on the activating actor/component. + * 如果演员/组件具有这些标签,将阻止激活。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTagContainer ActivationBlockedTags; + +#if WITH_EDITORONLY_DATA + /** + * Description for developers in the editor. + * 编辑器中用于开发者的描述。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FString DevDescription; + + /** + * Editor-friendly name for the relationship. + * 关系在编辑器中的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Struct defining ability tag relationships with a tag query condition. + * 定义带有标签查询条件的技能标签关系的结构体。 + */ +USTRUCT() +struct FGGA_AbilityTagRelationshipsWithQuery +{ + GENERATED_BODY() + + /** + * Tag query to determine when these relationships apply. + * 用于确定何时应用这些关系的标签查询。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities") + FGameplayTagQuery ActorTagQuery; + + /** + * Array of ability tag relationships. + * 技能标签关系数组。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities", meta=(TitleProperty="EditorFriendlyName")) + TArray AbilityTagRelationships; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the query. + * 查询在编辑器中的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Data asset for mapping ability tag relationships (block or cancel). + * 用于映射技能标签关系(阻挡或取消)的数据资产。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilityTagRelationshipMapping : public UDataAsset +{ + GENERATED_BODY() + +private: + /** + * List of ability tag relationships. + * 技能标签关系列表。 + */ + UPROPERTY(EditAnywhere, Category = "GGA|Abilities", meta=(TitleProperty="EditorFriendlyName")) + TArray AbilityTagRelationships; + + /** + * Conditional ability tag relationships based on tag queries. + * 基于标签查询的条件技能标签关系。 + */ + UPROPERTY(EditAnywhere, Category = "GCS", meta=(TitleProperty="EditorFriendlyName")) + TArray Layered; + +public: + /** + * Retrieves tags to block and cancel based on actor and ability tags. + * 根据演员和技能标签获取要阻挡和取消的标签。 + * @param ActorTags Tags on the actor. 演员标签。 + * @param AbilityTags Tags of the ability. 技能标签。 + * @param OutTagsToBlock Tags to block (output). 要阻挡的标签(输出)。 + * @param OutTagsToCancel Tags to cancel (output). 要取消的标签(输出)。 + */ + void GetAbilityTagsToBlockAndCancelV2(const FGameplayTagContainer& ActorTags, const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutTagsToBlock, + FGameplayTagContainer* OutTagsToCancel) const; + + /** + * Retrieves tags to block and cancel based on ability tags. + * 根据技能标签获取要阻挡和取消的标签。 + * @param AbilityTags Tags of the ability. 技能标签。 + * @param OutTagsToBlock Tags to block (output). 要阻挡的标签(输出)。 + * @param OutTagsToCancel Tags to cancel (output). 要取消的标签(输出)。 + */ + void GetAbilityTagsToBlockAndCancel(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutTagsToBlock, FGameplayTagContainer* OutTagsToCancel) const; + + /** + * Retrieves required and blocked activation tags based on actor and ability tags. + * 根据演员和技能标签获取所需和阻止的激活标签。 + * @param ActorTags Tags on the actor. 演员标签。 + * @param AbilityTags Tags of the ability. 技能标签。 + * @param OutActivationRequired Required activation tags (output). 所需激活标签(输出)。 + * @param OutActivationBlocked Blocked activation tags (output). 阻止激活标签(输出)。 + */ + void GetRequiredAndBlockedActivationTagsV2(const FGameplayTagContainer& ActorTags, const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutActivationRequired, + FGameplayTagContainer* OutActivationBlocked) const; + + /** + * Retrieves required and blocked activation tags based on ability tags. + * 根据技能标签获取所需和阻止的激活标签。 + * @param AbilityTags Tags of the ability. 技能标签。 + * @param OutActivationRequired Required activation tags (output). 所需激活标签(输出)。 + * @param OutActivationBlocked Blocked activation tags (output). 阻止激活标签(输出)。 + */ + void GetRequiredAndBlockedActivationTags(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutActivationRequired, FGameplayTagContainer* OutActivationBlocked) const; + + /** + * Checks if an ability is cancelled by a specific action tag. + * 检查技能是否被特定动作标签取消。 + * @param AbilityTags Tags of the ability. 技能标签。 + * @param ActionTag The action tag to check. 要检查的动作标签。 + * @return True if the ability is cancelled, false otherwise. 如果技能被取消则返回true,否则返回false。 + */ + bool IsAbilityCancelledByTag(const FGameplayTagContainer& AbilityTags, const FGameplayTag& ActionTag) const; + +#if WITH_EDITOR + /** + * Pre-save processing for editor. + * 编辑器预保存处理。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GameplayTags.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GameplayTags.h new file mode 100644 index 0000000..1090d5d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GameplayTags.h @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "NativeGameplayTags.h" + + +namespace GGA_AbilityActivateFailTags +{ + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(IsDead) + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Cooldown) + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Cost) + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TagsBlocked); + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TagsMissing); + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Networking); + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(ActivationGroup); +} + +namespace GGA_AbilityTraitTags +{ + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(ActivationOnSpawn) + GENERICGAMEPLAYABILITIES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Persistent) + +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GlobalAbilitySystem.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GlobalAbilitySystem.h new file mode 100644 index 0000000..381bc5b --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_GlobalAbilitySystem.h @@ -0,0 +1,175 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "GameplayAbilitySpec.h" +#include "GameplayEffectTypes.h" +#include "GGA_GlobalAbilitySystem.generated.h" + +class UGGA_AbilitySystemComponent; +class UAbilitySystemComponent; +class UGameplayEffect; +class UGameplayAbility; + +/** + * Struct to track globally applied abilities. + * 跟踪全局应用的技能的结构体。 + */ +USTRUCT() +struct GENERICGAMEPLAYABILITIES_API FGGA_GlobalAppliedAbilityList +{ + GENERATED_BODY() + + /** + * Map of ability system components to their ability spec handles. + * 技能系统组件到技能规格句柄的映射。 + */ + UPROPERTY() + TMap, FGameplayAbilitySpecHandle> Handles; + + /** + * Adds an ability to an ability system component. + * 将技能添加到技能系统组件。 + * @param Ability The ability class to add. 要添加的技能类。 + * @param ASC The ability system component. 技能系统组件。 + */ + void AddToASC(TSubclassOf Ability, UGGA_AbilitySystemComponent* ASC); + + /** + * Removes an ability from an ability system component. + * 从技能系统组件移除技能。 + * @param ASC The ability system component. 技能系统组件。 + */ + void RemoveFromASC(UGGA_AbilitySystemComponent* ASC); + + /** + * Removes all abilities from all components. + * 从所有组件移除所有技能。 + */ + void RemoveFromAll(); +}; + +/** + * Struct to track globally applied effects. + * 跟踪全局应用的效果的结构体。 + */ +USTRUCT() +struct GENERICGAMEPLAYABILITIES_API FGGA_GlobalAppliedEffectList +{ + GENERATED_BODY() + + /** + * Map of ability system components to their effect handles. + * 技能系统组件到效果句柄的映射。 + */ + UPROPERTY() + TMap, FActiveGameplayEffectHandle> Handles; + + /** + * Adds an effect to an ability system component. + * 将效果添加到技能系统组件。 + * @param Effect The effect class to add. 要添加的效果类。 + * @param ASC The ability system component. 技能系统组件。 + */ + void AddToASC(TSubclassOf Effect, UGGA_AbilitySystemComponent* ASC); + + /** + * Removes an effect from an ability system component. + * 从技能系统组件移除效果。 + * @param ASC The ability system component. 技能系统组件。 + */ + void RemoveFromASC(UGGA_AbilitySystemComponent* ASC); + + /** + * Removes all effects from all components. + * 从所有组件移除所有效果。 + */ + void RemoveFromAll(); +}; + +/** + * World subsystem for managing global abilities and effects. + * 管理全局技能和效果的世界子系统。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GlobalAbilitySystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + /** + * Constructor for the global ability system. + * 全局技能系统构造函数。 + */ + UGGA_GlobalAbilitySystem(); + + /** + * Applies an ability to all registered ability system components. + * 将技能应用到所有注册的技能系统组件。 + * @param Ability The ability class to apply. 要应用的技能类。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|Global") + void ApplyAbilityToAll(TSubclassOf Ability); + + /** + * Applies an effect to all registered ability system components. + * 将效果应用到所有注册的技能系统组件。 + * @param Effect The effect class to apply. 要应用的效果类。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|Global") + void ApplyEffectToAll(TSubclassOf Effect); + + /** + * Removes an ability from all registered ability system components. + * 从所有注册的技能系统组件移除技能。 + * @param Ability The ability class to remove. 要移除的技能类。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|Global") + void RemoveAbilityFromAll(TSubclassOf Ability); + + /** + * Removes an effect from all registered ability system components. + * 从所有注册的技能系统组件移除效果。 + * @param Effect The effect class to remove. 要移除的效果类。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|Global") + void RemoveEffectFromAll(TSubclassOf Effect); + + /** + * Registers an ability system component with the global system. + * 将技能系统组件注册到全局系统。 + * @param ASC The ability system component to register. 要注册的技能系统组件。 + */ + void RegisterASC(UGGA_AbilitySystemComponent* ASC); + + /** + * Unregisters an ability system component from the global system. + * 从全局系统取消注册技能系统组件。 + * @param ASC The ability system component to unregister. 要取消注册的技能系统组件。 + */ + void UnregisterASC(UGGA_AbilitySystemComponent* ASC); + +private: + /** + * Map of applied abilities. + * 已应用的技能映射。 + */ + UPROPERTY() + TMap, FGGA_GlobalAppliedAbilityList> AppliedAbilities; + + /** + * Map of applied effects. + * 已应用的效果映射。 + */ + UPROPERTY() + TMap, FGGA_GlobalAppliedEffectList> AppliedEffects; + + /** + * List of registered ability system components. + * 注册的技能系统组件列表。 + */ + UPROPERTY() + TArray> RegisteredASCs; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_LogChannels.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_LogChannels.h new file mode 100644 index 0000000..91099c2 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GGA_LogChannels.h @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + + +GENERICGAMEPLAYABILITIES_API DECLARE_LOG_CATEGORY_EXTERN(LogGGA_Ability, Log, All); + +GENERICGAMEPLAYABILITIES_API DECLARE_LOG_CATEGORY_EXTERN(LogGGA_AbilitySystem, Log, All); + +GENERICGAMEPLAYABILITIES_API DECLARE_LOG_CATEGORY_EXTERN(LogGGA_Tasks, Log, All); + +#define GGA_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGGA_AbilitySystem, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_Character.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_Character.h new file mode 100644 index 0000000..be425a7 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_Character.h @@ -0,0 +1,68 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemInterface.h" +#include "GameplayTagAssetInterface.h" +#include "GameFramework/Character.h" +#include "GGA_Character.generated.h" + + +/** + * @brief A character with ability system. + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_Character : public ACharacter, public IAbilitySystemInterface, public IGameplayTagAssetInterface + +{ + GENERATED_BODY() + +public: + // Sets default values for this character's properties + AGGA_Character(const FObjectInitializer& ObjectInitializer); + + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +#pragma region Pawn + +protected: + virtual void OnRep_Controller() override; + virtual void OnRep_PlayerState() override; + + /** + * @brief Called when controller replicated to client. + */ + UFUNCTION(BlueprintImplementableEvent, Category=Character, meta=(DisplayName="Receive Player Controller")) + void ReceivePlayerController(); + + /** + * @brief Called when player state replicated to client. + */ + UFUNCTION(BlueprintImplementableEvent, Category=Character, meta=(DisplayName="Receive Player State")) + void ReceivePlayerState(); + +#pragma endregion Pawn + + +#pragma region AbilitySystem + +public: + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + +protected: + UFUNCTION(BlueprintImplementableEvent) + UAbilitySystemComponent* CustomGetAbilitySystemComponent() const; + +private: +#pragma endregion AbilitySystem + +public: + virtual void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const override; + + + // Called to bind functionality to input + virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_CharacterWithAbilities.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_CharacterWithAbilities.h new file mode 100644 index 0000000..462294a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_CharacterWithAbilities.h @@ -0,0 +1,23 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemComponent.h" +#include "GGA_Character.h" +#include "GGA_CharacterWithAbilities.generated.h" + +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_CharacterWithAbilities : public AGGA_Character +{ + GENERATED_BODY() + +public: + AGGA_CharacterWithAbilities(const FObjectInitializer& ObjectInitializer); + + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + +private: + UPROPERTY(Category=Character, VisibleAnywhere, BlueprintReadOnly, meta=(AllowPrivateAccess = "true")) + TObjectPtr AbilitySystemComponent; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_GameState.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_GameState.h new file mode 100644 index 0000000..cb1f157 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_GameState.h @@ -0,0 +1,78 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "AbilitySystemInterface.h" +#include "Engine/EngineTypes.h" +#include "GameFramework/GameState.h" +#include "GameFramework/GameStateBase.h" +#include "GGA_GameState.generated.h" + +class UGGA_AbilitySystemComponent; +class UObject; + + +/** + * @brief A Game State base with ability system component. + */ +UCLASS(Blueprintable) +class GENERICGAMEPLAYABILITIES_API AGGA_GameStateBase : public AGameStateBase, public IAbilitySystemInterface +{ + GENERATED_BODY() + +public: + + AGGA_GameStateBase(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void PostInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface + + //~IAbilitySystemInterface + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + //~End of IAbilitySystemInterface + +private: + // The ability system component subobject for game-wide things (primarily gameplay cues) + UPROPERTY(VisibleAnywhere, Category = "GGA|GameState") + TObjectPtr AbilitySystemComponent; +}; + + +/** + * @brief A Game State with ability system component. + */ +UCLASS(Blueprintable) +class GENERICGAMEPLAYABILITIES_API AGGA_GameState : public AGameState, public IAbilitySystemInterface +{ + GENERATED_BODY() + +public: + + AGGA_GameState(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void PostInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface + + protected: + //~ Begin AGameState interface + virtual void HandleMatchHasStarted() override; + //~ Begin AGameState interface + + + //~IAbilitySystemInterface + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + //~End of IAbilitySystemInterface + +private: + // The ability system component subobject for game-wide things (primarily gameplay cues) + UPROPERTY(VisibleAnywhere, Category = "GameState") + TObjectPtr AbilitySystemComponent; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_PlayerState.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_PlayerState.h new file mode 100644 index 0000000..384c4c7 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GameplayActors/GGA_PlayerState.h @@ -0,0 +1,48 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemInterface.h" +#include "GGA_AbilitySystemComponent.h" +#include "GameFramework/PlayerState.h" +#include "GGA_PlayerState.generated.h" + +/** + * @brief Minimal PlayerState class that supports extension by game feature plugins. + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_PlayerState : public APlayerState, public IAbilitySystemInterface +{ + GENERATED_BODY() + +public: + AGGA_PlayerState(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void Reset() override; + virtual void ClientInitialize(AController* C) override; + //~ End AActor interface + + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + +protected: + //~ Begin APlayerState interface + virtual void CopyProperties(APlayerState* PlayerState); + //~ End APlayerState interface + +protected: + /** + * Called by Controller when its PlayerState is initially replicated. + * @param Controller + */ + UFUNCTION(BlueprintImplementableEvent) + void ReceiveClientInitialize(AController* Controller); + +private: + UPROPERTY(Category=PlayerState, VisibleAnywhere, BlueprintReadOnly, meta=(AllowPrivateAccess = "true")) + TObjectPtr AbilitySystemComponent; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/GenericGameplayAbilities.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GenericGameplayAbilities.h new file mode 100644 index 0000000..1033638 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/GenericGameplayAbilities.h @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FGenericGameplayAbilitiesModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_AbilitySystemGlobals.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_AbilitySystemGlobals.h new file mode 100644 index 0000000..1cd2367 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_AbilitySystemGlobals.h @@ -0,0 +1,153 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemGlobals.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "GGA_AbilitySystemGlobals.generated.h" + +class IGGA_AbilitySystemGlobalsEventReceiver; + +/** + * Extended ability system globals for custom functionality. + * 扩展的技能系统全局类,提供自定义功能。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilitySystemGlobals : public UAbilitySystemGlobals +{ + GENERATED_BODY() + +public: + /** + * Processes gameplay effect specs before application. + * 在应用游戏效果规格前进行处理。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 技能系统组件。 + */ + virtual void GlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) override; + + /** + * Allocate a GGA_GameplayEffectContext struct. Caller is responsible for deallocation + * 分配一个GGA_GameplayEffectContext. + */ + virtual FGameplayEffectContext* AllocGameplayEffectContext() const override; + + /** + * Retrieves the ability system globals instance. + * 获取技能系统全局实例。 + * @return The ability system globals instance. 技能系统全局实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Globals") + static const UAbilitySystemGlobals* GetAbilitySystemGlobals(); + + /** + * Retrieves a typed ability system globals instance. + * 获取特定类型的技能系统全局实例。 + * @param DesiredClass The desired class type. 所需的类类型。 + * @return The typed ability system globals instance. 特定类型的技能系统全局实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Globals", meta=(DynamicOutputParam=ReturnValue, DeterminesOutputType=DesiredClass)) + static const UAbilitySystemGlobals* GetTypedAbilitySystemGloabls(TSubclassOf DesiredClass); + + /** + * Registers an event receiver for global events. + * 为全局事件注册事件接收器。 + * @param NewReceiver The event receiver to register. 要注册的事件接收器。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Globals", meta=(DefaultToSelf="NewReceiver")) + static void RegisterEventReceiver(TScriptInterface NewReceiver); + + /** + * Unregisters an event receiver from global events. + * 从全局事件取消注册事件接收器。 + * @param NewReceiver The event receiver to unregister. 要取消注册的事件接收器。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Globals", meta=(DefaultToSelf="NewReceiver")) + static void UnregisterEventReceiver(TScriptInterface NewReceiver); + + /** + * Retrieves all currently loaded attribute default curve tables. + * 获取当前加载的所有属性默认曲线表。 + * @return Array of curve tables. 曲线表数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Globals") + TArray GetAttributeDefaultsTables() const; + + /** + * Initializes attribute set defaults for an ability system component. + * 为技能系统组件初始化属性集默认值。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param GroupName The attribute group name. 属性组名称。 + * @param Level The level to initialize. 初始化等级。 + * @param bInitialInit Whether this is the initial initialization. 是否为初始初始化。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|Globals") + void InitAttributeSetDefaults(UAbilitySystemComponent* AbilitySystem, const FGGA_AttributeGroupName& GroupName, int32 Level = 1, bool bInitialInit = false) const; + + /** + * Applies a single attribute default to an ability system component. + * 为技能系统组件应用单个属性默认值。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param InAttribute The attribute to apply. 要应用的属性。 + * @param GroupName The attribute group name. 属性组名称。 + * @param Level The level to apply. 应用等级。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|Globals") + void ApplyAttributeDefault(UAbilitySystemComponent* AbilitySystem, FGameplayAttribute& InAttribute, const FGGA_AttributeGroupName& GroupName, int32 Level) const; + +private: + /** + * List of registered event receivers. + * 注册的事件接收器列表。 + */ + UPROPERTY() + TArray> Receivers; +}; + +/** + * Interface for receiving global ability system events. + * 接收全局技能系统事件的接口。 + */ +UINTERFACE() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilitySystemGlobalsEventReceiver : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Implementation class for global ability system event receiver. + * 全局技能系统事件接收器的实现类。 + */ +class GENERICGAMEPLAYABILITIES_API IGGA_AbilitySystemGlobalsEventReceiver +{ + GENERATED_BODY() + +public: + /** + * Handles global pre-gameplay effect spec application. + * 处理全局游戏效果规格应用前的事件。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 技能系统组件。 + */ + virtual void ReceiveGlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent); + +protected: + /** + * Virtual function for handling pre-gameplay effect spec application. + * 处理游戏效果规格应用前的虚函数。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 技能系统组件。 + */ + virtual void OnGlobalPreGameplayEffectSpecApply(FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent) = 0; + + /** + * Blueprint event for handling pre-gameplay effect spec application. + * 处理游戏效果规格应用前的蓝图事件。 + * @param Spec The gameplay effect spec. 游戏效果规格。 + * @param AbilitySystemComponent The ability system component. 技能系统组件。 + * @param OutDynamicTagsAppendToSpec Tags to append to the spec (output). 要附加到规格的标签(输出)。 + */ + UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category="GGA|Global", meta=(DisplayName="On Global Pre Gameplay Effect Spec Apply")) + void OnGlobalPreGameplayEffectSpecApply_Bp(const FGameplayEffectSpec& Spec, UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer& OutDynamicTagsAppendToSpec); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayAbilityTargetData_Payload.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayAbilityTargetData_Payload.h new file mode 100644 index 0000000..dd1a8ff --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayAbilityTargetData_Payload.h @@ -0,0 +1,58 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "StructUtils/InstancedStruct.h" +#include "Abilities/GameplayAbilityTargetTypes.h" +#include "GGA_GameplayAbilityTargetData_Payload.generated.h" + +/** + * + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayAbilityTargetData_Payload : public FGameplayAbilityTargetData +{ + GENERATED_BODY() + + virtual ~FGGA_GameplayAbilityTargetData_Payload() override + { + } + + FGGA_GameplayAbilityTargetData_Payload() + { + }; + + FGGA_GameplayAbilityTargetData_Payload(const FInstancedStruct& InPayload) + : Payload(InPayload) + { + } + + virtual UScriptStruct* GetScriptStruct() const override + { + return FGGA_GameplayAbilityTargetData_Payload::StaticStruct(); + } + + virtual FString ToString() const override + { + return TEXT("FGGA_GameplayAbilityTargetData_Payload"); + } + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting) + FInstancedStruct Payload; + + bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) + { + return Payload.NetSerialize(Ar, Map, bOutSuccess); + }; +}; + + +template <> +struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetSerializer = true, // For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work + }; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayEffectContext.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayEffectContext.h new file mode 100644 index 0000000..7cc13d2 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Globals/GGA_GameplayEffectContext.h @@ -0,0 +1,108 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectTypes.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GGA_GameplayEffectContext.generated.h" + + +/** + * + */ +USTRUCT() +struct GENERICGAMEPLAYABILITIES_API FGGA_GameplayEffectContext : public FGameplayEffectContext +{ + GENERATED_BODY() + + virtual FGameplayEffectContext* Duplicate() const override; + virtual UScriptStruct* GetScriptStruct() const override; + virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override; + + TArray& GetPayloads(); + + void AddOrOverwriteData(const FInstancedStruct& DataInstance); + const FInstancedStruct* FindPayloadByType(const UScriptStruct* PayloadType) const; + FInstancedStruct* FindPayloadByType(const UScriptStruct* PayloadType); + FInstancedStruct* FindOrAddPayloadByType(const UScriptStruct* PayloadType); + FInstancedStruct* AddPayloadByType(const UScriptStruct* PayloadType); + bool RemovePayloadByType(const UScriptStruct* PayloadType); + + /** Find payload of a specific type in this context (mutable version). If not found, null will be returned. */ + template + T* FindMutablePayloadByType() + { + if (FInstancedStruct* FoundData = FindPayloadByType(T::StaticStruct())) + { + return FoundData->GetMutablePtr(); + } + return nullptr; + } + + /** Find payload of a specific type in this context. If not found, null will be returned. */ + template + const T* FindPayload() const + { + if (const FInstancedStruct* FoundData = FindPayloadByType(T::StaticStruct())) + { + return FoundData->GetPtr(); + } + return nullptr; + } + + /** Find payload of a specific type in this context. If not found, a new default instance will be added. */ + template + const T& FindOrAddPayload() + { + if (const T* ExistingData = FindPayload()) + { + return *ExistingData; + } + FInstancedStruct* NewData = AddPayloadByType(T::StaticStruct()); + return NewData->Get(); + } + + /** Find payload of a specific type in this context. (mutable version). If not found, a new default instance will be added. */ + template + T& FindOrAddMutablePayload() + { + if (T* ExistingData = FindMutablePayloadByType()) + { + return *ExistingData; + } + FInstancedStruct* NewData = AddPayloadByType(T::StaticStruct()); + return NewData->GetMutable(); + } + + template + T* FindOrAddMutablePayloadPtr() + { + if (T* ExistingData = FindMutablePayloadByType()) + { + return ExistingData; + } + FInstancedStruct* NewData = AddPayloadByType(T::StaticStruct()); + return NewData->GetMutablePtr(); + } + +protected: + UPROPERTY() + TArray Payloads; +}; + + +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetSerializer = true, + WithCopy = true + }; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Notifies/GGA_AnimNotify_SendGameplayEvent.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Notifies/GGA_AnimNotify_SendGameplayEvent.h new file mode 100644 index 0000000..1253585 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Notifies/GGA_AnimNotify_SendGameplayEvent.h @@ -0,0 +1,25 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Animation/AnimNotifies/AnimNotify.h" +#include "GGA_AnimNotify_SendGameplayEvent.generated.h" + +/** + * An anim notify to send gameplay event to owner. + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AnimNotify_SendGameplayEvent : public UAnimNotify +{ + GENERATED_BODY() + + virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override; + + virtual FString GetNotifyName_Implementation() const override; + +protected: + UPROPERTY(EditAnywhere, Category="GGA", BlueprintReadWrite) + FGameplayTag EventTag; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseAbility.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseAbility.h new file mode 100644 index 0000000..944cd1c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseAbility.h @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Abilities/GGA_GameplayAbility.h" +#include "GGA_GamePhaseAbility.generated.h" + +/** + * UGGA_GamePhaseAbility + * + * The base gameplay ability for any ability that is used to change the active game phase. + */ +UCLASS(Abstract, HideCategories = Input) +class UGGA_GamePhaseAbility : public UGGA_GameplayAbility +{ + GENERATED_BODY() + +public: + + UGGA_GamePhaseAbility(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + const FGameplayTag& GetGamePhaseTag() const { return GamePhaseTag; } + +#if WITH_EDITOR +#if ENGINE_MINOR_VERSION > 2 + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +#endif + +protected: + + virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; + virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override; + +protected: + + // Defines the game phase that this game phase ability is part of. So for example, + // if your game phase is GamePhase.RoundStart, then it will cancel all sibling phases. + // So if you had a phase such as GamePhase.WaitingToStart that was active, starting + // the ability part of RoundStart would end WaitingToStart. However to get nested behaviors + // you can also nest the phases. So for example, GamePhase.Playing.NormalPlay, is a sub-phase + // of the parent GamePhase.Playing, so changing the sub-phase to GamePhase.Playing.SuddenDeath, + // would stop any ability tied to GamePhase.Playing.*, but wouldn't end any ability + // tied to the GamePhase.Playing phase. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GGA|GamePhase") + FGameplayTag GamePhaseTag; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseLog.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseLog.h new file mode 100644 index 0000000..dd90fed --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseLog.h @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogGGA_GamePhase, Log, All); diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseSubsystem.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseSubsystem.h new file mode 100644 index 0000000..709faa1 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Phases/GGA_GamePhaseSubsystem.h @@ -0,0 +1,103 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayAbilitySpecHandle.h" +#include "GameplayTagContainer.h" +#include "Subsystems/WorldSubsystem.h" + +#include "GGA_GamePhaseSubsystem.generated.h" + +class UGGA_GamePhaseAbility; + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGGamePhaseDynamicDelegate, const UGGA_GamePhaseAbility*, Phase); + +DECLARE_DELEGATE_OneParam(FGGamePhaseDelegate, const UGGA_GamePhaseAbility* Phase); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGGamePhaseTagDynamicDelegate, const FGameplayTag&, PhaseTag); + +DECLARE_DELEGATE_OneParam(FGGamePhaseTagDelegate, const FGameplayTag& PhaseTag); + +// Match rule for message receivers +UENUM(BlueprintType) +enum class EGGA_PhaseTagMatchType : uint8 +{ + // An exact match will only receive messages with exactly the same channel + // (e.g., registering for "A.B" will match a broadcast of A.B but not A.B.C) + ExactMatch, + + // A partial match will receive any messages rooted in the same channel + // (e.g., registering for "A.B" will match a broadcast of A.B as well as A.B.C) + PartialMatch +}; + + +/** Subsystem for managing game phases using gameplay tags in a nested manner, which allows parent and child + * phases to be active at the same time, but not sibling phases. + * Example: Game.Playing and Game.Playing.WarmUp can coexist, but Game.Playing and Game.ShowingScore cannot. + * When a new phase is started, any active phases that are not ancestors will be ended. + * Example: if Game.Playing and Game.Playing.CaptureTheFlag are active when Game.Playing.PostGame is started, + * Game.Playing will remain active, while Game.Playing.CaptureTheFlag will end. + */ +UCLASS() +class UGGA_GamePhaseSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + UGGA_GamePhaseSubsystem(); + + //virtual void PostInitialize() override; + + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + void StartPhase(TSubclassOf PhaseAbility, FGGamePhaseDelegate PhaseEndedCallback = FGGamePhaseDelegate()); + + //TODO Return a handle so folks can delete these. They will just grow until the world resets. + //TODO Should we just occasionally clean these observers up? It's not as if everyone will properly unhook them even if there is a handle. + void WhenPhaseStartsOrIsActive(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, const FGGamePhaseTagDelegate& WhenPhaseActive); + void WhenPhaseEnds(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, const FGGamePhaseTagDelegate& WhenPhaseEnd); + + UFUNCTION(BlueprintCallable, Category = "GGA|GamePhase", BlueprintAuthorityOnly, BlueprintPure = false, meta = (AutoCreateRefTerm = "PhaseTag")) + bool IsPhaseActive(const FGameplayTag& PhaseTag) const; + +protected: + virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override; + + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|GamePhase", meta = (DisplayName="Start Phase", AutoCreateRefTerm = "PhaseEnded")) + void K2_StartPhase(TSubclassOf Phase, const FGGamePhaseDynamicDelegate& PhaseEnded); + + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|GamePhase", meta = (DisplayName = "When Phase Starts or Is Active", AutoCreateRefTerm = "WhenPhaseActive")) + void K2_WhenPhaseStartsOrIsActive(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, FGGamePhaseTagDynamicDelegate WhenPhaseActive); + + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GGA|GamePhase", meta = (DisplayName = "When Phase Ends", AutoCreateRefTerm = "WhenPhaseEnd")) + void K2_WhenPhaseEnds(FGameplayTag PhaseTag, EGGA_PhaseTagMatchType MatchType, FGGamePhaseTagDynamicDelegate WhenPhaseEnd); + + void OnBeginPhase(const UGGA_GamePhaseAbility* PhaseAbility, const FGameplayAbilitySpecHandle PhaseAbilityHandle); + void OnEndPhase(const UGGA_GamePhaseAbility* PhaseAbility, const FGameplayAbilitySpecHandle PhaseAbilityHandle); + +private: + struct FGGamePhaseEntry + { + public: + FGameplayTag PhaseTag; + FGGamePhaseDelegate PhaseEndedCallback; + }; + + TMap ActivePhaseMap; + + struct FGPhaseObserver + { + public: + bool IsMatch(const FGameplayTag& ComparePhaseTag) const; + + FGameplayTag PhaseTag; + EGGA_PhaseTagMatchType MatchType = EGGA_PhaseTagMatchType::ExactMatch; + FGGamePhaseTagDelegate PhaseCallback; + }; + + TArray PhaseStartObservers; + TArray PhaseEndObservers; + + friend class UGGA_GamePhaseAbility; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_LineTrace.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_LineTrace.h new file mode 100644 index 0000000..8372a76 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_LineTrace.h @@ -0,0 +1,95 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilityTargetActor_Trace.h" +#include "DrawDebugHelpers.h" +#include "Engine/CollisionProfile.h" +#include "GGA_AbilityTargetActor_LineTrace.generated.h" + +/** +* 可重用、配置的线形检测目标捕获Actor。 +* 与UGAT_WaitTargetDataUsingActor配合使用。 +*/ +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_AbilityTargetActor_LineTrace : public AGGA_AbilityTargetActor_Trace +{ + GENERATED_BODY() + +public: + AGGA_AbilityTargetActor_LineTrace(); + + /** + * Configure the TargetActor for use. This TargetActor could be used in multiple abilities and there's no guarantee + * what state it will be in. You will need to make sure that only one ability is using this TargetActor at a time. + * + * @param InStartLocation Location to trace from. + * @param InAimingTag Optional. Predicted GameplayTag for aiming. Only used if we mofify spread while aiming. If used, + * must set InAimingRemovalTag also. + * @param InAimingRemovalTag Optional. Predicted GameplayTag for aiming removal. Only used if we mofify spread while + * aiming. If used, must set InAimingTag also. + * @param InTraceProfile Collision profile to use for tracing. + * @param InFilter Hit Actors must pass this filter to be returned in the TargetData. + * @param InReticleClass Reticle that will appear on top of acquired targets. Reticles will be spawned/despawned as targets are acquired/lost. + * @param InReticleParams Parameters for world reticle. Usage of these parameters is dependent on the reticle. + * @param bInIgnoreBlockingHits Ignore blocking collision hits in the trace. Useful if you want to target through walls. + * @param bInShouldProduceTargetDataOnServer If set, this TargetActor will produce TargetData on the Server in addition + * to the client and the client will just send a generic "Confirm" event to the server. If false, the client will send + * the TargetData to the Server. This is handled by the WaitTargetDataUsingActor AbilityTask. + * @param bInUsePersistentHitResults Should HitResults persist while targeting? HitResults are cleared on Confirm/Cancel or + * when new HitResults take their place. + * @param bInDebug When true, this TargetActor will show debug lines of the trace and hit results. + * @param bInTraceAffectsAimPitch Does the trace affect the aiming pitch? + * @param bInTraceFromPlayerViewPoint Should we trace from the player ViewPoint instead of the StartLocation? The + * TargetData HitResults will still have the StartLocation for the TraceStart. This is useful for FPS where we want + * to trace from the player ViewPoint but draw the bullet tracer from the weapon muzzle. + * TODO: AI Controllers should fall back to muzzle location. Not implemented yet. + * @param bInUseAImingSpreadMod Should we modify spread based on if we're aiming? If true, must set InAimingTag and + * InAimingRemovalTag. + * @param InMaxRange Max range for this trace. + * @param InBaseSpread Base targeting spread in degrees. + * @param InAimingSpreadMod Optional. Multiplicative modifier to spread if aiming. + * @param InTargetingSpreadIncrement Amount spread increments from continuous targeting in degrees. + * @param InTargetingSpreadMax Maximum amount of spread for continuous targeting in degrees. + * @param InMaxHitResultsPerTrace Max hit results that a trace can return. < 1 just returns the trace end point. + * @param InNumberOfTraces Number of traces to perform. Intended to be used with BaseSpread for multi-shot weapons + * like shotguns. Not intended to be used with PersistentHitsResults. If using PersistentHitResults, NumberOfTraces is + * hardcoded to 1. You will need to add support for this in your project if you need it. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + void Configure( + UPARAM(DisplayName = "Start Location") const FGameplayAbilityTargetingLocationInfo& InStartLocation, + UPARAM(DisplayName = "Aiming Tag") FGameplayTag InAimingTag, + UPARAM(DisplayName = "Aiming Removal Tag") FGameplayTag InAimingRemovalTag, + UPARAM(DisplayName = "Trace Profile") FCollisionProfileName InTraceProfile, + UPARAM(DisplayName = "Filter") FGameplayTargetDataFilterHandle InFilter, + UPARAM(DisplayName = "Reticle Class") TSubclassOf InReticleClass, + UPARAM(DisplayName = "Reticle Params") FWorldReticleParameters InReticleParams, + UPARAM(DisplayName = "Ignore Blocking Hits") bool bInIgnoreBlockingHits = false, + UPARAM(DisplayName = "Should Produce Target Data on Server") bool bInShouldProduceTargetDataOnServer = false, + UPARAM(DisplayName = "Use Persistent Hit Results") bool bInUsePersistentHitResults = false, + UPARAM(DisplayName = "Debug") bool bInDebug = false, + UPARAM(DisplayName = "Trace Affects Aim Pitch") bool bInTraceAffectsAimPitch = true, + UPARAM(DisplayName = "Trace From Player ViewPoint") bool bInTraceFromPlayerViewPoint = false, + UPARAM(DisplayName = "Use Aiming Spread Mod") bool bInUseAimingSpreadMod = false, + UPARAM(DisplayName = "Max Range") float InMaxRange = 999999.0f, + UPARAM(DisplayName = "Base Targeting Spread") float InBaseSpread = 0.0f, + UPARAM(DisplayName = "Aiming Spread Mod") float InAimingSpreadMod = 0.0f, + UPARAM(DisplayName = "Targeting Spread Increment") float InTargetingSpreadIncrement = 0.0f, + UPARAM(DisplayName = "Targeting Spread Max") float InTargetingSpreadMax = 0.0f, + UPARAM(DisplayName = "Max Hit Results Per Trace") int32 InMaxHitResultsPerTrace = 1, + UPARAM(DisplayName = "Number of Traces") int32 InNumberOfTraces = 1 + ); + +protected: + virtual void DoTrace(TArray& HitResults, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, FName ProfileName, + const FCollisionQueryParams Params) override; + virtual void ShowDebugTrace(TArray& HitResults, EDrawDebugTrace::Type DrawDebugType, float Duration = 2.0f) override; + +#if ENABLE_DRAW_DEBUG + // Util for drawing result of multi line trace from KismetTraceUtils.h + void DrawDebugLineTraceMulti(const UWorld* World, const FVector& Start, const FVector& End, EDrawDebugTrace::Type DrawDebugType, bool bHit, const TArray& OutHits, + FLinearColor TraceColor, FLinearColor TraceHitColor, float DrawTime); +#endif // ENABLE_DRAW_DEBUG +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_SphereTrace.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_SphereTrace.h new file mode 100644 index 0000000..455a92d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_SphereTrace.h @@ -0,0 +1,105 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilityTargetActor_Trace.h" +#include "DrawDebugHelpers.h" +#include "GGA_AbilityTargetActor_SphereTrace.generated.h" + +/** + * 可重用、配置的球形检测目标捕获Actor。 + * 与UETA_WaitTargetDataUsingActor配合使用。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_AbilityTargetActor_SphereTrace : public AGGA_AbilityTargetActor_Trace +{ + GENERATED_BODY() + +public: + AGGA_AbilityTargetActor_SphereTrace(); + + /**球形检测半径*/ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + float TraceSphereRadius; + + /** + * Configure the TargetActor for use. This TargetActor could be used in multiple abilities and there's no guarantee + * what state it will be in. You will need to make sure that only one ability is using this TargetActor at a time. + * + * @param InStartLocation Location to trace from. + * @param InAimingTag Optional. Predicted GameplayTag for aiming. Only used if we mofify spread while aiming. If used, + * must set InAimingRemovalTag also. + * @param InAimingRemovalTag Optional. Predicted GameplayTag for aiming removal. Only used if we mofify spread while + * aiming. If used, must set InAimingTag also. + * @param InTraceProfile Collision profile to use for tracing. + * @param InFilter Hit Actors must pass this filter to be returned in the TargetData. + * @param InReticleClass Reticle that will appear on top of acquired targets. Reticles will be spawned/despawned as targets are acquired/lost. + * @param InReticleParams Parameters for world reticle. Usage of these parameters is dependent on the reticle. + * @param bInIgnoreBlockingHits Ignore blocking collision hits in the trace. Useful if you want to target through walls. + * @param bInShouldProduceTargetDataOnServer If set, this TargetActor will produce TargetData on the Server in addition + * to the client and the client will just send a generic "Confirm" event to the server. If false, the client will send + * the TargetData to the Server. This is handled by the WaitTargetDataUsingActor AbilityTask. + * @param bInUsePersistentHitResults Should HitResults persist while targeting? HitResults are cleared on Confirm/Cancel or + * when new HitResults take their place. + * @param bInDebug When true, this TargetActor will show debug lines of the trace and hit results. + * @param bInTraceAffectsAimPitch Does the trace affect the aiming pitch? + * @param bInTraceFromPlayerViewPoint Should we trace from the player ViewPoint instead of the StartLocation? The + * TargetData HitResults will still have the StartLocation for the TraceStart. This is useful for FPS where we want + * to trace from the player ViewPoint but draw the bullet tracer from the weapon muzzle. + * TODO: AI Controllers should fall back to muzzle location. Not implemented yet. + * @param bInUseAImingSpreadMod Should we modify spread based on if we're aiming? If true, must set InAimingTag and + * InAimingRemovalTag. + * @param InMaxRange Max range for this trace. + * @param InTraceSphereRadius Radius for the sphere trace. + * @param InBaseSpread Base targeting spread in degrees. + * @param InAimingSpreadMod Optional. Multiplicative modifier to spread if aiming. + * @param InTargetingSpreadIncrement Amount spread increments from continuous targeting in degrees. + * @param InTargetingSpreadMax Maximum amount of spread for continuous targeting in degrees. + * @param InMaxHitResultsPerTrace Max hit results that a trace can return. < 1 just returns the trace end point. + * @param InNumberOfTraces Number of traces to perform. Intended to be used with BaseSpread for multi-shot weapons + * like shotguns. Not intended to be used with PersistentHitsResults. If using PersistentHitResults, NumberOfTraces is + * hardcoded to 1. You will need to add support for this in your project if you need it. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + void Configure( + UPARAM(DisplayName = "Start Location") const FGameplayAbilityTargetingLocationInfo& InStartLocation, + UPARAM(DisplayName = "Aiming Tag") FGameplayTag InAimingTag, + UPARAM(DisplayName = "Aiming Removal Tag") FGameplayTag InAimingRemovalTag, + UPARAM(DisplayName = "Trace Profile") FCollisionProfileName InTraceProfile, + UPARAM(DisplayName = "Filter") FGameplayTargetDataFilterHandle InFilter, + UPARAM(DisplayName = "Reticle Class") TSubclassOf InReticleClass, + UPARAM(DisplayName = "Reticle Params") FWorldReticleParameters InReticleParams, + UPARAM(DisplayName = "Ignore Blocking Hits") bool bInIgnoreBlockingHits = false, + UPARAM(DisplayName = "Should Produce Target Data on Server") bool bInShouldProduceTargetDataOnServer = false, + UPARAM(DisplayName = "Use Persistent Hit Results") bool bInUsePersistentHitResults = false, + UPARAM(DisplayName = "Debug") bool bInDebug = false, + UPARAM(DisplayName = "Trace Affects Aim Pitch") bool bInTraceAffectsAimPitch = true, + UPARAM(DisplayName = "Trace From Player ViewPoint") bool bInTraceFromPlayerViewPoint = false, + UPARAM(DisplayName = "Use Aiming Spread Mod") bool bInUseAimingSpreadMod = false, + UPARAM(DisplayName = "Max Range") float InMaxRange = 999999.0f, + UPARAM(DisplayName = "Trace Sphere Radius") float InTraceSphereRadius = 100.0f, + UPARAM(DisplayName = "Base Targeting Spread") float InBaseSpread = 0.0f, + UPARAM(DisplayName = "Aiming Spread Mod") float InAimingSpreadMod = 0.0f, + UPARAM(DisplayName = "Targeting Spread Increment") float InTargetingSpreadIncrement = 0.0f, + UPARAM(DisplayName = "Targeting Spread Max") float InTargetingSpreadMax = 0.0f, + UPARAM(DisplayName = "Max Hit Results Per Trace") int32 InMaxHitResultsPerTrace = 1, + UPARAM(DisplayName = "Number of Traces") int32 InNumberOfTraces = 1 + ); + + virtual void SphereTraceWithFilter(TArray& OutHitResults, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, + float Radius, FName ProfileName, const FCollisionQueryParams Params); + +protected: + virtual void DoTrace(TArray& HitResults, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, FName ProfileName, + const FCollisionQueryParams Params) override; + virtual void ShowDebugTrace(TArray& HitResults, EDrawDebugTrace::Type DrawDebugType, float Duration = 2.0f) override; + +#if ENABLE_DRAW_DEBUG + // Utils for drawing result of multi line trace from KismetTraceUtils.h + void DrawDebugSweptSphere(const UWorld* InWorld, FVector const& Start, FVector const& End, float Radius, FColor const& Color, bool bPersistentLines = false, float LifeTime = -1.f, + uint8 DepthPriority = 0); + void DrawDebugSphereTraceMulti(const UWorld* World, const FVector& Start, const FVector& End, float Radius, EDrawDebugTrace::Type DrawDebugType, bool bHit, const TArray& OutHits, + FLinearColor TraceColor, FLinearColor TraceHitColor, float DrawTime); +#endif // ENABLE_DRAW_DEBUG +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_Trace.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_Trace.h new file mode 100644 index 0000000..e532490 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetActors/GGA_AbilityTargetActor_Trace.h @@ -0,0 +1,163 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTargetActor.h" +#include "CollisionQueryParams.h" +#include "Engine/CollisionProfile.h" +#include "Kismet/KismetSystemLibrary.h" +#include "GGA_AbilityTargetActor_Trace.generated.h" + +/** + * 可重用、配置的目标捕获Actor。子类继承实现新的检测形状。 + * 与WaitTargetDataWithResuableActor配合使用。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API AGGA_AbilityTargetActor_Trace : public AGameplayAbilityTargetActor +{ + GENERATED_BODY() + +public: + AGGA_AbilityTargetActor_Trace(); + + // 基本瞄准扩散(角度) + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + float BaseSpread; + + // 瞄准扩散修改器 + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + float AimingSpreadMod; + + // 连续瞄准: 扩散增量 + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + float TargetingSpreadIncrement; + + // 连续瞄准: 最大增量 + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + float TargetingSpreadMax; + + // 连续瞄准的当前扩散 + float CurrentTargetingSpread; + + /** 是否使用瞄准扩散,开启后,检测方向会产生随机扩散(轻微改变检测方向) */ + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + bool bUseAimingSpreadMod; + + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + FGameplayTag AimingTag; + + UPROPERTY(BlueprintReadWrite, Category = "GGA|TargetActor") + FGameplayTag AimingRemovalTag; + + /** 最大范围 */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + float MaxRange; + + /** 检测预设 */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, config, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + FCollisionProfileName TraceProfile; + + /** 检测是否影响瞄准偏移(沿Y轴的旋转) */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + bool bTraceAffectsAimPitch; + + /** 每次检测所返回的最大碰撞结果数量。0表示只返回检测终点。 */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + int32 MaxHitResultsPerTrace; + + /** 一次性检测的次数,单发射击武器(如来福枪)只会进行一次检测,而多发射击武器(如散弹枪)可以进行多次检测。 + * 不可与PersistentHits配合使用。 + * 会影响十字线Actor的数量 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + int32 NumberOfTraces; + + /**是否忽略阻挡Hits*/ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + bool bIgnoreBlockingHits; + + /**是否从玩家控制器的视角开始检测,否则从StartLocation开始检测*/ + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + bool bTraceFromPlayerViewPoint; + + // HitResults will persist until Confirmation/Cancellation or until a new HitResult takes its place + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = "GGA|TargetActor") + bool bUsePersistentHitResults; + + /**重置扩散 */ + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + virtual void ResetSpread(); + + virtual float GetCurrentSpread() const; + + // 设置开始位置信息 + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + void SetStartLocation(const FGameplayAbilityTargetingLocationInfo& InStartLocation); + + // 是否在服务端产生目标数据 + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + virtual void SetShouldProduceTargetDataOnServer(bool bInShouldProduceTargetDataOnServer); + + // 设置当玩家确认目标后是否销毁此TargetingActor。 + UFUNCTION(BlueprintCallable, Category = "GGA|TargetActor") + void SetDestroyOnConfirmation(bool bInDestroyOnConfirmation = false); + + virtual void StartTargeting(UGameplayAbility* Ability) override; + + virtual void ConfirmTargetingAndContinue() override; + + virtual void CancelTargeting() override; + + virtual void BeginPlay() override; + + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + virtual void Tick(float DeltaSeconds) override; + + // Traces as normal, but will manually filter all hit actors + virtual void LineTraceWithFilter(TArray& OutHitResults, const UWorld* World, + const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, + const FVector& End, FName ProfileName, const FCollisionQueryParams Params); + + virtual void AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, + const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch = false); + + virtual bool ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, + float AbilityRange, FVector& ClippedPosition); + + virtual void StopTargeting(); + +protected: + // 检测终点, useful for debug drawing + FVector CurrentTraceEnd; + + // 对准心Actor的引用 + TArray> ReticleActors; + TArray PersistentHitResults; + + TArray CurrentHitResults; + + virtual FGameplayAbilityTargetDataHandle MakeTargetData(const TArray& HitResults) const; + virtual TArray PerformTrace(AActor* InSourceActor); + + + //实际的检测,由子类覆写。 + virtual void DoTrace(TArray& HitResults, const UWorld* World, + const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, + FName ProfileName, const FCollisionQueryParams Params) PURE_VIRTUAL(AUETA_Trace, return;); + + virtual void ShowDebugTrace(TArray& HitResults, EDrawDebugTrace::Type DrawDebugType, + float Duration = 2.0f) PURE_VIRTUAL(AUETA_Trace, return;); + + /** 生成准心Actor */ + virtual AGameplayAbilityWorldReticle* SpawnReticleActor(FVector Location, FRotator Rotation); + + /**销毁所有准心Actor */ + virtual void DestroyReticleActors(); + +public: + /**获取当前的hitresult */ + UFUNCTION(BlueprintPure, Category = "GGA|TargetActor") + void GetCurrentHitResult(TArray& HitResults); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType.h new file mode 100644 index 0000000..40bc84a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType.h @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "GGA_TargetType.generated.h" + +/** +* Deprecated +*/ +UCLASS(Blueprintable, meta = (ShowWorldContextPin)) +class GENERICGAMEPLAYABILITIES_API UGGA_TargetType : public UObject +{ + GENERATED_BODY() + +public: + // Constructor and overrides + UGGA_TargetType() + { + } + + /** Called to determine targets to apply gameplay effects to */ + UFUNCTION(BlueprintNativeEvent) + void GetTargets(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const; + virtual void GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseEventData.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseEventData.h new file mode 100644 index 0000000..4ad233f --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseEventData.h @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_TargetType.h" +#include "GGA_TargetType_UseEventData.generated.h" + +/** Trivial target type that pulls the target out of the event data */ +UCLASS(NotBlueprintable) +class GENERICGAMEPLAYABILITIES_API UGGA_TargetType_UseEventData : public UGGA_TargetType +{ + GENERATED_BODY() +public: + // Constructor and overrides + UGGA_TargetType_UseEventData() {} + + /** Uses the passed in event data */ + virtual void GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseOwner.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseOwner.h new file mode 100644 index 0000000..8e0ceea --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/TargetTypes/GGA_TargetType_UseOwner.h @@ -0,0 +1,23 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_TargetType.h" +#include "GGA_TargetType_UseOwner.generated.h" + +/** Trivial target type that uses the owner */ +UCLASS(NotBlueprintable) +class GENERICGAMEPLAYABILITIES_API UGGA_TargetType_UseOwner : public UGGA_TargetType +{ + GENERATED_BODY() + +public: + // Constructor and overrides + UGGA_TargetType_UseOwner() + { + } + + /** Uses the passed in event data */ + virtual void GetTargets_Implementation(AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_AbilitySystemFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_AbilitySystemFunctionLibrary.h new file mode 100644 index 0000000..0af53aa --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_AbilitySystemFunctionLibrary.h @@ -0,0 +1,540 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "AbilitySystemComponent.h" +#include "GameplayEffectExecutionCalculation.h" +#include "GGA_AbilitySystemComponent.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "GGA_AbilitySystemFunctionLibrary.generated.h" + +/** + * Blueprint function library for ability system operations. + * 用于技能系统操作的蓝图函数库。 + * @details Provides utility functions for managing ability system components, abilities, and attributes. + * @细节 提供管理技能系统组件、技能和属性的实用函数。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_AbilitySystemFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: +#pragma region AbilitySystem + /** + * Finds an ability system component on an actor. + * 在演员上查找技能系统组件。 + * @param Actor The actor to search. 要搜索的演员。 + * @param DesiredClass The desired class of the component. 所需的组件类。 + * @param ASC The found ability system component (output). 找到的技能系统组件(输出)。 + * @return True if the component was found, false otherwise. 如果找到组件则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem", + meta=(DisplayName="Find Typed Ability System Component", DefaultToSelf="Actor", DynamicOutputParam="ASC", DeterminesOutputType="DesiredClass", ExpandBoolAsExecs="ReturnValue")) + static bool FindAbilitySystemComponent(AActor* Actor, TSubclassOf DesiredClass, UAbilitySystemComponent*& ASC); + + /** + * Retrieves an ability system component from an actor. + * 从演员获取技能系统组件。 + * @param Actor The actor to search. 要搜索的演员。 + * @param DesiredClass The desired class of the component. 所需的组件类。 + * @return The found ability system component, or nullptr if not found. 找到的技能系统组件,未找到时为nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem", + meta=(DisplayName="Get Typed Ability System Component", DefaultToSelf="Actor", DynamicOutputParam="ReturnValue", DeterminesOutputType="DesiredClass")) + static UAbilitySystemComponent* GetAbilitySystemComponent(AActor* Actor, TSubclassOf DesiredClass); + + /** + * Initializes the actor info for an ability system component. + * 初始化技能系统组件的Actor信息。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param InOwnerActor The owner actor. 拥有者演员。 + * @param InAvatarActor The avatar actor. 化身演员。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void InitAbilityActorInfo(UAbilitySystemComponent* AbilitySystem, AActor* InOwnerActor, AActor* InAvatarActor); + + /** + * Retrieves the owner actor of an ability system component. + * 获取技能系统组件的拥有者演员。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @return The owner actor. 拥有者演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static AActor* GetOwnerActor(UAbilitySystemComponent* AbilitySystem); + + /** + * Retrieves the avatar actor of an ability system component. + * 获取技能系统组件的化身演员。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @return The avatar actor. 化身演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static AActor* GetAvatarActor(UAbilitySystemComponent* AbilitySystem); + + /** + * Handles a gameplay event on an ability system component. + * 在技能系统组件上处理游戏事件。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param EventTag The event tag. 事件标签。 + * @param Payload The event data payload. 事件数据负载。 + * @return The number of successful ability activations. 成功激活的技能数量。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static int32 HandleGameplayEvent(UAbilitySystemComponent* AbilitySystem, FGameplayTag EventTag, const FGameplayEventData& Payload); + + /** + * Attempts to activate abilities one by one. + * 尝试逐一激活技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param AbilitiesToActivate Array of abilities to activate. 要激活的技能数组。 + * @param bAllowRemoteActivation Whether to allow remote activation. 是否允许远程激活。 + * @param bFirstOnly Whether to stop after the first successful activation. 是否在第一次成功激活后停止。 + * @return True if at least one ability was activated, false otherwise. 如果至少激活一个技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem", meta=(DisplayName="Try Activate Abilities(one by one)")) + static bool TryActivateAbilities(UAbilitySystemComponent* AbilitySystem, TArray AbilitiesToActivate, bool bAllowRemoteActivation = true, bool bFirstOnly = true); + + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + bool HasActivatableTriggeredAbility(UAbilitySystemComponent* AbilitySystem, FGameplayTag Tag); + + /** + * Retrieves activatable ability specs matching all tags. + * 获取与所有标签匹配的可激活技能规格。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Tags The tags to match. 要匹配的标签。 + * @param MatchingGameplayAbilities Array to store matching ability specs (output). 存储匹配技能规格的数组(输出)。 + * @param bOnlyAbilitiesThatSatisfyTagRequirements Whether to include only abilities satisfying tag requirements. 是否仅包括满足标签要求的技能。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void GetActivatableGameplayAbilitySpecsByAllMatchingTags(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, + TArray& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true); + + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void GetActivatableGameplayAbilitySpecs(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, const UObject* SourceObject, + TArray& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true); + + /** + * Retrieves the first activatable ability matching all tags. + * 获取与所有标签匹配的第一个可激活技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Tags The tags to match. 要匹配的标签。 + * @param MatchingGameplayAbility The matching ability spec handle (output). 匹配的技能句柄(输出)。 + * @param bOnlyAbilitiesThatSatisfyTagRequirements Whether to include only abilities satisfying tag requirements. 是否仅包括满足标签要求的技能。 + * @return True if a matching ability was found, false otherwise. 如果找到匹配技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool GetFirstActivatableAbilityByAllMatchingTags(const UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer Tags, FGameplayAbilitySpecHandle& MatchingGameplayAbility, + bool bOnlyAbilitiesThatSatisfyTagRequirements = true); + + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool GetFirstActivatableAbility(const UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer Tags, FGameplayAbilitySpecHandle& MatchingGameplayAbility, const UObject* SourceObject, + bool bOnlyAbilitiesThatSatisfyTagRequirements = true); + + /** + * Retrieves active ability instances matching the specified tags. + * 获取与指定标签匹配的激活技能实例。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Tags The tags to match. 要匹配的标签。 + * @param MatchingAbilityInstances Array to store matching ability instances. 存储匹配技能实例的数组。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void GetActiveAbilityInstancesWithTags(const UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& Tags, TArray& MatchingAbilityInstances); + + /** + * Sends a gameplay event to an actor. + * 向演员发送游戏事件。 + * @param Actor The actor to send the event to. 要发送事件的演员。 + * @param EventTag The event tag. 事件标签。 + * @param Payload The event data payload. 事件数据负载。 + * @return True if any abilities were activated, false otherwise. 如果激活了任何技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem", Meta = (Tooltip = "This function can be used to trigger an ability on the actor in question with useful payload data.")) + static bool SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload); + + /** + * Breaks down ability ended data into its components. + * 将技能结束数据分解为其组件。 + * @param AbilityEndedData The ability ended data. 技能结束数据。 + * @param AbilityThatEnded The ability that ended (output). 结束的技能(输出)。 + * @param AbilitySpecHandle The ability spec handle (output). 技能句柄(输出)。 + * @param bReplicateEndAbility Whether to replicate the end ability (output). 是否复制结束技能(输出)。 + * @param bWasCancelled Whether the ability was cancelled (output). 技能是否被取消(输出)。 + */ + UFUNCTION(BlueprintPure, Category = "GGA|AbilitySystem") + static void BreakAbilityEndedData(const FAbilityEndedData& AbilityEndedData, UGameplayAbility*& AbilityThatEnded, FGameplayAbilitySpecHandle& AbilitySpecHandle, bool& bReplicateEndAbility, + bool& bWasCancelled); + + /** + * Finds all abilities matching the provided tags in order. + * 按顺序查找与提供的标签匹配的所有技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Tags The tags to match. 要匹配的标签。 + * @param OutAbilityHandles Array to store matching ability handles (output). 存储匹配技能句柄的数组(输出)。 + * @param bExactMatch Whether to require an exact tag match. 是否要求完全标签匹配。 + * @return True if any abilities were found, false otherwise. 如果找到任何技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool FindAllAbilitiesWithTagsInOrder(const UAbilitySystemComponent* AbilitySystem, TArray Tags, TArray& OutAbilityHandles, + bool bExactMatch = true); + + /** + * Finds the first ability matching a gameplay tag query. + * 查找与游戏标签查询匹配的第一个技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param OutAbilityHandle The matching ability handle (output). 匹配的技能句柄(输出)。 + * @param Query The gameplay tag query. 游戏标签查询。 + * @param SourceObject Optional source object filter. 可选的源对象过滤(只会返回源对象与之匹配的Ability)。 + * @return True if a matching ability was found, false otherwise. 如果找到匹配技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool FindAbilityMatchingQuery(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, FGameplayTagQuery Query, const UObject* SourceObject = nullptr); + + static FGameplayAbilitySpec* FindAbilitySpecFromClass(const UAbilitySystemComponent* AbilitySystem, TSubclassOf AbilityClass, const UObject* SourceObject = nullptr); + + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool FindAbilityFromClass(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, TSubclassOf AbilityClass, + const UObject* SourceObject = nullptr); + + /** + * Finds the first ability matching the provided tags. + * 查找与提供的标签匹配的第一个技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param OutAbilityHandle The matching ability handle (output). 匹配的技能句柄(输出)。 + * @param Tags The tags to match. 要匹配的标签。 + * @param bExactMatch Whether to require an exact tag match. 是否要求完全标签匹配。 + * @param SourceObject Optional source object filter. 可选的源对象过滤(只会返回源对象与之匹配的Ability)。 + * @return True if a matching ability was found, false otherwise. 如果找到匹配技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool FindAbilityWithTags(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle& OutAbilityHandle, FGameplayTagContainer Tags, bool bExactMatch = true, + const UObject* SourceObject = nullptr); + + /** + * Adds a non-replicated gameplay tag to an ability system component. + * 向技能系统组件添加非复制的游戏标签。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param GameplayTag The tag to add. 要添加的标签。 + * @param Count The number of times to add the tag. 添加标签的次数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void AddLooseGameplayTag(UAbilitySystemComponent* AbilitySystem, const FGameplayTag& GameplayTag, int32 Count = 1); + + /** + * Adds multiple non-replicated gameplay tags to an ability system component. + * 向技能系统组件添加多个非复制的游戏标签。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param GameplayTags The tags to add. 要添加的标签。 + * @param Count The number of times to add the tags. 添加标签的次数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void AddLooseGameplayTags(UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& GameplayTags, int32 Count = 1); + + /** + * Removes a non-replicated gameplay tag from an ability system component. + * 从技能系统组件移除非复制的游戏标签。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param GameplayTag The tag to remove. 要移除的标签。 + * @param Count The number of times to remove the tag. 移除标签的次数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void RemoveLooseGameplayTag(UAbilitySystemComponent* AbilitySystem, const FGameplayTag& GameplayTag, int32 Count = 1); + + /** + * Removes multiple non-replicated gameplay tags from an ability system component. + * 从技能系统组件移除多个非复制的游戏标签。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param GameplayTags The tags to remove. 要移除的标签。 + * @param Count The number of times to remove the tags. 移除标签的次数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void RemoveLooseGameplayTags(UAbilitySystemComponent* AbilitySystem, const FGameplayTagContainer& GameplayTags, int32 Count = 1); + + /** + * Removes all gameplay cues from an ability system component. + * 从技能系统组件移除所有游戏反馈。 + * @param AbilitySystem The ability system component. 技能系统组件。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void RemoveAllGameplayCues(UAbilitySystemComponent* AbilitySystem); + +#pragma endregion AbilitySystem + +#pragma region AnimMontage Support + + /** Plays a montage and handles replication and prediction based on passed in ability/activation info */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static float PlayMontage(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* AnimatingAbility, FGameplayAbilityActivationInfo ActivationInfo, UAnimMontage* Montage, float InPlayRate, + FName StartSectionName = NAME_None, + float StartTimeSeconds = 0.0f); + + /** Stops whatever montage is currently playing. Expectation is caller should only be stopping it if they are the current animating ability (or have good reason not to check) */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CurrentMontageStop(UAbilitySystemComponent* AbilitySystem, float OverrideBlendOutTime = -1.0f); + + /** Stops current montage if it's the one given as the Montage param */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void StopMontageIfCurrent(UAbilitySystemComponent* AbilitySystem, const UAnimMontage* Montage, float OverrideBlendOutTime = -1.0f); + + /** Clear the animating ability that is passed in, if it's still currently animating */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void ClearAnimatingAbility(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* Ability); + + /** Jumps current montage to given section. Expectation is caller should only be stopping it if they are the current animating ability (or have good reason not to check) */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CurrentMontageJumpToSection(UAbilitySystemComponent* AbilitySystem, FName SectionName); + + /** Sets current montages next section name. Expectation is caller should only be stopping it if they are the current animating ability (or have good reason not to check) */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CurrentMontageSetNextSectionName(UAbilitySystemComponent* AbilitySystem, FName FromSectionName, FName ToSectionName); + + /** Sets current montage's play rate */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CurrentMontageSetPlayRate(UAbilitySystemComponent* AbilitySystem, float InPlayRate); + + /** + * Checks if the specified ability is the current animating ability. + * 检查指定技能是否为当前播放动画的技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability to check. 要检查的技能。 + * @return True if the ability is animating, false otherwise. 如果技能正在播放动画则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static bool IsAnimatingAbility(UAbilitySystemComponent* AbilitySystem, UGameplayAbility* Ability); + + /** + * Retrieves the ability instance playing the specified animation. + * 获取播放指定动画的技能实例。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @return The ability instance, or nullptr if not found. 技能实例,未找到时为nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static UGameplayAbility* GetAnimatingAbility(UAbilitySystemComponent* AbilitySystem); + + /** + * Retrieves the ability instance playing the specified animation on an actor. + * 获取在演员上播放指定动画的技能实例。 + * @param Actor The actor to search. 要搜索的演员。 + * @param Animation The animation to check. 要检查的动画。 + * @return The ability instance, or nullptr if not found. 技能实例,未找到时为nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem", meta=(DefaultToSelf="Actor")) + static UGameplayAbility* GetAnimatingAbilityFromActor(AActor* Actor, UAnimSequenceBase* Animation); + + /** + * Finds the ability instance playing the specified animation with a desired class. + * 查找播放指定动画且具有所需类的技能实例。 + * @param Actor The actor to search. 要搜索的演员。 + * @param Animation The animation to check. 要检查的动画。 + * @param DesiredClass The desired ability class. 所需的技能类。 + * @param AbilityInstance The found ability instance. 找到的技能实例。 + * @return True if an ability was found, false otherwise. 如果找到技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem", + meta=(DefaultToSelf="Actor", DisplayName="Get Animating Ability from Actor", DeterminesOutputType=DesiredClass, DynamicOutputParam=AbilityInstance, ExpandBoolAsExecs=ReturnValue)) + static bool FindAnimatingAbilityFromActor(AActor* Actor, UAnimSequenceBase* Animation, TSubclassOf DesiredClass, UGameplayAbility*& AbilityInstance); + + /** Returns montage that is currently playing */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static UAnimMontage* GetCurrentMontage(UAbilitySystemComponent* AbilitySystem); + + /** Get SectionID of currently playing AnimMontage */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static int32 GetCurrentMontageSectionID(UAbilitySystemComponent* AbilitySystem); + + /** Get SectionName of currently playing AnimMontage */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static FName GetCurrentMontageSectionName(UAbilitySystemComponent* AbilitySystem); + + /** Get length in time of current section */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static float GetCurrentMontageSectionLength(UAbilitySystemComponent* AbilitySystem); + + /** Returns amount of time left in current section */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static float GetCurrentMontageSectionTimeLeft(UAbilitySystemComponent* AbilitySystem); + + +#pragma endregion + +#pragma region Ability + + /** + * Sets the input pressed state for an ability. + * 为技能设置输入按下状态。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void SetAbilityInputPressed(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Sets the input released state for an ability. + * 为技能设置输入释放状态。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void SetAbilityInputReleased(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Checks if an ability can be activated. + * 检查技能是否可以激活。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param AbilityToActivate The ability to activate. 要激活的技能。 + * @param RelevantTags Tags relevant to the activation check (output). 与激活检查相关的标签(输出)。 + * @return True if the ability can be activated, false otherwise. 如果技能可以激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool CanActivateAbility(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle AbilityToActivate, FGameplayTagContainer& RelevantTags); + + /** + * Selects the first ability that can be activated from a list. + * 从列表中选择第一个可以激活的技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Abilities Array of abilities to check. 要检查的技能数组。 + * @param OutAbilityHandle The selected ability handle (output). 选中的技能句柄(输出)。 + * @return True if an ability was selected, false otherwise. 如果选中技能则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + static bool SelectFirstCanActivateAbility(const UAbilitySystemComponent* AbilitySystem, TArray Abilities, FGameplayAbilitySpecHandle& OutAbilityHandle); + + /** + * Cancels an ability. + * 取消一个技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle to cancel. 要取消的技能句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CancelAbility(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Cancels abilities based on tags. + * 根据标签取消技能。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param WithTags Tags to include for cancellation. 要包含的标签。 + * @param WithoutTags Tags to exclude from cancellation. 要排除的标签。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|AbilitySystem") + static void CancelAbilities(UAbilitySystemComponent* AbilitySystem, FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags); + + /** + * Checks if an ability's primary instance is active. + * 检查技能的主要实例是否激活。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return True if the ability instance is active, false otherwise. 如果技能实例激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem", meta=(DisplayName="Is Ability Primary Instance Active")) + static bool IsAbilityInstanceActive(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves all ability instances from ability spec, including replicated and non-replicated instances. + * 从技能规格获取所有技能实例(含复制的和未网络复制的)。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return Array of spec's replicated and non-replicated ability instances. 复制以及非复制技能实例的组合。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static TArray GetAbilityInstances(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the ability definition from ability spec. + * 从技能规格获取技能定义。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The ability CDO. 技能CDO。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static const UGameplayAbility* GetAbilityCDO(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the level from ability spec. + * 从技能规格获取技能的等级。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The ability level. 技能等级。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static int32 GetAbilityLevel(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the input ID from ability spec, if bound. + * 从技能规格获取技能的输入ID(如果已绑定)。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The input ID. 输入ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static int32 GetAbilityInputId(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the source object from ability spec. + * 从技能规格获取技能的源对象。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The source object. 源对象。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static UObject* GetAbilitySourceObject(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the dynamic tags from ability spec. + * 从技能规格获取技能的动态标签。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The dynamic tag container. 动态标签容器。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static FGameplayTagContainer GetAbilityDynamicTags(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Retrieves the primary instance from ability spec. + * 从技能规格获取技能的主要实例。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return The primary ability instance. 主要技能实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static UGameplayAbility* GetAbilityPrimaryInstance(UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + + /** + * Checks if an ability spec‘s active count > 0. + * 检查技能规格的激活计数是否>0。 + * @attention Only available on server side,The spec's active acount are not replicated! 仅在服务端有效,实例的ActiveCount未网络同步。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param Ability The ability spec handle. 技能句柄。 + * @return True if the ability is active, false otherwise. 如果技能激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static bool IsAbilityActive(const UAbilitySystemComponent* AbilitySystem, FGameplayAbilitySpecHandle Ability); + +#pragma endregion Ability + +#pragma region AttributeSet + /** + * Finds an attribute set by class. + * 通过类查找属性集。 + * @param AbilitySystem The ability system component. 技能系统组件。 + * @param AttributeSetClass The attribute set class to find. 要查找的属性集类。 + * @return The found attribute set, or nullptr if not found. 找到的属性集,未找到时为nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|AbilitySystem") + static UAttributeSet* GetAttributeSetByClass(const UAbilitySystemComponent* AbilitySystem, const TSubclassOf AttributeSetClass); +#pragma endregion + +#pragma region ScalableFloat + /** + * Retrieves the value of a scalable float at a specific level. + * 获取特定级别下可扩展浮点数的值。 + * @param ScalableFloat The scalable float. 可扩展浮点数。 + * @param Level The level to evaluate. 要评估的级别。 + * @param ContextString Context string for debugging. 用于调试的上下文字符串。 + * @return The evaluated value. 评估值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|Extensions|ScalableFloat") + static float GetValueAtLevel(const FScalableFloat& ScalableFloat, float Level, FString ContextString); +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityFunctionLibrary.h new file mode 100644 index 0000000..9ef201c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityFunctionLibrary.h @@ -0,0 +1,113 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayAbilitySpecHandle.h" +#include "GameplayTagContainer.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGA_GameplayAbilityFunctionLibrary.generated.h" + +class UGameplayAbility; + +/** + * Blueprint function library for gameplay ability operations. + * 用于游戏技能操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayAbilityFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Checks if an ability spec handle is valid. + * 检查技能规格句柄是否有效。 + * @param Handle The ability spec handle. 技能规格句柄。 + * @return True if the handle is valid, false otherwise. 如果句柄有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility") + static bool IsAbilitySpecHandleValid(FGameplayAbilitySpecHandle Handle); + + /** + * Retrieves the default object for an ability class. + * 获取技能类的默认对象。 + * @param AbilityClass The ability class. 技能类。 + * @return The default object for the ability class. 技能类的默认对象。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility") + static const UGameplayAbility* GetAbilityCDOFromClass(TSubclassOf AbilityClass); + + /** + * Retrieves the current ability spec handle. + * 获取当前技能规格句柄。 + * @param Ability The gameplay ability. 游戏技能。 + * @return The ability spec handle. 技能规格句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category= "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static FGameplayAbilitySpecHandle GetCurrentAbilitySpecHandle(const UGameplayAbility* Ability); + + /** + * Checks if an ability is currently active. + * 检查技能是否当前激活。 + * @param Ability The gameplay ability. 游戏技能。 + * @return True if the ability is active, false otherwise. 如果技能激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category= "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static bool IsAbilityActive(const UGameplayAbility* Ability); + + /** + * Retrieves the replication policy for an ability. + * 获取技能的复制策略。 + * @param Ability The gameplay ability. 游戏技能。 + * @return The replication policy. 复制策略。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category= "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static EGameplayAbilityReplicationPolicy::Type GetReplicationPolicy(const UGameplayAbility* Ability); + + /** + * Retrieves the instancing policy for an ability. + * 获取技能的实例化策略。 + * @param Ability The gameplay ability. 游戏技能。 + * @return The instancing policy. 实例化策略。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category= "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static EGameplayAbilityInstancingPolicy::Type GetInstancingPolicy(const UGameplayAbility* Ability); + + /** + * Retrieves the tags associated with an ability. + * 获取与技能关联的标签。 + * @param Ability The gameplay ability. 游戏技能。 + * @return The gameplay tag container. 游戏标签容器。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static FGameplayTagContainer GetAbilityTags(const UGameplayAbility* Ability); + + /** + * Checks if the ability is running on a predicting client. + * 检查技能是否在预测客户端上运行。 + * @param Ability The gameplay ability. 游戏技能。 + * @return True if running on a predicting client, false otherwise. 如果在预测客户端上运行则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static bool IsPredictingClient(const UGameplayAbility* Ability); + + /** + * Checks if the ability is for a remote client. + * 检查技能是否用于远程客户端。 + * @param Ability The gameplay ability. 游戏技能。 + * @return True if for a remote client, false otherwise. 如果用于远程客户端则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static bool IsForRemoteClient(const UGameplayAbility* Ability); + + /** + * Checks if the ability has authority or a valid prediction key. + * 检查技能是否具有权限或有效预测键。 + * @param Ability The gameplay ability. 游戏技能。 + * @return True if it has authority or a valid prediction key, false otherwise. 如果具有权限或有效预测键则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAbility", meta = (DefaultToSelf="Ability")) + static bool HasAuthorityOrPredictionKey(const UGameplayAbility* Ability); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.h new file mode 100644 index 0000000..c7e1c70 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayAbilityTargetDataFunctionLibrary.h @@ -0,0 +1,48 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/GameplayAbilityTargetTypes.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "StructUtils/InstancedStruct.h" +#include "GGA_GameplayAbilityTargetDataFunctionLibrary.generated.h" + +/** + * Blueprint function library for gameplay ability target data operations. + * 用于游戏技能目标数据操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayAbilityTargetDataFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|TargetData") + static FGameplayAbilityTargetDataHandle AbilityTargetDataFromPayload(const FInstancedStruct& Payload); + + /** Returns the hit result for a given index if it exists */ + UFUNCTION(BlueprintPure, Category = "Ability|TargetData") + static FInstancedStruct GetPayloadFromTargetData(const FGameplayAbilityTargetDataHandle& TargetData, int32 Index); + + /** + * Creates a target data handle from hit results. + * 从命中结果创建目标数据句柄。 + * @param HitResults Array of hit results. 命中结果数组。 + * @param OneTargetPerHandle Whether to create one target per handle. 是否每个句柄一个目标。 + * @return The target data handle. 目标数据句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|TargetData") + static FGameplayAbilityTargetDataHandle AbilityTargetDataFromHitResults(const TArray& HitResults, bool OneTargetPerHandle); + + /** + * Adds target data to an effect context. + * 将目标数据添加到效果上下文。 + * @param TargetData The target data to add. 要添加的目标数据。 + * @param EffectContext The effect context to modify. 要修改的效果上下文。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|TargetData") + static void AddTargetDataToContext(UPARAM(ref) + FGameplayAbilityTargetDataHandle TargetData, UPARAM(ref) + FGameplayEffectContextHandle EffectContext); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayCueFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayCueFunctionLibrary.h new file mode 100644 index 0000000..931a497 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayCueFunctionLibrary.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectTypes.h" +#include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGA_GameplayCueFunctionLibrary.generated.h" + +/** + * Blueprint function library for gameplay cue operations. + * 用于游戏反馈操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayCueFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Executes a local gameplay cue on an actor. + * 在演员上执行本地游戏反馈。 + * @param Actor The actor to execute the cue on. 要执行反馈的演员。 + * @param GameplayCueTag The gameplay cue tag. 游戏反馈标签。 + * @param GameplayCueParameters Parameters for the gameplay cue. 游戏反馈参数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayCue", Meta = (DefaultToSelf="Actor", AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue")) + static void ExecuteGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters); + + /** + * Adds a local gameplay cue to an actor. + * 向演员添加本地游戏反馈。 + * @param Actor The actor to add the cue to. 要添加反馈的演员。 + * @param GameplayCueTag The gameplay cue tag. 游戏反馈标签。 + * @param GameplayCueParameters Parameters for the gameplay cue. 游戏反馈参数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayCue", Meta = (DefaultToSelf="Actor", AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue")) + static void AddGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters); + + /** + * Removes a local gameplay cue from an actor. + * 从演员移除本地游戏反馈。 + * @param Actor The actor to remove the cue from. 要移除反馈的演员。 + * @param GameplayCueTag The gameplay cue tag. 游戏反馈标签。 + * @param GameplayCueParameters Parameters for the gameplay cue. 游戏反馈参数。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayCue", Meta = (DefaultToSelf="Actor", AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue")) + static void RemoveGameplayCueLocal(AActor* Actor, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.h new file mode 100644 index 0000000..4986d75 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectCalculationFunctionLibrary.h @@ -0,0 +1,233 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectExecutionCalculation.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGA_GameplayEffectCalculationFunctionLibrary.generated.h" + +/** + * Blueprint function library for gameplay effect calculation operations. + * 用于游戏效果计算操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayEffectCalculationFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Retrieves the owning gameplay effect spec. + * 获取拥有的游戏效果规格。 + * @param InParams The execution parameters. 执行参数。 + * @return The owning gameplay effect spec. 拥有的游戏效果规格。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static const FGameplayEffectSpec& GetOwningSpec(const FGameplayEffectCustomExecutionParameters& InParams); + + /** Simple accessor to the Passed In Tags to this execution */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static const FGameplayTagContainer& GetPassedInTags(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the effect context from execution parameters. + * 从执行参数获取效果上下文。 + * @param InParams The execution parameters. 执行参数。 + * @return The effect context handle. 效果上下文句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static FGameplayEffectContextHandle GetEffectContext(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the SetByCaller magnitude by tag from execution parameters. + * 从执行参数通过标签获取SetByCaller大小。 + * @param InParams The execution parameters. 执行参数。 + * @param Tag The tag to query. 要查询的标签。 + * @param WarnIfNotFound Whether to warn if not found. 如果未找到是否警告。 + * @param DefaultIfNotFound Default value if not found. 如果未找到的默认值。 + * @return The magnitude value, or default if not found. 大小值,未找到时返回默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|Calculation") + static float GetSetByCallerMagnitudeByTag(const FGameplayEffectCustomExecutionParameters& InParams, const FGameplayTag& Tag, bool WarnIfNotFound = false, float DefaultIfNotFound = 0.f); + + /** + * Retrieves the SetByCaller magnitude by name from execution parameters. + * 从执行参数通过名称获取SetByCaller大小。 + * @param InParams The execution parameters. 执行参数。 + * @param MagnitudeName The name to query. 要查询的名称。 + * @param WarnIfNotFound Whether to warn if not found. 如果未找到是否警告。 + * @param DefaultIfNotFound Default value if not found. 如果未找到的默认值。 + * @return The magnitude value, or default if not found. 大小值,未找到时返回默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GGA|Calculation") + static float GetSetByCallerMagnitudeByName(const FGameplayEffectCustomExecutionParameters& InParams, const FName& MagnitudeName, bool WarnIfNotFound = false, float DefaultIfNotFound = 0.f); + + /** + * Retrieves source aggregated tags from the owning spec. + * 从拥有规格获取源聚合标签。 + * @param InParams The execution parameters. 执行参数。 + * @return The source aggregated tags. 源聚合标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static FGameplayTagContainer GetSourceAggregatedTags(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves target aggregated tags from the owning spec. + * 从拥有规格获取目标聚合标签。 + * @param InParams The execution parameters. 执行参数。 + * @return The target aggregated tags. 目标聚合标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static FGameplayTagContainer GetTargetAggregatedTags(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the target ability system component. + * 获取目标技能系统组件。 + * @param InParams The execution parameters. 执行参数。 + * @return The target ability system component. 目标技能系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static UAbilitySystemComponent* GetTargetASC(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the target actor. + * 获取目标演员。 + * @param InParams The execution parameters. 执行参数。 + * @return The target actor. 目标演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static AActor* GetTargetActor(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the source ability system component. + * 获取源技能系统组件。 + * @param InParams The execution parameters. 执行参数。 + * @return The source ability system component. 源技能系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static UAbilitySystemComponent* GetSourceASC(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Retrieves the source actor. + * 获取源演员。 + * @param InParams The execution parameters. 执行参数。 + * @return The source actor. 源演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static AActor* GetSourceActor(const FGameplayEffectCustomExecutionParameters& InParams); + + /** + * Attempts to calculate the magnitude of a captured attribute. + * 尝试计算捕获属性的值。 + * @param InParams The execution parameters. 执行参数。 + * @param InAttributeCaptureDefinitions Attribute capture definitions. 属性捕获定义。 + * @param InAttribute The attribute to calculate. 要计算的属性。 + * @param OutMagnitude The calculated magnitude (output). 计算出的大小(输出)。 + * @return True if calculation was successful, false otherwise. 如果计算成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static bool AttemptCalculateCapturedAttributeMagnitude(const FGameplayEffectCustomExecutionParameters& InParams, TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, + float& OutMagnitude); + + /** + * Attempts to calculate the magnitude of a captured attribute with source and target tags. + * 尝试使用源和目标标签计算捕获属性的值。 + * @param InParams The execution parameters. 执行参数。 + * @param SourceTags Source tags for the calculation. 用于计算的源标签。 + * @param TargetTags Target tags for the calculation. 用于计算的目标标签。 + * @param InAttributeCaptureDefinitions Attribute capture definitions. 属性捕获定义。 + * @param InAttribute The attribute to calculate. 要计算的属性。 + * @param OutMagnitude The calculated magnitude (output). 计算出的大小(输出)。 + * @return True if calculation was successful, false otherwise. 如果计算成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static bool AttemptCalculateCapturedAttributeMagnitudeExt(const FGameplayEffectCustomExecutionParameters& InParams, const FGameplayTagContainer& SourceTags, + const FGameplayTagContainer& TargetTags, TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, + float& OutMagnitude); + + /** + * Attempts to calculate the magnitude of a captured attribute with a base value. + * 使用基础值尝试计算捕获属性的值。 + * @param InParams The execution parameters. 执行参数。 + * @param InAttributeCaptureDefinitions Attribute capture definitions. 属性捕获定义。 + * @param InAttribute The attribute to calculate. 要计算的属性。 + * @param InBaseValue The base value for the calculation. 计算的基础值。 + * @param OutMagnitude The calculated magnitude (output). 计算出的大小(输出)。 + * @return True if calculation was successful, false otherwise. 如果计算成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|Calculation") + static bool AttemptCalculateCapturedAttributeMagnitudeWithBase(const FGameplayEffectCustomExecutionParameters& InParams, + TArray InAttributeCaptureDefinitions, + FGameplayAttribute InAttribute, float InBaseValue, float& OutMagnitude); + + /** + * Adds an output modifier to the execution output. + * 将输出修改器添加到执行输出。 + * @param InExecutionOutput The execution output to modify. 要修改的执行输出。 + * @param InAttribute The attribute to modify. 要修改的属性。 + * @param InModifierOp The modifier operation type. 修改器操作类型。 + * @param InMagnitude The magnitude of the modifier. 修改器的大小。 + * @return The modified execution output. 修改后的执行输出。 + */ + UFUNCTION(BlueprintCallable, Category="GGA|Calculation") + static FGameplayEffectCustomExecutionOutput AddOutputModifier(UPARAM(ref) + FGameplayEffectCustomExecutionOutput& InExecutionOutput, FGameplayAttribute InAttribute, + EGameplayModOp::Type InModifierOp, float InMagnitude); + + /** + * Marks conditional gameplay effects to trigger. + * 标记条件游戏效果以触发。 + * @param InExecutionOutput The execution output to modify. 要修改的执行输出。 + */ + UFUNCTION(BlueprintCallable, Category="GGA|Calculation") + static void MarkConditionalGameplayEffectsToTrigger(UPARAM(ref) + FGameplayEffectCustomExecutionOutput& InExecutionOutput); + + /** + * Marks gameplay cues as handled manually. + * 将游戏反馈标记为手动处理。 + * @param InExecutionOutput The execution output to modify. 要修改的执行输出。 + */ + UFUNCTION(BlueprintCallable, Category="GGA|Calculation") + static void MarkGameplayCuesHandledManually(UPARAM(ref) + FGameplayEffectCustomExecutionOutput& InExecutionOutput); + + /** + * Marks the stack count as handled manually. + * 将堆叠计数标记为手动处理。 + * @param InExecutionOutput The execution output to modify. 要修改的执行输出。 + */ + UFUNCTION(BlueprintCallable, Category="GGA|Calculation") + static void MarkStackCountHandledManually(UPARAM(ref) + FGameplayEffectCustomExecutionOutput& InExecutionOutput); + + /** + * Retrieves the effect context from a gameplay effect spec. + * 从游戏效果规格获取效果上下文。 + * @param EffectSpec The gameplay effect spec. 游戏效果规格。 + * @return The effect context handle. 效果上下文句柄。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Calculation") + static FGameplayEffectContextHandle GetEffectContextFromSpec(const FGameplayEffectSpec& EffectSpec); + + /** + * Adds a tag to the spec before applying modifiers. + * 在应用修改器前向规格添加标签。 + * @param InParams The execution parameters. 执行参数。 + * @param NewGameplayTag The tag to add. 要添加的标签。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Calculation") + static void AddAssetTagForPreMod(const FGameplayEffectCustomExecutionParameters& InParams, FGameplayTag NewGameplayTag); + + /** + * Adds multiple tags to the spec before applying modifiers. + * 在应用修改器前向规格添加多个标签。 + * @param InParams The execution parameters. 执行参数。 + * @param NewGameplayTags The tags to add. 要添加的标签。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Calculation") + static void AddAssetTagsForPreMod(const FGameplayEffectCustomExecutionParameters& InParams, FGameplayTagContainer NewGameplayTags); +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectContainerFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectContainerFunctionLibrary.h new file mode 100644 index 0000000..0946085 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectContainerFunctionLibrary.h @@ -0,0 +1,90 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGA_GameplayEffectContainerFunctionLibrary.generated.h" + +/** + * Blueprint function library for gameplay effect container operations. + * 用于游戏效果容器操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayEffectContainerFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Checks if the gameplay effect container is valid. + * 检查游戏效果容器是否有效。 + * @param Container The gameplay effect container. 游戏效果容器。 + * @return True if the container is valid, false otherwise. 如果容器有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Container") + static bool IsValidContainer(const FGGA_GameplayEffectContainer& Container); + + /** + * Checks if the container spec has valid effects. + * 检查容器规格是否具有有效效果。 + * @param ContainerSpec The gameplay effect container spec. 游戏效果容器规格。 + * @return True if the spec has valid effects, false otherwise. 如果规格有有效效果则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Container") + static bool HasValidEffects(const FGGA_GameplayEffectContainerSpec& ContainerSpec); + + /** + * Checks if the container spec has valid targets. + * 检查容器规格是否具有有效目标。 + * @param ContainerSpec The gameplay effect container spec. 游戏效果容器规格。 + * @return True if the spec has valid targets, false otherwise. 如果规格有有效目标则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Container") + static bool HasValidTargets(const FGGA_GameplayEffectContainerSpec& ContainerSpec); + + /** + * Adds targets to a copy of the effect container spec. + * 将目标添加到效果容器规格的副本。 + * @param ContainerSpec The gameplay effect container spec. 游戏效果容器规格。 + * @param HitResults Array of hit results to add. 要添加的命中结果数组。 + * @param TargetActors Array of target actors to add. 要添加的目标演员数组。 + * @return The modified container spec. 修改后的容器规格。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Container", meta = (AutoCreateRefTerm = "HitResults,TargetActors")) + static FGGA_GameplayEffectContainerSpec AddTargets(const FGGA_GameplayEffectContainerSpec& ContainerSpec, const TArray& HitResults, + const TArray& TargetActors); + + /** + * Creates a gameplay effect container spec from a container and event data. + * 从容器和事件数据创建游戏效果容器规格。 + * @param Container The gameplay effect container. 游戏效果容器。 + * @param EventData Event data for creating the spec. 用于创建规格的事件数据。 + * @param OverrideGameplayLevel Optional override for gameplay effect level. 可选的游戏效果等级覆盖。 + * @param SourceAbility Optional ability to derive source data. 可选的技能以获取源数据。 + * @return The created container spec. 创建的容器规格。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Container", meta=(AutoCreateRefTerm = "EventData")) + static FGGA_GameplayEffectContainerSpec MakeEffectContainerSpec(UPARAM(ref) const FGGA_GameplayEffectContainer& Container, const FGameplayEventData& EventData, + int32 OverrideGameplayLevel = -1, UGameplayAbility* SourceAbility = nullptr); + + /** + * Applies a gameplay effect container spec. + * 应用游戏效果容器规格。 + * @param ExecutingAbility The ability executing the spec. 执行规格的技能。 + * @param ContainerSpec The container spec to apply. 要应用的容器规格。 + * @return Array of active gameplay effect handles. 活动游戏效果句柄数组。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|Ability|EffectContainer",meta=(DefaultToSelf="ExecutingAbility")) + static TArray ApplyEffectContainerSpec(UGameplayAbility* ExecutingAbility, const FGGA_GameplayEffectContainerSpec& ContainerSpec); + + /** + * Applies a gameplay effect container spec to all targets in its target data. + * 将游戏效果容器规格应用于其目标数据中的所有目标。 + * @param ContainerSpec The container spec to apply. 要应用的容器规格。 + * @return Array of active gameplay effect handles. 活动游戏效果句柄数组。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Container") + static TArray ApplyExternalEffectContainerSpec(const FGGA_GameplayEffectContainerSpec& ContainerSpec); +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectFunctionLibrary.h b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectFunctionLibrary.h new file mode 100644 index 0000000..82641ff --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilities/Public/Utilities/GGA_GameplayEffectFunctionLibrary.h @@ -0,0 +1,226 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffectTypes.h" +#include "GGA_GameplayEffectContext.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GGA_GameplayEffectFunctionLibrary.generated.h" + +UENUM() +enum class EGGA_ContextPayloadResult : uint8 +{ + Valid, + NotValid, +}; + +/** + * Blueprint function library for gameplay effect operations. + * 用于游戏效果操作的蓝图函数库。 + */ +UCLASS() +class GENERICGAMEPLAYABILITIES_API UGGA_GameplayEffectFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: +#pragma region EffectSpecHandle + + /** + * Retrieves the magnitude of a SetByCaller modifier by tag. + * 通过标签获取SetByCaller修改器的大小。 + * @param SpecHandle The gameplay effect spec handle. 游戏效果规格句柄。 + * @param DataTag The tag to query. 要查询的标签。 + * @param WarnIfNotFound Whether to warn if the tag is not found. 如果未找到标签是否警告。 + * @param DefaultIfNotFound Default value if the tag is not found. 如果未找到标签的默认值。 + * @return The magnitude value, or default if not found. 大小值,未找到时返回默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect") + static float GetSetByCallerMagnitudeByTag(FGameplayEffectSpecHandle SpecHandle, FGameplayTag DataTag, bool WarnIfNotFound = false, float DefaultIfNotFound = 0.0f); + + /** + * Retrieves the magnitude of a SetByCaller modifier by name. + * 通过名称获取SetByCaller修改器的大小。 + * @param SpecHandle The gameplay effect spec handle. 游戏效果规格句柄。 + * @param DataName The name to query. 要查询的名称。 + * @return The magnitude value, or zero if not found. 大小值,未找到时返回零。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect") + static float GetSetByCallerMagnitudeByName(FGameplayEffectSpecHandle SpecHandle, FName DataName); + + /** + * Checks if the active gameplay effect handle is valid. + * 检查活动游戏效果句柄是否有效。 + * @param Handle The active gameplay effect handle. 活动游戏效果句柄。 + * @return True if the handle is valid, false otherwise. 如果句柄有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect") + static bool IsActiveGameplayEffectHandleValid(FActiveGameplayEffectHandle Handle); + +#pragma endregion + +#pragma region EffectSpec + /** + * Retrieves the magnitude of a SetByCaller modifier by tag. + * 通过标签获取SetByCaller修改器的大小。 + * @param EffectSpec The gameplay effect spec . 游戏效果规格。 + * @param DataTag The tag to query. 要查询的标签。 + * @param WarnIfNotFound Whether to warn if the tag is not found. 如果未找到标签是否警告。 + * @param DefaultIfNotFound Default value if the tag is not found. 如果未找到标签的默认值。 + * @return The magnitude value, or default if not found. 大小值,未找到时返回默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect") + static float GetSetByCallerMagnitudeByTagFromSpec(const FGameplayEffectSpec& EffectSpec, FGameplayTag DataTag, bool WarnIfNotFound = false, float DefaultIfNotFound = 0.0f); + +#pragma endregion + +#pragma region Effect Context + /** + * Retrieves gameplay tags from the effect context. + * 从效果上下文中获取游戏标签。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @param ActorTagContainer Container for actor tags. 演员标签容器。 + * @param SpecTagContainer Container for spec tags. 规格标签容器。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Context") + static void GetOwnedGameplayTags(FGameplayEffectContextHandle EffectContext, FGameplayTagContainer& ActorTagContainer, FGameplayTagContainer& SpecTagContainer); + + /** + * Sets the instigator and effect causer in the effect context. + * 在效果上下文中设置发起者和效果原因。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @param InInstigator The instigator actor. 发起者演员。 + * @param InEffectCauser The effect causer actor. 效果原因演员。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Context") + static void AddInstigator(FGameplayEffectContextHandle EffectContext, AActor* InInstigator, AActor* InEffectCauser); + + /** + * Sets the effect causer in the effect context. + * 在效果上下文中设置效果原因。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @param InEffectCauser The effect causer actor. 效果原因演员。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Context") + static void SetEffectCauser(FGameplayEffectContextHandle EffectContext, AActor* InEffectCauser); + + /** + * Sets the ability in the effect context. + * 在效果上下文中设置技能。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @param InGameplayAbility The gameplay ability to set. 要设置的游戏技能。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Context") + static void SetAbility(FGameplayEffectContextHandle EffectContext, const UGameplayAbility* InGameplayAbility); + + /** + * Retrieves the ability CDO from the effect context. + * 从效果上下文中获取技能CDO。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return The ability CDO. 技能CDO。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static const UGameplayAbility* GetAbilityCDO(FGameplayEffectContextHandle EffectContext); + + /** + * Retrieves the ability instance from the effect context. + * 从效果上下文中获取技能实例。 + * @attention The ability instance is not replicated so it's only valid on server side. 技能实例未网络复制,因此仅服务端可用。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return The ability instance. 技能实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static const UGameplayAbility* GetAbilityInstance(FGameplayEffectContextHandle EffectContext); + + /** + * Retrieves the ability level from the effect context. + * 从效果上下文中获取技能等级。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return The ability level. 技能等级。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static int32 GetAbilityLevel(FGameplayEffectContextHandle EffectContext); + + /** + * Retrieves the instigator's ability system component. + * 获取发起者的技能系统组件。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return The instigator's ability system component. 发起者的技能系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static UAbilitySystemComponent* GetInstigatorAbilitySystemComponent(FGameplayEffectContextHandle EffectContext); + + /** + * Retrieves the original instigator's ability system component. + * 获取原始发起者的技能系统组件。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return The original instigator's ability system component. 原始发起者的技能系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static UAbilitySystemComponent* GetOriginalInstigatorAbilitySystemComponent(FGameplayEffectContextHandle EffectContext); + + /** + * Sets the source object in the effect context. + * 在效果上下文中设置源对象。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @param NewSourceObject The source object to set. 要设置的源对象。 + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayEffect|Context") + static void AddSourceObject(FGameplayEffectContextHandle EffectContext, const UObject* NewSourceObject); + + /** + * Checks if the effect context has an origin. + * 检查效果上下文是否具有起源。 + * @param EffectContext The effect context handle. 效果上下文句柄。 + * @return True if the context has an origin, false otherwise. 如果上下文有起源则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static bool HasOrigin(FGameplayEffectContextHandle EffectContext); + +#pragma endregion + +#pragma region Custom Effect Context + + /** + * Helper method to safely get the pointer of GGA_GameplayEffectContext. + * 用于安全地获取GGA_GameplayEffectContext的实用函数。 + * @param EffectContext The gameplay effect context handle 游戏效果上下文句柄。 + * @return The actually FGGA_GameplayEffectContext pointer. + */ + static FGGA_GameplayEffectContext* GetEffectContextPtr(FGameplayEffectContextHandle EffectContext); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context") + static bool HasContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType); + + static bool GetContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, FInstancedStruct& OutPayload); + + /** + * Do not call this,It's for blueprint only. + * 不要调用这个,这是给蓝图专用的。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayEffect|Context", meta=(BlueprintInternalUseOnly="true")) + static FInstancedStruct GetValidContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, bool& bValid); + + /** + * Do not call this,It's for blueprint only. + * 不要调用这个,这是给蓝图专用的。 + */ + UFUNCTION(BlueprintCallable, CustomThunk, Category = "GGA|GameplayEffect|Context", meta=(CustomStructureParam = "Value", ExpandEnumAsExecs = "ExecResult", BlueprintInternalUseOnly="true")) + static void GetContextPayload(FGameplayEffectContextHandle EffectContext, const UScriptStruct* PayloadType, EGGA_ContextPayloadResult& ExecResult, int32& Value); + + UFUNCTION(BlueprintCallable, CustomThunk, Category = "GGA|GameplayEffect|Context", + meta=(CustomStructureParam = "Value", ExpandEnumAsExecs = "ExecResult", BlueprintInternalUseOnly="true")) + static void SetContextPayload(FGameplayEffectContextHandle EffectContext, EGGA_ContextPayloadResult& ExecResult, const int32& Value); + +private: + DECLARE_FUNCTION(execGetContextPayload); + DECLARE_FUNCTION(execSetContextPayload); +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/GenericGameplayAbilitiesEditor.Build.cs b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/GenericGameplayAbilitiesEditor.Build.cs new file mode 100644 index 0000000..d9ea34d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/GenericGameplayAbilitiesEditor.Build.cs @@ -0,0 +1,37 @@ +using UnrealBuildTool; + +public class GenericGameplayAbilitiesEditor : ModuleRules +{ + public GenericGameplayAbilitiesEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new[] + { + "Core", "UnrealEd" + } + ); + + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "GameplayAbilities", + "GameplayAbilitiesEditor", + "PropertyEditor", + "GameplayTasks", + "GameplayTasksEditor", + "GameplayTags", + "ToolMenus", + "AssetDefinition", + "BlueprintGraph", + "KismetCompiler", + "GameplayTagsEditor", "GenericGameplayAbilities" + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_AttributeGroupNameCustomization.cpp b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_AttributeGroupNameCustomization.cpp new file mode 100644 index 0000000..6997297 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_AttributeGroupNameCustomization.cpp @@ -0,0 +1,177 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_AttributeGroupNameCustomization.h" +#include "DetailWidgetRow.h" +#include "GGA_AbilitySystemGlobals.h" +#include "GGA_AbilitySystemStructLibrary.h" +#include "PropertyCustomizationHelpers.h" + + +TSharedRef FGGA_AttributeGroupNameCustomization::MakeInstance() +{ + return MakeShareable(new FGGA_AttributeGroupNameCustomization()); +} + +void FGGA_AttributeGroupNameCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + const UGGA_AbilitySystemGlobals* Globals = Cast(IGameplayAbilitiesModule::Get().GetAbilitySystemGlobals()); + + if (Globals == nullptr) + { + return; + } + + FNameMap.Reset(); + + for (const UCurveTable* CurTable : Globals->GetAttributeDefaultsTables()) + { + if (!IsValid(CurTable)) + { + continue; + } + + for (const TPair& CurveRow : CurTable->GetRowMap()) + { + FString RowName = CurveRow.Key.ToString(); + + TArray RowParts; //[0]GroupName [1]SetName [2]AttribueName + RowName.ParseIntoArray(RowParts, TEXT(".")); + if (RowParts.Num() != 3) + { + continue; + } + + TArray GroupParts; //[0]MainName [1]SubName + RowName.ParseIntoArray(GroupParts, TEXT("->")); + if (GroupParts.Num() != 2) + { + //Add class name as group. + FNameMap.FindOrAdd(FName(*RowParts[0])); + } + else if (GroupParts.Num() == 2) + { + TArray& Rows = FNameMap.FindOrAdd(FName(*GroupParts[0])); + Rows.AddUnique(FName(*GroupParts[1])); + } + } + } + PropertyUtilities = StructCustomizationUtils.GetPropertyUtilities(); + + MainNamePropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGGA_AttributeGroupName, MainName)); + SubNamePropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGGA_AttributeGroupName, SubName)); + if (MainNamePropertyHandle.IsValid() && SubNamePropertyHandle.IsValid()) + { + HeaderRow + .NameContent() + [ + StructPropertyHandle->CreatePropertyNameWidget() + ] + .ValueContent() + .MinDesiredWidth(600) + .MaxDesiredWidth(4096) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .HAlign(HAlign_Fill) + .Padding(1.f, 0.f, 2.f, 0.f) + [ + PropertyCustomizationHelpers::MakePropertyComboBox(MainNamePropertyHandle, + FOnGetPropertyComboBoxStrings::CreateStatic(&FGGA_AttributeGroupNameCustomization::GeneratePrimaryComboboxStrings, true, + false, &FNameMap), + FOnGetPropertyComboBoxValue::CreateSP(this, &FGGA_AttributeGroupNameCustomization::GenerateMainString), + FOnPropertyComboBoxValueSelected::CreateSP(this, &FGGA_AttributeGroupNameCustomization::OnMainValueSelected)) + ] + + SVerticalBox::Slot() + .HAlign(HAlign_Fill) + .Padding(2.f, 0.f, 2.f, 0.f) + [ + PropertyCustomizationHelpers::MakePropertyComboBox(MainNamePropertyHandle, + FOnGetPropertyComboBoxStrings::CreateStatic(&FGGA_AttributeGroupNameCustomization::GenerateSubComboboxStrings, true, + false, &FNameMap, + MainNamePropertyHandle), + FOnGetPropertyComboBoxValue::CreateSP(this, &FGGA_AttributeGroupNameCustomization::GenerateSubString), + FOnPropertyComboBoxValueSelected::CreateSP(this, &FGGA_AttributeGroupNameCustomization::OnSubValueSelected)) + ] + ] + + ]; + } +} + + +void FGGA_AttributeGroupNameCustomization::GeneratePrimaryComboboxStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems, + bool bAllowClear, bool bAllowAll, + TMap>* InItems) +{ + for (auto Iter = InItems->CreateConstIterator(); Iter; ++Iter) + { + OutComboBoxStrings.Add(MakeShared(Iter.Key().ToString())); + } +} + +void FGGA_AttributeGroupNameCustomization::GenerateSubComboboxStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, + TArray& OutRestrictedItems, bool bAllowClear, bool bAllowAll, + TMap>* InItems, TSharedPtr PrimaryKey) +{ + void* TagDataPtr = nullptr; + PrimaryKey->GetValueData(TagDataPtr); + const FName* TagPtr = static_cast(TagDataPtr); + + if (!TagPtr || TagPtr->IsNone()) + { + OutComboBoxStrings.Add(MakeShared("Invalid")); + } + else + { + const auto Arr = InItems->Find(*TagPtr); + if (Arr) + { + for (auto& Name : *Arr) + { + OutComboBoxStrings.Add(MakeShared(Name.ToString())); + } + } + } +} + +FString FGGA_AttributeGroupNameCustomization::GenerateSubString() +{ + void* TagDataPtr = nullptr; + SubNamePropertyHandle->GetValueData(TagDataPtr); + const FName* TagPtr = static_cast(TagDataPtr); + + FString TagString = TagPtr ? TagPtr->ToString() : "Invalid"; + + return TagString; +} + +void FGGA_AttributeGroupNameCustomization::OnSubValueSelected(const FString& String) +{ + SubNamePropertyHandle->SetValue(FName(*String)); +} + +FString FGGA_AttributeGroupNameCustomization::GenerateMainString() +{ + void* TagDataPtr = nullptr; + MainNamePropertyHandle->GetValueData(TagDataPtr); + const FName* TagPtr = static_cast(TagDataPtr); + + FString TagString = TagPtr ? TagPtr->ToString() : "Invalid"; + + return TagString; +} + +void FGGA_AttributeGroupNameCustomization::OnMainValueSelected(const FString& String) +{ + MainNamePropertyHandle->SetValue(FName(*String)); + SubNamePropertyHandle->ResetToDefault(); +} + +void FGGA_AttributeGroupNameCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_ContextPayload.cpp b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_ContextPayload.cpp new file mode 100644 index 0000000..9518317 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_ContextPayload.cpp @@ -0,0 +1,52 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_K2Node_ContextPayload.h" +#include "EdGraphSchema_K2.h" +#include "BlueprintNodeSpawner.h" +#include "BlueprintActionDatabaseRegistrar.h" +#include "Utilities/GGA_GameplayEffectFunctionLibrary.h" + +void UGGA_K2Node_ContextPayload::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + Super::GetMenuActions(ActionRegistrar); + UClass* Action = GetClass(); + if (ActionRegistrar.IsOpenForRegistration(Action)) + { + auto CustomizeLambda = [](UEdGraphNode* NewNode, bool bIsTemplateNode, const FName FunctionName) + { + UGGA_K2Node_ContextPayload* Node = CastChecked(NewNode); + UFunction* Function = UGGA_GameplayEffectFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName); + check(Function); + Node->SetFromFunction(Function); + }; + + UBlueprintNodeSpawner* SetNodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); + check(SetNodeSpawner != nullptr); + SetNodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic( + CustomizeLambda, GET_FUNCTION_NAME_CHECKED(UGGA_GameplayEffectFunctionLibrary, SetContextPayload)); + ActionRegistrar.AddBlueprintAction(Action, SetNodeSpawner); + + // UBlueprintNodeSpawner* GetNodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); + // check(GetNodeSpawner != nullptr); + // GetNodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic( + // CustomizeLambda, GET_FUNCTION_NAME_CHECKED(UGGA_GameplayEffectFunctionLibrary, GetContextPayload)); + // ActionRegistrar.AddBlueprintAction(Action, GetNodeSpawner); + } +} + +bool UGGA_K2Node_ContextPayload::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const +{ + const UEdGraphPin* ValuePin = FindPinChecked(FName(TEXT("Value"))); + + if (MyPin == ValuePin && MyPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard) + { + if (OtherPin->PinType.PinCategory != UEdGraphSchema_K2::PC_Struct) + { + OutReason = TEXT("Value must be a struct."); + return true; + } + } + + return false; +} diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_EffectContextPayload.cpp b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_EffectContextPayload.cpp new file mode 100644 index 0000000..53be3e0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GGA_K2Node_EffectContextPayload.cpp @@ -0,0 +1,309 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_K2Node_EffectContextPayload.h" + +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintNodeSpawner.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_AssignmentStatement.h" +#include "K2Node_CallFunction.h" +#include "K2Node_TemporaryVariable.h" +#include "KismetCompiler.h" +#include "Kismet/BlueprintInstancedStructLibrary.h" +#include "Utilities/GGA_GameplayEffectFunctionLibrary.h" + +#define LOCTEXT_NAMESPACE "K2Node" + + +/////////////////////////////////////////////////////////////////////////// +// Node Change events interface implementations +/////////////////////////////////////////////////////////////////////////// + + +void UGGA_K2Node_EffectContextPayload::ReallocatePinsDuringReconstruction(TArray& OldPins) +{ + AllocateDefaultPins(); + RestoreSplitPins(OldPins); +} + +void UGGA_K2Node_EffectContextPayload::PostPlacedNewNode() +{ + Super::PostPlacedNewNode(); + RefreshOutputType(); +} + +void UGGA_K2Node_EffectContextPayload::PinConnectionListChanged(UEdGraphPin* Pin) +{ + Super::PinConnectionListChanged(Pin); + + if (Pin && Pin == GetPayloadTypePin()) + { + RefreshOutputType(); + } +} + +void UGGA_K2Node_EffectContextPayload::PinDefaultValueChanged(UEdGraphPin* Pin) +{ + if (Pin == GetPayloadTypePin()) + { + if (Pin->LinkedTo.Num() == 0) + { + RefreshOutputType(); + } + } +} + +void UGGA_K2Node_EffectContextPayload::PostReconstructNode() +{ + Super::PostReconstructNode(); + RefreshOutputType(); +} + +void UGGA_K2Node_EffectContextPayload::RefreshOutputType() +{ + UEdGraphPin* OutStructPin = GetOutStructPin(); + UEdGraphPin* PayloadType = GetPayloadTypePin(); + + if (PayloadType->DefaultObject != OutStructPin->PinType.PinSubCategoryObject) + { + if (OutStructPin->SubPins.Num() > 0) + { + GetSchema()->RecombinePin(OutStructPin); + } + + OutStructPin->PinType.PinSubCategoryObject = PayloadType->DefaultObject; + OutStructPin->PinType.PinCategory = (PayloadType->DefaultObject == nullptr) ? UEdGraphSchema_K2::PC_Wildcard : UEdGraphSchema_K2::PC_Struct; + } +} + + +//////////////////////////////////////////////////////////////////////////// +//UK2_Node and EDGraph Interface Implementations +//////////////////////////////////////////////////////////////////////////// + + +FString UGGA_K2Node_EffectContextPayload::GetPinMetaData(FName InPinName, FName InKey) +{ + FString MetaData = Super::GetPinMetaData(InPinName, InKey); + if (MetaData.IsEmpty()) + { + //Filters out the abstract classes from the pin search + if (InPinName == GetPayloadTypePin()->GetName() && InKey == FBlueprintMetadata::MD_AllowAbstractClasses) + { + MetaData = TEXT("false"); + } + } + return MetaData; +} + +FSlateIcon UGGA_K2Node_EffectContextPayload::GetIconAndTint(FLinearColor& OutColor) const +{ + OutColor = FLinearColor(FColor::Black); + return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.AllClasses.FunctionIcon"); +} + + +FLinearColor UGGA_K2Node_EffectContextPayload::GetNodeTitleColor() const +{ + return FLinearColor(FColor::Cyan); +} + + +FText UGGA_K2Node_EffectContextPayload::GetMenuCategory() const +{ + return LOCTEXT("K2Node_GetProperty_Category", "GGA|GameplayEffect|Context"); +} + + +/////////////////////////////////////////////////////////////////// +// PIN GETTERS +/////////////////////////////////////////////////////////////////// + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetEffectContextPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::EffectContextPinName); + check(Pin->Direction == EGPD_Input); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetInstancedStructPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::InstancedStructPinName); + check(Pin->Direction == EGPD_Input); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetKeyPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::KeyPinName); + check(Pin->Direction == EGPD_Input); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetPayloadTypePin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::PayloadTypePinName); + check(Pin->Direction == EGPD_Input); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetOutStructPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::OutStructPinName); + check(Pin->Direction == EGPD_Output); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetValidPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::ValidExecPinName); + check(Pin->Direction == EGPD_Output); + return Pin; +} + +UEdGraphPin* UGGA_K2Node_EffectContextPayload::GetInvalidPin() const +{ + UEdGraphPin* Pin = FindPinChecked(K2Node_ContextPayload_PinNames::InvalidExecPinName); + check(Pin->Direction == EGPD_Output); + return Pin; +} + + +void UGGA_K2Node_GetEffectContextPayload::AllocateDefaultPins() +{ + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, FGameplayEffectContextHandle::StaticStruct(), K2Node_ContextPayload_PinNames::EffectContextPinName); + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UScriptStruct::StaticClass(), K2Node_ContextPayload_PinNames::PayloadTypePinName); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, K2Node_ContextPayload_PinNames::ValidExecPinName); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, K2Node_ContextPayload_PinNames::InvalidExecPinName); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, K2Node_ContextPayload_PinNames::OutStructPinName); + + Super::AllocateDefaultPins(); +} + +void UGGA_K2Node_GetEffectContextPayload::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) +{ + Super::ExpandNode(CompilerContext, SourceGraph); + + UFunction* GetValidContextPayloadFunc = UGGA_GameplayEffectFunctionLibrary::StaticClass()->FindFunctionByName( + GET_FUNCTION_NAME_CHECKED_ThreeParams(UGGA_GameplayEffectFunctionLibrary, GetValidContextPayload, FGameplayEffectContextHandle, const UScriptStruct*, bool&)); + UFunction* GetInstStructValueFunc = UBlueprintInstancedStructLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UBlueprintInstancedStructLibrary, GetInstancedStructValue)); + + if (!GetValidContextPayloadFunc || !GetInstStructValueFunc) + { + CompilerContext.MessageLog.Error( + *LOCTEXT("InvalidClass", "The GetValidContextPayload or GetInstancedStructValue functions have not been found. Check ExpandNode in GGA_K2Node_EffectContextPayload.cpp").ToString(), + this); + return; + } + + const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); + bool bIsErrorFree = true; + + + const FEdGraphPinType& PinType = GetOutStructPin()->PinType; + + UK2Node_TemporaryVariable* TempVarOutput = CompilerContext.SpawnInternalVariable( + this, PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject.Get(), PinType.ContainerType, PinType.PinValueType); + + UK2Node_AssignmentStatement* AssignNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + UK2Node_CallFunction* const GetPayloadNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + UK2Node_CallFunction* const GetStructValueNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + + GetPayloadNode->SetFromFunction(GetValidContextPayloadFunc); + GetStructValueNode->SetFromFunction(GetInstStructValueFunc); + + GetPayloadNode->AllocateDefaultPins(); + GetStructValueNode->AllocateDefaultPins(); + AssignNode->AllocateDefaultPins(); + + CompilerContext.MessageLog.NotifyIntermediateObjectCreation(GetPayloadNode, this); + CompilerContext.MessageLog.NotifyIntermediateObjectCreation(GetStructValueNode, this); + CompilerContext.MessageLog.NotifyIntermediateObjectCreation(AssignNode, this); + + //Connect input pins to the GetValidContextPayloadNode + //---------------------------------------------------------------------------------------- + // UEdGraphPin* EffectContextPin = Schema->FindSelfPin(*GetPayloadNode, EGPD_Input); + UEdGraphPin* EffectContextPin = GetPayloadNode->FindPinChecked(TEXT("EffectContext")); + UEdGraphPin* InterExecLeftPin = GetPayloadNode->GetThenPin(); + UEdGraphPin* PayloadTypePin = GetPayloadNode->FindPinChecked(TEXT("PayloadType")); + UEdGraphPin* InterStructLeftPin = GetPayloadNode->GetReturnValuePin(); + + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetEffectContextPin(), *EffectContextPin).CanSafeConnect(); + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetPayloadTypePin(), *PayloadTypePin).CanSafeConnect(); + //---------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + + + //Connect input Target Pin and the output GetPayloadNode pins to the GetStructValueNode pin + //---------------------------------------------------------------------------------------- + UEdGraphPin* InterExecRightPin = GetStructValueNode->GetExecPin(); + UEdGraphPin* ValidPin = GetStructValueNode->FindPinChecked(TEXT("Valid")); + UEdGraphPin* InvalidPin = GetStructValueNode->FindPinChecked(TEXT("NotValid")); + UEdGraphPin* OutStructPin = GetStructValueNode->FindPinChecked(TEXT("Value")); + UEdGraphPin* InterStructRightPin = GetStructValueNode->FindPinChecked(TEXT("InstancedStruct")); + + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *InterExecRightPin).CanSafeConnect(); + bIsErrorFree &= Schema->TryCreateConnection(InterStructLeftPin, InterStructRightPin); + //---------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + + + //Connect the output of GetStructValueNode to the input of Assign node. The Invalid pin directly goes to Invalid pin + // but the valid pin goes through the Assign node. The temporary variable is assigned a value, and is connected to the + // node's output struct. The order that the connections are made is critical. First the variable needs to connect to the output, + // then the variable should be connected to the Variable pin of Assign so the variable type is updated, and only then the Value pin + // of Assign can be connected to OutStructPin of the GetStructValueNode. + //---------------------------------------------------------------------------------------- + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetOutStructPin(), *TempVarOutput->GetVariablePin()).CanSafeConnect(); + bIsErrorFree &= Schema->TryCreateConnection(AssignNode->GetVariablePin(), TempVarOutput->GetVariablePin()); + bIsErrorFree &= Schema->TryCreateConnection(AssignNode->GetValuePin(), OutStructPin); + + bIsErrorFree &= Schema->TryCreateConnection(ValidPin, AssignNode->GetExecPin()); + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetInvalidPin(), *InvalidPin).CanSafeConnect(); + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*GetValidPin(), *AssignNode->GetThenPin()).CanSafeConnect(); + //---------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + + + if (!bIsErrorFree) + { + CompilerContext.MessageLog.Error(*LOCTEXT("InternalConnectionError", "Get Context Payload: Internal connection error. @@").ToString(), this); + } + BreakAllNodeLinks(); +} + + +//////////////////////////////////////////////////////////////////////////// +//UK2_Node and EDGraph Interface Implementations +//////////////////////////////////////////////////////////////////////////// + +void UGGA_K2Node_GetEffectContextPayload::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + Super::GetMenuActions(ActionRegistrar); + UClass* Action = GetClass(); + if (ActionRegistrar.IsOpenForRegistration(Action)) + { + UBlueprintNodeSpawner* Spawner = UBlueprintNodeSpawner::Create(GetClass()); + ActionRegistrar.AddBlueprintAction(Action, Spawner); + } +} + +FText UGGA_K2Node_GetEffectContextPayload::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("K2Node_GetProperty_NodeTitle", "Get Context Payload"); +} + +FText UGGA_K2Node_GetEffectContextPayload::GetTooltipText() const +{ + return LOCTEXT("K2Node_GetProperty_TooltipText", "Outputs the context payload based on the selected payload type"); +} + +FText UGGA_K2Node_GetEffectContextPayload::GetKeywords() const +{ + return LOCTEXT("GetEffectContextPayload", "Get Gameplay Effect Context Payload"); +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GenericGameplayAbilitiesEditor.cpp b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GenericGameplayAbilitiesEditor.cpp new file mode 100644 index 0000000..7f532cf --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Private/GenericGameplayAbilitiesEditor.cpp @@ -0,0 +1,21 @@ +#include "GenericGameplayAbilitiesEditor.h" + +#include "GGA_AttributeGroupNameCustomization.h" + +#define LOCTEXT_NAMESPACE "FGenericGameplayAbilitiesEditorModule" + +void FGenericGameplayAbilitiesEditorModule::StartupModule() +{ + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + PropertyModule.RegisterCustomPropertyTypeLayout("GGA_AttributeGroupName", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FGGA_AttributeGroupNameCustomization::MakeInstance)); +} + +void FGenericGameplayAbilitiesEditorModule::ShutdownModule() +{ + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + PropertyModule.UnregisterCustomPropertyTypeLayout("GGA_AttributeGroupName"); +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericGameplayAbilitiesEditorModule, GenericGameplayAbilitiesEditor) \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_AttributeGroupNameCustomization.h b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_AttributeGroupNameCustomization.h new file mode 100644 index 0000000..fd820c0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_AttributeGroupNameCustomization.h @@ -0,0 +1,41 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Layout/Visibility.h" +#include "IPropertyTypeCustomization.h" + +class FDetailWidgetRow; +class IDetailChildrenBuilder; +class IPropertyHandle; + +/** Details customization for FAttributeBasedFloat */ +class GENERICGAMEPLAYABILITIESEDITOR_API FGGA_AttributeGroupNameCustomization : public IPropertyTypeCustomization +{ +public: + static TSharedRef MakeInstance(); + + /** Overridden to provide the property name or hide, if necessary */ + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + + static void GeneratePrimaryComboboxStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems, bool bAllowClear, bool bAllowAll, + TMap>* InItems); + FString GenerateMainString(); + void OnMainValueSelected(const FString& String); + static void GenerateSubComboboxStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems, bool bAllowClear, bool bAllowAll, + TMap>* InItems, TSharedPtr PrimaryKey); + FString GenerateSubString(); + void OnSubValueSelected(const FString& String); + /** Overridden to allow for possibly being hidden */ + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +private: + TMap> FNameMap; + + TSharedPtr MainNamePropertyHandle; + TSharedPtr SubNamePropertyHandle; + TWeakPtr PropertyUtilities; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_ContextPayload.h b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_ContextPayload.h new file mode 100644 index 0000000..23c1648 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_ContextPayload.h @@ -0,0 +1,33 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "K2Node_CallFunction.h" +#include "GGA_K2Node_ContextPayload.generated.h" + +class UEdGraphPin; +class FBlueprintActionDatabaseRegistrar; + +/** + * Node customization for MakeInstancedStruct(), SetContextPayload(), and GetContextPayload(). + */ +UCLASS() +class GENERICGAMEPLAYABILITIESEDITOR_API UGGA_K2Node_ContextPayload : public UK2Node_CallFunction +{ + GENERATED_BODY() + + + //~ Begin UEdGraphNode Interface + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + //~ End UEdGraphNode Interface + + //~ Begin K2Node Interface + virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override; + //~ End K2Node Interface + +protected: + //~ UK2Node_CallFunction interface + virtual bool CanToggleNodePurity() const override { return false; } + //~ End UK2Node_CallFunction interface +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_EffectContextPayload.h b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_EffectContextPayload.h new file mode 100644 index 0000000..4c36e1c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GGA_K2Node_EffectContextPayload.h @@ -0,0 +1,73 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "K2Node.h" +#include "GGA_K2Node_EffectContextPayload.generated.h" + +namespace K2Node_ContextPayload_PinNames +{ + static FName EffectContextPinName = "EffectContext"; + static FName InstancedStructPinName = "InstancedStruct"; + static FName KeyPinName = "Key"; + static FName PayloadTypePinName = "PayloadType"; + static FName OutStructPinName = "ReturnValue"; + static FName ValidExecPinName = "Valid"; + static FName InvalidExecPinName = "Invalid"; +}; + + +/** + * + */ +UCLASS() +class GENERICGAMEPLAYABILITIESEDITOR_API UGGA_K2Node_EffectContextPayload : public UK2Node +{ + GENERATED_BODY() + +protected: + //~ UEdGraphNode Interface. + virtual void PinConnectionListChanged(UEdGraphPin* Pin) override; + virtual void PostPlacedNewNode() override; + virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override; + virtual FString GetPinMetaData(FName InPinName, FName InKey) override; + virtual FSlateIcon GetIconAndTint(FLinearColor& OutColor) const override; + virtual FLinearColor GetNodeTitleColor() const override; + + + //~ UK2Node Interface + virtual FText GetMenuCategory() const override; + virtual void ReallocatePinsDuringReconstruction(TArray& OldPins) override; + virtual bool IsNodeSafeToIgnore() const override { return true; } + virtual bool IsNodePure() const override { return false; } + virtual void PostReconstructNode() override; + + //~ Getters + UEdGraphPin* GetEffectContextPin() const; + UEdGraphPin* GetInstancedStructPin() const; + UEdGraphPin* GetKeyPin() const; + UEdGraphPin* GetPayloadTypePin() const; + UEdGraphPin* GetOutStructPin() const; + UEdGraphPin* GetValidPin() const; + UEdGraphPin* GetInvalidPin() const; + + //~ Helper Functions + void RefreshOutputType(); +}; + +UCLASS(BlueprintType, Blueprintable) +class GENERICGAMEPLAYABILITIESEDITOR_API UGGA_K2Node_GetEffectContextPayload : public UGGA_K2Node_EffectContextPayload +{ + GENERATED_BODY() + + //~ UEdGraphNode Interface. + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual FText GetTooltipText() const override; + virtual FText GetKeywords() const override; + virtual void AllocateDefaultPins() override; + + //~ UK2Node Interface + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GenericGameplayAbilitiesEditor.h b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GenericGameplayAbilitiesEditor.h new file mode 100644 index 0000000..140047e --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAbilitiesEditor/Public/GenericGameplayAbilitiesEditor.h @@ -0,0 +1,11 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FGenericGameplayAbilitiesEditorModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/GenericGameplayAttributes.Build.cs b/Plugins/GCS/Source/GenericGameplayAttributes/GenericGameplayAttributes.Build.cs new file mode 100644 index 0000000..6a3fda4 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/GenericGameplayAttributes.Build.cs @@ -0,0 +1,28 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +using UnrealBuildTool; + +public class GenericGameplayAttributes : ModuleRules +{ + public GenericGameplayAttributes(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "GameplayTags","GameplayAbilities","GameplayTasks" + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Combat.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Combat.cpp new file mode 100644 index 0000000..e7afba2 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Combat.cpp @@ -0,0 +1,332 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/AS_Combat.h" + +#include "Net/UnrealNetwork.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameplayEffectExtension.h" +#include "GGA_GameplayAttributesHelper.h" +#include "GGA_AttributeSystemComponent.h" + +namespace AS_Combat +{ + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Damage, TEXT("GGF.Attribute.CombatSet.Damage"), "The damage that will apply to target") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(DamageNegation, TEXT("GGF.Attribute.CombatSet.DamageNegation"), "The damage reduction(percentage) for incoming health damage") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(GuardDamageNegation, TEXT("GGF.Attribute.CombatSet.GuardDamageNegation"), "The damage reduction(percentage) for incoming health damage while guarding") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(StaminaDamage, TEXT("GGF.Attribute.CombatSet.StaminaDamage"), "The stamina damage that will apply to target") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(StaminaDamageNegation, TEXT("GGF.Attribute.CombatSet.StaminaDamageNegation"), "The damage reduction(percentage) for incoming stamina damage") + + +} + +UAS_Combat::UAS_Combat() +{ + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Combat::Damage,GetDamageAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Combat::DamageNegation,GetDamageNegationAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Combat::GuardDamageNegation,GetGuardDamageNegationAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Combat::StaminaDamage,GetStaminaDamageAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Combat::StaminaDamageNegation,GetStaminaDamageNegationAttribute()); + + +} + +void UAS_Combat::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, Damage, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, DamageNegation, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, GuardDamageNegation, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, StaminaDamage, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, StaminaDamageNegation, COND_None, REPNOTIFY_Always); + +} + + +void UAS_Combat::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + + + + + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePreAttributeChange(this,Attribute,NewValue); + } + } +} + +bool UAS_Combat::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data) +{ + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + return ASS->ReceivePreGameplayEffectExecute(this, Data); + } + } + + return Super::PreGameplayEffectExecute(Data); +} + +void UAS_Combat::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + Super::PostAttributeChange(Attribute, OldValue, NewValue); + + + + + + + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostAttributeChange(this, Attribute, OldValue, NewValue); + } + } +} + +void UAS_Combat::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + + + + + + + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostGameplayEffectExecute(this,Data); + } + } +} + +void UAS_Combat::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, + const FGameplayAttribute& AffectedAttributeProperty) +{ + UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); + const float CurrentMaxValue = MaxAttribute.GetCurrentValue(); + if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp) + { + // Change current value to maintain the current Val / Max percent + const float CurrentValue = AffectedAttribute.GetCurrentValue(); + float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue; + + AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta); + } +} + + + +FGameplayAttribute UAS_Combat::Bp_GetDamageAttribute() +{ + return ThisClass::GetDamageAttribute(); +} + +float UAS_Combat::Bp_GetDamage() const +{ + return GetDamage(); +} + +void UAS_Combat::Bp_SetDamage(float NewValue) +{ + SetDamage(NewValue); +} + +void UAS_Combat::Bp_InitDamage(float NewValue) +{ + InitDamage(NewValue); +} + +void UAS_Combat::OnRep_Damage(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, Damage, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetDamageAttribute(),GetDamage(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Combat::Bp_GetDamageNegationAttribute() +{ + return ThisClass::GetDamageNegationAttribute(); +} + +float UAS_Combat::Bp_GetDamageNegation() const +{ + return GetDamageNegation(); +} + +void UAS_Combat::Bp_SetDamageNegation(float NewValue) +{ + SetDamageNegation(NewValue); +} + +void UAS_Combat::Bp_InitDamageNegation(float NewValue) +{ + InitDamageNegation(NewValue); +} + +void UAS_Combat::OnRep_DamageNegation(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, DamageNegation, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetDamageNegationAttribute(),GetDamageNegation(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Combat::Bp_GetGuardDamageNegationAttribute() +{ + return ThisClass::GetGuardDamageNegationAttribute(); +} + +float UAS_Combat::Bp_GetGuardDamageNegation() const +{ + return GetGuardDamageNegation(); +} + +void UAS_Combat::Bp_SetGuardDamageNegation(float NewValue) +{ + SetGuardDamageNegation(NewValue); +} + +void UAS_Combat::Bp_InitGuardDamageNegation(float NewValue) +{ + InitGuardDamageNegation(NewValue); +} + +void UAS_Combat::OnRep_GuardDamageNegation(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, GuardDamageNegation, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetGuardDamageNegationAttribute(),GetGuardDamageNegation(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Combat::Bp_GetStaminaDamageAttribute() +{ + return ThisClass::GetStaminaDamageAttribute(); +} + +float UAS_Combat::Bp_GetStaminaDamage() const +{ + return GetStaminaDamage(); +} + +void UAS_Combat::Bp_SetStaminaDamage(float NewValue) +{ + SetStaminaDamage(NewValue); +} + +void UAS_Combat::Bp_InitStaminaDamage(float NewValue) +{ + InitStaminaDamage(NewValue); +} + +void UAS_Combat::OnRep_StaminaDamage(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, StaminaDamage, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetStaminaDamageAttribute(),GetStaminaDamage(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Combat::Bp_GetStaminaDamageNegationAttribute() +{ + return ThisClass::GetStaminaDamageNegationAttribute(); +} + +float UAS_Combat::Bp_GetStaminaDamageNegation() const +{ + return GetStaminaDamageNegation(); +} + +void UAS_Combat::Bp_SetStaminaDamageNegation(float NewValue) +{ + SetStaminaDamageNegation(NewValue); +} + +void UAS_Combat::Bp_InitStaminaDamageNegation(float NewValue) +{ + InitStaminaDamageNegation(NewValue); +} + +void UAS_Combat::OnRep_StaminaDamageNegation(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, StaminaDamageNegation, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetStaminaDamageNegationAttribute(),GetStaminaDamageNegation(),OldValue.GetCurrentValue()); + } + } +} + + + diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Health.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Health.cpp new file mode 100644 index 0000000..7bc079c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Health.cpp @@ -0,0 +1,251 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/AS_Health.h" + +#include "Net/UnrealNetwork.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameplayEffectExtension.h" +#include "GGA_GameplayAttributesHelper.h" +#include "GGA_AttributeSystemComponent.h" + +namespace AS_Health +{ + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Health, TEXT("GGF.Attribute.HealthSet.Health"), "Current health of an actor.(actor的当前生命值)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(MaxHealth, TEXT("GGF.Attribute.HealthSet.MaxHealth"), "Max health value of an actor.(actor的最大生命值)") + + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(IncomingHealing, TEXT("GGF.Attribute.HealthSet.IncomingHealing"), "Incoming healing. This is mapped directly to +Health.(即将到来的恢复值,映射为+Health)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(IncomingDamage, TEXT("GGF.Attribute.HealthSet.IncomingDamage"), "Incoming damage. This is mapped directly to -Health(即将到来的伤害值,映射为-Health)") + +} + +UAS_Health::UAS_Health() +{ + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Health::Health,GetHealthAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Health::MaxHealth,GetMaxHealthAttribute()); + + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Health::IncomingHealing,GetIncomingHealingAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Health::IncomingDamage,GetIncomingDamageAttribute()); + +} + +void UAS_Health::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, Health, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, MaxHealth, COND_None, REPNOTIFY_Always); + +} + + +void UAS_Health::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + + if (Attribute == GetHealthAttribute()) + { + NewValue = FMath::Clamp(NewValue,0,GetMaxHealth()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePreAttributeChange(this,Attribute,NewValue); + } + } +} + +bool UAS_Health::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data) +{ + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + return ASS->ReceivePreGameplayEffectExecute(this, Data); + } + } + + return Super::PreGameplayEffectExecute(Data); +} + +void UAS_Health::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + Super::PostAttributeChange(Attribute, OldValue, NewValue); + + + + if (Attribute == GetMaxHealthAttribute()) + { + AdjustAttributeForMaxChange(Health, OldValue, NewValue, GetHealthAttribute()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostAttributeChange(this, Attribute, OldValue, NewValue); + } + } +} + +void UAS_Health::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + + if (Data.EvaluatedData.Attribute == GetHealthAttribute()) + { + SetHealth(FMath::Clamp(GetHealth(),0,GetMaxHealth())); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostGameplayEffectExecute(this,Data); + } + } +} + +void UAS_Health::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, + const FGameplayAttribute& AffectedAttributeProperty) +{ + UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); + const float CurrentMaxValue = MaxAttribute.GetCurrentValue(); + if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp) + { + // Change current value to maintain the current Val / Max percent + const float CurrentValue = AffectedAttribute.GetCurrentValue(); + float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue; + + AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta); + } +} + + + +FGameplayAttribute UAS_Health::Bp_GetHealthAttribute() +{ + return ThisClass::GetHealthAttribute(); +} + +float UAS_Health::Bp_GetHealth() const +{ + return GetHealth(); +} + +void UAS_Health::Bp_SetHealth(float NewValue) +{ + SetHealth(NewValue); +} + +void UAS_Health::Bp_InitHealth(float NewValue) +{ + InitHealth(NewValue); +} + +void UAS_Health::OnRep_Health(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, Health, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetHealthAttribute(),GetHealth(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Health::Bp_GetMaxHealthAttribute() +{ + return ThisClass::GetMaxHealthAttribute(); +} + +float UAS_Health::Bp_GetMaxHealth() const +{ + return GetMaxHealth(); +} + +void UAS_Health::Bp_SetMaxHealth(float NewValue) +{ + SetMaxHealth(NewValue); +} + +void UAS_Health::Bp_InitMaxHealth(float NewValue) +{ + InitMaxHealth(NewValue); +} + +void UAS_Health::OnRep_MaxHealth(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, MaxHealth, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetMaxHealthAttribute(),GetMaxHealth(),OldValue.GetCurrentValue()); + } + } +} + + + + + +FGameplayAttribute UAS_Health::Bp_GetIncomingHealingAttribute() +{ + return ThisClass::GetIncomingHealingAttribute(); +} + +float UAS_Health::Bp_GetIncomingHealing() const +{ + return GetIncomingHealing(); +} + +void UAS_Health::Bp_SetIncomingHealing(float NewValue) +{ + SetIncomingHealing(NewValue); +} + + + +FGameplayAttribute UAS_Health::Bp_GetIncomingDamageAttribute() +{ + return ThisClass::GetIncomingDamageAttribute(); +} + +float UAS_Health::Bp_GetIncomingDamage() const +{ + return GetIncomingDamage(); +} + +void UAS_Health::Bp_SetIncomingDamage(float NewValue) +{ + SetIncomingDamage(NewValue); +} + diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Mana.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Mana.cpp new file mode 100644 index 0000000..2ca464a --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Mana.cpp @@ -0,0 +1,209 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/AS_Mana.h" + +#include "Net/UnrealNetwork.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameplayEffectExtension.h" +#include "GGA_GameplayAttributesHelper.h" +#include "GGA_AttributeSystemComponent.h" + +namespace AS_Mana +{ + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Mana, TEXT("GGF.Attribute.ManaSet.Mana"), "Current mana of an actor.(actor的当前魔法值)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(MaxMana, TEXT("GGF.Attribute.ManaSet.MaxMana"), "Max mana value of an actor.(actor的最大魔法值)") + + +} + +UAS_Mana::UAS_Mana() +{ + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Mana::Mana,GetManaAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Mana::MaxMana,GetMaxManaAttribute()); + + +} + +void UAS_Mana::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, Mana, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, MaxMana, COND_None, REPNOTIFY_Always); + +} + + +void UAS_Mana::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + + if (Attribute == GetManaAttribute()) + { + NewValue = FMath::Clamp(NewValue,0,GetMaxMana()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePreAttributeChange(this,Attribute,NewValue); + } + } +} + +bool UAS_Mana::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data) +{ + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + return ASS->ReceivePreGameplayEffectExecute(this, Data); + } + } + + return Super::PreGameplayEffectExecute(Data); +} + +void UAS_Mana::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + Super::PostAttributeChange(Attribute, OldValue, NewValue); + + + + if (Attribute == GetMaxManaAttribute()) + { + AdjustAttributeForMaxChange(Mana, OldValue, NewValue, GetManaAttribute()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostAttributeChange(this, Attribute, OldValue, NewValue); + } + } +} + +void UAS_Mana::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + + if (Data.EvaluatedData.Attribute == GetManaAttribute()) + { + SetMana(FMath::Clamp(GetMana(),0,GetMaxMana())); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostGameplayEffectExecute(this,Data); + } + } +} + +void UAS_Mana::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, + const FGameplayAttribute& AffectedAttributeProperty) +{ + UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); + const float CurrentMaxValue = MaxAttribute.GetCurrentValue(); + if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp) + { + // Change current value to maintain the current Val / Max percent + const float CurrentValue = AffectedAttribute.GetCurrentValue(); + float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue; + + AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta); + } +} + + + +FGameplayAttribute UAS_Mana::Bp_GetManaAttribute() +{ + return ThisClass::GetManaAttribute(); +} + +float UAS_Mana::Bp_GetMana() const +{ + return GetMana(); +} + +void UAS_Mana::Bp_SetMana(float NewValue) +{ + SetMana(NewValue); +} + +void UAS_Mana::Bp_InitMana(float NewValue) +{ + InitMana(NewValue); +} + +void UAS_Mana::OnRep_Mana(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, Mana, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetManaAttribute(),GetMana(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Mana::Bp_GetMaxManaAttribute() +{ + return ThisClass::GetMaxManaAttribute(); +} + +float UAS_Mana::Bp_GetMaxMana() const +{ + return GetMaxMana(); +} + +void UAS_Mana::Bp_SetMaxMana(float NewValue) +{ + SetMaxMana(NewValue); +} + +void UAS_Mana::Bp_InitMaxMana(float NewValue) +{ + InitMaxMana(NewValue); +} + +void UAS_Mana::OnRep_MaxMana(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, MaxMana, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetMaxManaAttribute(),GetMaxMana(),OldValue.GetCurrentValue()); + } + } +} + + + diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Stamina.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Stamina.cpp new file mode 100644 index 0000000..75ad12f --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/Attributes/AS_Stamina.cpp @@ -0,0 +1,251 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/AS_Stamina.h" + +#include "Net/UnrealNetwork.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "GameplayEffectExtension.h" +#include "GGA_GameplayAttributesHelper.h" +#include "GGA_AttributeSystemComponent.h" + +namespace AS_Stamina +{ + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Stamina, TEXT("GGF.Attribute.StaminaSet.Stamina"), "Current stamina of an actor.(actor的当前生命值)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(MaxStamina, TEXT("GGF.Attribute.StaminaSet.MaxStamina"), "Max stamina value of an actor.(actor的最大生命值)") + + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(IncomingHealing, TEXT("GGF.Attribute.StaminaSet.IncomingHealing"), "Incoming healing. This is mapped directly to +Stamina.(即将到来的恢复值,映射为+Stamina)") + + UE_DEFINE_GAMEPLAY_TAG_COMMENT(IncomingDamage, TEXT("GGF.Attribute.StaminaSet.IncomingDamage"), "Incoming damage. This is mapped directly to -Stamina(即将到来的伤害值,映射为-Stamina)") + +} + +UAS_Stamina::UAS_Stamina() +{ + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Stamina::Stamina,GetStaminaAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Stamina::MaxStamina,GetMaxStaminaAttribute()); + + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Stamina::IncomingHealing,GetIncomingHealingAttribute()); + + UGGA_GameplayAttributesHelper::RegisterTagToAttribute(AS_Stamina::IncomingDamage,GetIncomingDamageAttribute()); + +} + +void UAS_Stamina::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, Stamina, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, MaxStamina, COND_None, REPNOTIFY_Always); + +} + + +void UAS_Stamina::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + + if (Attribute == GetStaminaAttribute()) + { + NewValue = FMath::Clamp(NewValue,0,GetMaxStamina()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePreAttributeChange(this,Attribute,NewValue); + } + } +} + +bool UAS_Stamina::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data) +{ + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + return ASS->ReceivePreGameplayEffectExecute(this, Data); + } + } + + return Super::PreGameplayEffectExecute(Data); +} + +void UAS_Stamina::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + Super::PostAttributeChange(Attribute, OldValue, NewValue); + + + + if (Attribute == GetMaxStaminaAttribute()) + { + AdjustAttributeForMaxChange(Stamina, OldValue, NewValue, GetStaminaAttribute()); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostAttributeChange(this, Attribute, OldValue, NewValue); + } + } +} + +void UAS_Stamina::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + + if (Data.EvaluatedData.Attribute == GetStaminaAttribute()) + { + SetStamina(FMath::Clamp(GetStamina(),0,GetMaxStamina())); + } + + + + + + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceivePostGameplayEffectExecute(this,Data); + } + } +} + +void UAS_Stamina::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, + const FGameplayAttribute& AffectedAttributeProperty) +{ + UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); + const float CurrentMaxValue = MaxAttribute.GetCurrentValue(); + if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp) + { + // Change current value to maintain the current Val / Max percent + const float CurrentValue = AffectedAttribute.GetCurrentValue(); + float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue; + + AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta); + } +} + + + +FGameplayAttribute UAS_Stamina::Bp_GetStaminaAttribute() +{ + return ThisClass::GetStaminaAttribute(); +} + +float UAS_Stamina::Bp_GetStamina() const +{ + return GetStamina(); +} + +void UAS_Stamina::Bp_SetStamina(float NewValue) +{ + SetStamina(NewValue); +} + +void UAS_Stamina::Bp_InitStamina(float NewValue) +{ + InitStamina(NewValue); +} + +void UAS_Stamina::OnRep_Stamina(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, Stamina, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetStaminaAttribute(),GetStamina(),OldValue.GetCurrentValue()); + } + } +} + + + +FGameplayAttribute UAS_Stamina::Bp_GetMaxStaminaAttribute() +{ + return ThisClass::GetMaxStaminaAttribute(); +} + +float UAS_Stamina::Bp_GetMaxStamina() const +{ + return GetMaxStamina(); +} + +void UAS_Stamina::Bp_SetMaxStamina(float NewValue) +{ + SetMaxStamina(NewValue); +} + +void UAS_Stamina::Bp_InitMaxStamina(float NewValue) +{ + InitMaxStamina(NewValue); +} + +void UAS_Stamina::OnRep_MaxStamina(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(ThisClass, MaxStamina, OldValue); + if (AActor* Actor = GetOwningActor()) + { + if (UGGA_AttributeSystemComponent* ASS = Actor->FindComponentByClass()) + { + ASS->ReceiveAttributeChange(this,GetMaxStaminaAttribute(),GetMaxStamina(),OldValue.GetCurrentValue()); + } + } +} + + + + + +FGameplayAttribute UAS_Stamina::Bp_GetIncomingHealingAttribute() +{ + return ThisClass::GetIncomingHealingAttribute(); +} + +float UAS_Stamina::Bp_GetIncomingHealing() const +{ + return GetIncomingHealing(); +} + +void UAS_Stamina::Bp_SetIncomingHealing(float NewValue) +{ + SetIncomingHealing(NewValue); +} + + + +FGameplayAttribute UAS_Stamina::Bp_GetIncomingDamageAttribute() +{ + return ThisClass::GetIncomingDamageAttribute(); +} + +float UAS_Stamina::Bp_GetIncomingDamage() const +{ + return GetIncomingDamage(); +} + +void UAS_Stamina::Bp_SetIncomingDamage(float NewValue) +{ + SetIncomingDamage(NewValue); +} + diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_AttributeSystemComponent.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_AttributeSystemComponent.cpp new file mode 100644 index 0000000..aab00c4 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_AttributeSystemComponent.cpp @@ -0,0 +1,106 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_AttributeSystemComponent.h" + +#include "AbilitySystemComponent.h" +#include "GameplayEffect.h" +#include "GameplayEffectExtension.h" +#include "GameplayEffectTypes.h" + +DEFINE_LOG_CATEGORY_STATIC(LogGGA_AttributeSystem, Log, All); + +// Sets default values for this component's properties +UGGA_AttributeSystemComponent::UGGA_AttributeSystemComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + + // ... +} + + +bool UGGA_AttributeSystemComponent::ReceivePreGameplayEffectExecute(UAttributeSet* AttributeSet, FGameplayEffectModCallbackData& Data) +{ + return true; +} + +void UGGA_AttributeSystemComponent::ReceivePostGameplayEffectExecute(UAttributeSet* AttributeSet, const FGameplayEffectModCallbackData& Data) +{ + if (!AttributeSet) + { + UE_LOG(LogGGA_AttributeSystem, Error, TEXT("Owner AttributeSet isn't valid")); + return; + } + + FGameplayEffectContextHandle ContextHandle = Data.EffectSpec.GetContext(); + + FGGA_GameplayEffectModCallbackData Payload; + Payload.AttributeSet = AttributeSet; + Payload.EvaluatedData = Data.EvaluatedData; + + for (const FGameplayEffectModifiedAttribute& ModifiedAttribute : Data.EffectSpec.ModifiedAttributes) + { + FGGA_ModifiedAttribute ModAttribute; + ModAttribute.Attribute = ModifiedAttribute.Attribute; + ModAttribute.TotalMagnitude = ModifiedAttribute.TotalMagnitude; + Payload.ModifiedAttributes.Add(ModAttribute); + } + + Payload.SetByCallerNameMagnitudes = Data.EffectSpec.SetByCallerNameMagnitudes; + Payload.SetByCallerTagMagnitudes = Data.EffectSpec.SetByCallerTagMagnitudes; + + Payload.ContextHandle = ContextHandle; + Payload.InstigatorActor = Data.EffectSpec.GetContext().GetInstigator(); + + Payload.TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();; + Payload.TargetAsc = &Data.Target; + + Payload.AggregatedSourceTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags(); + Payload.AggregatedTargetTags = *Data.EffectSpec.CapturedTargetTags.GetAggregatedTags(); + OnPostGameplayEffectExecute.Broadcast(Payload); + HandlePostGameplayEffectExecute(Payload); +} + +void UGGA_AttributeSystemComponent::ReceivePreAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float& NewValue) +{ + NewValue = HandlePreAttributeChange(AttributeSet, Attribute, NewValue); +} + +void UGGA_AttributeSystemComponent::ReceivePostAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ + HandlePostAttributeChange(AttributeSet, Attribute, OldValue, NewValue); + OnPostAttributeChange.Broadcast(AttributeSet, Attribute, OldValue, NewValue); +} + +void UGGA_AttributeSystemComponent::ReceiveAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, const float& NewValue, const float& OldValue) +{ + HandleAttributeChange(AttributeSet, Attribute, NewValue, OldValue); + OnAttributeChanged.Broadcast(AttributeSet, Attribute, NewValue, OldValue); +} + +float UGGA_AttributeSystemComponent::HandlePreAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float NewValue) +{ + return NewValue; +} + +void UGGA_AttributeSystemComponent::HandlePostAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float OldValue, float NewValue) +{ +} + +void UGGA_AttributeSystemComponent::HandlePostGameplayEffectExecute_Implementation(const FGGA_GameplayEffectModCallbackData& Payload) +{ +} + +void UGGA_AttributeSystemComponent::HandleAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, const float& NewValue, const float& OldValue) +{ +} + +// Called when the game starts +void UGGA_AttributeSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... +} diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributeStructLibrary.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributeStructLibrary.cpp new file mode 100644 index 0000000..9b6a919 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributeStructLibrary.cpp @@ -0,0 +1,3 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GGA_GameplayAttributeStructLibrary.h" diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributesHelper.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributesHelper.cpp new file mode 100644 index 0000000..726c4c0 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GGA_GameplayAttributesHelper.cpp @@ -0,0 +1,235 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGA_GameplayAttributesHelper.h" +#include "AbilitySystemBlueprintLibrary.h" +#include "UObject/UObjectIterator.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" + +TMap UGGA_GameplayAttributesHelper::TagToAttributeMapping = {}; +TMap UGGA_GameplayAttributesHelper::AttributeToTagMapping = {}; + +const TArray& UGGA_GameplayAttributesHelper::GetAllGameplayAttributes() +{ + static TArray Attributes = FindGameplayAttributes(); + return Attributes; +} + +TArray UGGA_GameplayAttributesHelper::FindGameplayAttributes() +{ + TArray Attributes; + for (TObjectIterator ClassIt; ClassIt; ++ClassIt) + { + UClass* Class = *ClassIt; + if (Class->IsChildOf(UAttributeSet::StaticClass())/*&& !Class->ClassGeneratedBy*/) + { + for (TFieldIterator PropertyIt(Class, EFieldIteratorFlags::ExcludeSuper); PropertyIt; + ++PropertyIt) + { + FProperty* Property = *PropertyIt; + Attributes.Add(FGameplayAttribute(Property)); + } + } + } + + return Attributes; +} + +void UGGA_GameplayAttributesHelper::RegisterTagToAttribute(FGameplayTag Tag, FGameplayAttribute Attribute) +{ + if (Tag.IsValid() && Attribute.IsValid()) + { + if (!TagToAttributeMapping.Contains(Tag) && !AttributeToTagMapping.Contains(Attribute)) + { + TagToAttributeMapping.Emplace(Tag, Attribute); + AttributeToTagMapping.Emplace(Attribute, Tag); + } + } +} + +void UGGA_GameplayAttributesHelper::UnregisterTagToAttribute(FGameplayTag Tag, FGameplayAttribute Attribute) +{ + if (Tag.IsValid() && Attribute.IsValid()) + { + if (TagToAttributeMapping.Contains(Tag) && AttributeToTagMapping.Contains(Attribute)) + { + TagToAttributeMapping.Remove(Tag); + AttributeToTagMapping.Remove(Attribute); + } + } +} + +FGameplayAttribute UGGA_GameplayAttributesHelper::TagToAttribute(FGameplayTag Tag) +{ + return Tag.IsValid() && TagToAttributeMapping.Contains(Tag) ? TagToAttributeMapping[Tag] : FGameplayAttribute(); +} + +TArray UGGA_GameplayAttributesHelper::TagsToAttributes(TArray Tags) +{ + TArray Attributes; + for (int32 i = 0; i < Tags.Num(); i++) + { + if (TagToAttributeMapping.Contains(Tags[i])) + { + Attributes.Add(TagToAttributeMapping[Tags[i]]); + } + } + return Attributes; +} + +FGameplayTag UGGA_GameplayAttributesHelper::AttributeToTag(FGameplayAttribute Attribute) +{ + if (Attribute.IsValid()) + { + if (AttributeToTagMapping.Contains(Attribute)) + { + return AttributeToTagMapping[Attribute]; + } + } + return FGameplayTag::EmptyTag; +} + +TArray UGGA_GameplayAttributesHelper::AttributesToTags(TArray Attributes) +{ + TArray Tags; + for (int32 i = 0; i < Attributes.Num(); i++) + { + if (AttributeToTagMapping.Contains(Attributes[i])) + { + Tags.Add(AttributeToTagMapping[Attributes[i]]); + } + } + return Tags; +} + +bool UGGA_GameplayAttributesHelper::IsTagOfAttribute(FGameplayTag Tag, FGameplayAttribute Attribute) +{ + if (Tag.IsValid() && Attribute.IsValid()) + { + if (TagToAttributeMapping.Contains(Tag)) + { + return TagToAttributeMapping[Tag] == Attribute; + } + } + return false; +} + +bool UGGA_GameplayAttributesHelper::IsAttributeOfTag(FGameplayAttribute Attribute, FGameplayTag Tag) +{ + if (Tag.IsValid() && Attribute.IsValid()) + { + if (AttributeToTagMapping.Contains(Attribute)) + { + return AttributeToTagMapping[Attribute] == Tag; + } + } + return false; +} + +void UGGA_GameplayAttributesHelper::SetFloatAttribute(const AActor* Actor, FGameplayAttribute Attribute, float NewValue) +{ + if (UAbilitySystemComponent* Asc = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor)) + { + Asc->SetNumericAttributeBase(Attribute, NewValue); + } +} + +void UGGA_GameplayAttributesHelper::SetFloatAttributeOnAbilitySystemComponent(UAbilitySystemComponent* AbilitySystem, FGameplayAttribute Attribute, float NewValue) +{ + if (IsValid(AbilitySystem)) + { + AbilitySystem->SetNumericAttributeBase(Attribute, NewValue); + } +} + +float UGGA_GameplayAttributesHelper::GetFloatAttributePercentage(const AActor* Actor, FGameplayTag AttributeTagOne, FGameplayTag AttributeTagTwo, bool& bSuccessfullyFoundAttribute) +{ + bool bFoundOne = true; + bool bFoundTwo = true; + float One = GetFloatAttribute(Actor, AttributeTagOne, bFoundOne); + float Two = GetFloatAttribute(Actor, AttributeTagTwo, bFoundTwo); + bSuccessfullyFoundAttribute = bFoundOne && bFoundTwo; + if (!bFoundOne || !bFoundTwo || Two == 0) + { + return 0; + } + + return One / Two; +} + +float UGGA_GameplayAttributesHelper::GetFloatAttributePercentage_Native(const AActor* Actor, FGameplayAttribute AttributeOne, FGameplayAttribute AttributeTwo, bool& bSuccessfullyFoundAttribute) +{ + bool bFoundOne = true; + bool bFoundTwo = true; + float One = UAbilitySystemBlueprintLibrary::GetFloatAttribute(Actor, AttributeOne, bFoundOne); + float Two = UAbilitySystemBlueprintLibrary::GetFloatAttribute(Actor, AttributeTwo, bFoundTwo); + + bSuccessfullyFoundAttribute = bFoundOne && bFoundTwo; + + if (!bFoundOne || !bFoundTwo || Two == 0) + { + return 0; + } + + return One / Two; +} + +float UGGA_GameplayAttributesHelper::GetFloatAttribute(const AActor* Actor, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute) +{ + return UAbilitySystemBlueprintLibrary::GetFloatAttribute(Actor, TagToAttribute(AttributeTag), bSuccessfullyFoundAttribute); +} + +float UGGA_GameplayAttributesHelper::GetFloatAttributeBase(const AActor* Actor, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute) +{ + return UAbilitySystemBlueprintLibrary::GetFloatAttributeBase(Actor, TagToAttribute(AttributeTag), bSuccessfullyFoundAttribute); +} + +float UGGA_GameplayAttributesHelper::GetFloatAttributeFromAbilitySystemComponent(const UAbilitySystemComponent* AbilitySystem, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute) +{ + return UAbilitySystemBlueprintLibrary::GetFloatAttributeFromAbilitySystemComponent(AbilitySystem, TagToAttribute(AttributeTag), bSuccessfullyFoundAttribute); +} + +float UGGA_GameplayAttributesHelper::GetFloatAttributeBaseFromAbilitySystemComponent(const UAbilitySystemComponent* AbilitySystem, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute) +{ + return UAbilitySystemBlueprintLibrary::GetFloatAttributeBaseFromAbilitySystemComponent(AbilitySystem, TagToAttribute(AttributeTag), bSuccessfullyFoundAttribute); +} + +FString UGGA_GameplayAttributesHelper::GetDebugString() +{ + FString Output; + Output.Append(TEXT("TagToAttributeMapping:\r")); + for (auto& Pair : TagToAttributeMapping) + { + Output.Append(FString::Format(TEXT("Tag:{0}->Attribute:{1} \r"), {Pair.Key.ToString(), Pair.Value.AttributeName})); + } + return Output; +} + +FGameplayAttribute UGGA_GameplayAttributesHelper::GetAttributeFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData) +{ + return EvaluatedData.Attribute; +} + +EGameplayModOp::Type UGGA_GameplayAttributesHelper::GetModifierOpFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData) +{ + return EvaluatedData.ModifierOp; +} + +float UGGA_GameplayAttributesHelper::GetMagnitudeFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData) +{ + return EvaluatedData.Magnitude; +} + +float UGGA_GameplayAttributesHelper::GetModifiedAttributeMagnitude(const TArray& ModifiedAttributes, FGameplayAttribute InAttribute) +{ + float Delta = 0.f; + for (const FGGA_ModifiedAttribute& Mod : ModifiedAttributes) + { + if (Mod.Attribute == InAttribute) + { + Delta += Mod.TotalMagnitude; + } + } + return Delta; +} diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Private/GenericGameplayAttributes.cpp b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GenericGameplayAttributes.cpp new file mode 100644 index 0000000..2ecfd81 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Private/GenericGameplayAttributes.cpp @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericGameplayAttributes.h" + +#define LOCTEXT_NAMESPACE "FGenericGameplayAttributesModule" + +void FGenericGameplayAttributesModule::StartupModule() +{ + +} + +void FGenericGameplayAttributesModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericGameplayAttributesModule, GenericGameplayAttributes) \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Combat.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Combat.h new file mode 100644 index 0000000..ab3e8db --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Combat.h @@ -0,0 +1,170 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" + +#include "AS_Combat.generated.h" + +namespace AS_Combat +{ + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Damage) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DamageNegation) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(GuardDamageNegation) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamage) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamageNegation) + + +} + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class GENERICGAMEPLAYATTRIBUTES_API UAS_Combat : public UAttributeSet +{ + GENERATED_BODY() + + +public: + + UAS_Combat(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + + virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; + + virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override; + + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + // The damage that will apply to target + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Damage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData Damage{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, Damage) + + // The damage reduction(percentage) for incoming health damage + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_DamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData DamageNegation{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, DamageNegation) + + // The damage reduction(percentage) for incoming health damage while guarding + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GuardDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData GuardDamageNegation{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, GuardDamageNegation) + + // The stamina damage that will apply to target + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData StaminaDamage{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamage) + + // The damage reduction(percentage) for incoming stamina damage + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData StaminaDamageNegation{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamageNegation) + + + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageAttribute"), Category = "Attribute|CombatSet") + static FGameplayAttribute Bp_GetDamageAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamage"), Category = "Attribute|CombatSet") + float Bp_GetDamage() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamage"), Category = "Attribute|CombatSet") + void Bp_SetDamage(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamage"), Category = "Attribute|CombatSet") + void Bp_InitDamage(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageNegationAttribute"), Category = "Attribute|CombatSet") + static FGameplayAttribute Bp_GetDamageNegationAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamageNegation"), Category = "Attribute|CombatSet") + float Bp_GetDamageNegation() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamageNegation"), Category = "Attribute|CombatSet") + void Bp_SetDamageNegation(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamageNegation"), Category = "Attribute|CombatSet") + void Bp_InitDamageNegation(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetGuardDamageNegationAttribute"), Category = "Attribute|CombatSet") + static FGameplayAttribute Bp_GetGuardDamageNegationAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetGuardDamageNegation"), Category = "Attribute|CombatSet") + float Bp_GetGuardDamageNegation() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetGuardDamageNegation"), Category = "Attribute|CombatSet") + void Bp_SetGuardDamageNegation(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitGuardDamageNegation"), Category = "Attribute|CombatSet") + void Bp_InitGuardDamageNegation(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageAttribute"), Category = "Attribute|CombatSet") + static FGameplayAttribute Bp_GetStaminaDamageAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamage"), Category = "Attribute|CombatSet") + float Bp_GetStaminaDamage() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamage"), Category = "Attribute|CombatSet") + void Bp_SetStaminaDamage(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamage"), Category = "Attribute|CombatSet") + void Bp_InitStaminaDamage(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageNegationAttribute"), Category = "Attribute|CombatSet") + static FGameplayAttribute Bp_GetStaminaDamageNegationAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamageNegation"), Category = "Attribute|CombatSet") + float Bp_GetStaminaDamageNegation() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamageNegation"), Category = "Attribute|CombatSet") + void Bp_SetStaminaDamageNegation(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamageNegation"), Category = "Attribute|CombatSet") + void Bp_InitStaminaDamageNegation(float NewValue); + + + + +protected: + + /** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */ + virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty); + + UFUNCTION() + virtual void OnRep_Damage(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_DamageNegation(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_GuardDamageNegation(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_StaminaDamage(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_StaminaDamageNegation(const FGameplayAttributeData& OldValue); + +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Health.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Health.h new file mode 100644 index 0000000..1443831 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Health.h @@ -0,0 +1,135 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" + +#include "AS_Health.generated.h" + +namespace AS_Health +{ + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Health) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MaxHealth) + + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(IncomingHealing) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(IncomingDamage) + +} + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class GENERICGAMEPLAYATTRIBUTES_API UAS_Health : public UAttributeSet +{ + GENERATED_BODY() + + +public: + + UAS_Health(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + + virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; + + virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override; + + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + // Current health of an actor.(actor的当前生命值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Attribute|HealthSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData Health{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, Health) + + // Max health value of an actor.(actor的最大生命值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Attribute|HealthSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData MaxHealth{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, MaxHealth) + + + + // Incoming healing. This is mapped directly to +Health.(即将到来的恢复值,映射为+Health) + UPROPERTY(BlueprintReadOnly,Category = "Attribute|HealthSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData IncomingHealing{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, IncomingHealing) + + // Incoming damage. This is mapped directly to -Health(即将到来的伤害值,映射为-Health) + UPROPERTY(BlueprintReadOnly,Category = "Attribute|HealthSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData IncomingDamage{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, IncomingDamage) + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetHealthAttribute"), Category = "Attribute|HealthSet") + static FGameplayAttribute Bp_GetHealthAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetHealth"), Category = "Attribute|HealthSet") + float Bp_GetHealth() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetHealth"), Category = "Attribute|HealthSet") + void Bp_SetHealth(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitHealth"), Category = "Attribute|HealthSet") + void Bp_InitHealth(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetMaxHealthAttribute"), Category = "Attribute|HealthSet") + static FGameplayAttribute Bp_GetMaxHealthAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetMaxHealth"), Category = "Attribute|HealthSet") + float Bp_GetMaxHealth() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetMaxHealth"), Category = "Attribute|HealthSet") + void Bp_SetMaxHealth(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitMaxHealth"), Category = "Attribute|HealthSet") + void Bp_InitMaxHealth(float NewValue); + + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetIncomingHealingAttribute"), Category = "Attribute|HealthSet") + static FGameplayAttribute Bp_GetIncomingHealingAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetIncomingHealing"), Category = "Attribute|HealthSet") + float Bp_GetIncomingHealing() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetIncomingHealing"), Category = "Attribute|HealthSet") + void Bp_SetIncomingHealing(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetIncomingDamageAttribute"), Category = "Attribute|HealthSet") + static FGameplayAttribute Bp_GetIncomingDamageAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetIncomingDamage"), Category = "Attribute|HealthSet") + float Bp_GetIncomingDamage() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetIncomingDamage"), Category = "Attribute|HealthSet") + void Bp_SetIncomingDamage(float NewValue); + + +protected: + + /** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */ + virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty); + + UFUNCTION() + virtual void OnRep_Health(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldValue); + +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Mana.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Mana.h new file mode 100644 index 0000000..4cdad39 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Mana.h @@ -0,0 +1,101 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" + +#include "AS_Mana.generated.h" + +namespace AS_Mana +{ + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Mana) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MaxMana) + + +} + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class GENERICGAMEPLAYATTRIBUTES_API UAS_Mana : public UAttributeSet +{ + GENERATED_BODY() + + +public: + + UAS_Mana(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + + virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; + + virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override; + + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + // Current mana of an actor.(actor的当前魔法值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Mana, Category = "Attribute|ManaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData Mana{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, Mana) + + // Max mana value of an actor.(actor的最大魔法值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxMana, Category = "Attribute|ManaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData MaxMana{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, MaxMana) + + + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetManaAttribute"), Category = "Attribute|ManaSet") + static FGameplayAttribute Bp_GetManaAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetMana"), Category = "Attribute|ManaSet") + float Bp_GetMana() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetMana"), Category = "Attribute|ManaSet") + void Bp_SetMana(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitMana"), Category = "Attribute|ManaSet") + void Bp_InitMana(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetMaxManaAttribute"), Category = "Attribute|ManaSet") + static FGameplayAttribute Bp_GetMaxManaAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetMaxMana"), Category = "Attribute|ManaSet") + float Bp_GetMaxMana() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetMaxMana"), Category = "Attribute|ManaSet") + void Bp_SetMaxMana(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitMaxMana"), Category = "Attribute|ManaSet") + void Bp_InitMaxMana(float NewValue); + + + + +protected: + + /** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */ + virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty); + + UFUNCTION() + virtual void OnRep_Mana(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_MaxMana(const FGameplayAttributeData& OldValue); + +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Stamina.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Stamina.h new file mode 100644 index 0000000..44f518c --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/Attributes/AS_Stamina.h @@ -0,0 +1,135 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" + +#include "AS_Stamina.generated.h" + +namespace AS_Stamina +{ + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Stamina) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MaxStamina) + + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(IncomingHealing) + + GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(IncomingDamage) + +} + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ +GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class GENERICGAMEPLAYATTRIBUTES_API UAS_Stamina : public UAttributeSet +{ + GENERATED_BODY() + + +public: + + UAS_Stamina(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + + virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; + + virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override; + + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + // Current stamina of an actor.(actor的当前生命值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Stamina, Category = "Attribute|StaminaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData Stamina{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, Stamina) + + // Max stamina value of an actor.(actor的最大生命值) + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxStamina, Category = "Attribute|StaminaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData MaxStamina{ 100 }; + ATTRIBUTE_ACCESSORS(ThisClass, MaxStamina) + + + + // Incoming healing. This is mapped directly to +Stamina.(即将到来的恢复值,映射为+Stamina) + UPROPERTY(BlueprintReadOnly,Category = "Attribute|StaminaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData IncomingHealing{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, IncomingHealing) + + // Incoming damage. This is mapped directly to -Stamina(即将到来的伤害值,映射为-Stamina) + UPROPERTY(BlueprintReadOnly,Category = "Attribute|StaminaSet", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData IncomingDamage{ 0.0 }; + ATTRIBUTE_ACCESSORS(ThisClass, IncomingDamage) + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaAttribute"), Category = "Attribute|StaminaSet") + static FGameplayAttribute Bp_GetStaminaAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetStamina"), Category = "Attribute|StaminaSet") + float Bp_GetStamina() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStamina"), Category = "Attribute|StaminaSet") + void Bp_SetStamina(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStamina"), Category = "Attribute|StaminaSet") + void Bp_InitStamina(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetMaxStaminaAttribute"), Category = "Attribute|StaminaSet") + static FGameplayAttribute Bp_GetMaxStaminaAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetMaxStamina"), Category = "Attribute|StaminaSet") + float Bp_GetMaxStamina() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetMaxStamina"), Category = "Attribute|StaminaSet") + void Bp_SetMaxStamina(float NewValue); + + UFUNCTION(BlueprintCallable,meta=(DisplayName="InitMaxStamina"), Category = "Attribute|StaminaSet") + void Bp_InitMaxStamina(float NewValue); + + + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetIncomingHealingAttribute"), Category = "Attribute|StaminaSet") + static FGameplayAttribute Bp_GetIncomingHealingAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetIncomingHealing"), Category = "Attribute|StaminaSet") + float Bp_GetIncomingHealing() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetIncomingHealing"), Category = "Attribute|StaminaSet") + void Bp_SetIncomingHealing(float NewValue); + + + UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetIncomingDamageAttribute"), Category = "Attribute|StaminaSet") + static FGameplayAttribute Bp_GetIncomingDamageAttribute(); + + UFUNCTION(BlueprintPure,meta=(DisplayName="GetIncomingDamage"), Category = "Attribute|StaminaSet") + float Bp_GetIncomingDamage() const; + + UFUNCTION(BlueprintCallable,meta=(DisplayName="SetIncomingDamage"), Category = "Attribute|StaminaSet") + void Bp_SetIncomingDamage(float NewValue); + + +protected: + + /** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */ + virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty); + + UFUNCTION() + virtual void OnRep_Stamina(const FGameplayAttributeData& OldValue); + + UFUNCTION() + virtual void OnRep_MaxStamina(const FGameplayAttributeData& OldValue); + +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_AttributeSystemComponent.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_AttributeSystemComponent.h new file mode 100644 index 0000000..ac32856 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_AttributeSystemComponent.h @@ -0,0 +1,98 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGA_GameplayAttributeStructLibrary.h" +#include "Components/ActorComponent.h" +#include "GGA_AttributeSystemComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FGGA_OnPostAttributeChangeSignature, UAttributeSet*, AttributeSet, const FGameplayAttribute&, Attribute, const float, OldValue, const float, NewValue); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FGGA_OnAttributeChangedSignature, UAttributeSet*, AttributeSet, const FGameplayAttribute&, Attribute, const float, NewValue, float, PrevValue); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGGA_OnPostGameplayEffectExecuteSignature, const FGGA_GameplayEffectModCallbackData&, Data); + +/** + * The main component for reacting to changes in gameplay attributes. + * 对游戏属性变化做出反应的主要组件。 + */ +UCLASS(ClassGroup=(GGA), BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) +class GENERICGAMEPLAYATTRIBUTES_API UGGA_AttributeSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UGGA_AttributeSystemComponent(); + + void ReceivePreAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float& NewValue); + + void ReceivePostAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float OldValue, float NewValue); + + bool ReceivePreGameplayEffectExecute(UAttributeSet* AttributeSet, FGameplayEffectModCallbackData& Data); + + void ReceivePostGameplayEffectExecute(UAttributeSet* AttributeSet, const FGameplayEffectModCallbackData& Data); + + void ReceiveAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, const float& NewValue, const float& OldValue); + + + UPROPERTY(BlueprintAssignable, Category="Event") + FGGA_OnPostGameplayEffectExecuteSignature OnPostGameplayEffectExecute; + + UPROPERTY(BlueprintAssignable, Category="Event") + FGGA_OnPostAttributeChangeSignature OnPostAttributeChange; + + /** + * Called whenever an attribute changed, And this event will be fired on both serve and client. + * 属性发生任意变化后调用,此事件会在服务端和客户端都进行调用。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FGGA_OnAttributeChangedSignature OnAttributeChanged; + +protected: + /** + * Called just before any modification happens to an attribute. This is lower level than PreAttributeChange/PostAttributeChange. + * There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc. + * This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc". + * You should return your modified new value. + * + * 在对属性进行任何修改之前调用。它比 PreAttributeChange/PostAttributeChange 更底层。 + * 这里没有提供额外的上下文,因为任何事情都可能触发它。已执行的效果、基于持续时间的效果、效果被移除、豁免被应用、堆叠规则改变等。 + * 该函数旨在执行 “Health = Clamp(Health,0,MaxHealth) ”之类的操作,而不是 “如果受到伤害,则触发该额外操作 ”之类的操作。 + * 你应返回修改后的新值。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGA") + float HandlePreAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float NewValue); + virtual float HandlePreAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float NewValue); + + /** + * Called just after any modification happens to an attribute. + * 在属性被修改后调用。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGA") + void HandlePostAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float OldValue, float NewValue); + virtual void HandlePostAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, float OldValue, float NewValue); + + /** + * Called just after a GameplayEffect is executed to modify the base value of an attribute. No more changes can be made. + * Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff. + * 在执行 GameplayEffect 之后调用,以修改属性的基本值。不能再做任何更改。 + * 注意只有在 “执行 ”过程中才会调用。例如,修改属性的 “基本值”。在应用 GameplayEffect(如 5 秒 +10 移动速度的缓冲)时不会调用。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGA") + void HandlePostGameplayEffectExecute(const FGGA_GameplayEffectModCallbackData& Payload); + virtual void HandlePostGameplayEffectExecute_Implementation(const FGGA_GameplayEffectModCallbackData& Payload); + + + /** + * Called whenever an attribute changed, And this event will be fired on both serve and client. + * 属性发生任意变化后调用,此事件会在服务端和客户端都进行调用。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGA") + void HandleAttributeChange(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, const float& NewValue, const float& OldValue); + virtual void HandleAttributeChange_Implementation(UAttributeSet* AttributeSet, const FGameplayAttribute& Attribute, const float& NewValue, const float& OldValue); + + // Called when the game starts + virtual void BeginPlay() override; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributeStructLibrary.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributeStructLibrary.h new file mode 100644 index 0000000..6091e7d --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributeStructLibrary.h @@ -0,0 +1,102 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "AttributeSet.h" +#include "GameplayEffectTypes.h" +#include "GGA_GameplayAttributeStructLibrary.generated.h" + +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYATTRIBUTES_API FGGA_ModifiedAttribute +{ + GENERATED_BODY() + + /** The attribute that has been modified */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + FGameplayAttribute Attribute; + + /** Total magnitude applied to that attribute */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCS") + float TotalMagnitude{0}; +}; + + +/** + * Information about PostGameplayEffectExecute event with related objects cached. + * 封装后的关于PostGameplayEffectExecute的回调数据,并提取相关对象以方便调用。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMEPLAYATTRIBUTES_API FGGA_GameplayEffectModCallbackData +{ + GENERATED_BODY() + + /** + * The owner AttributeSet from which the event was invoked + * 变化的属性所属的AttributeSet. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TObjectPtr AttributeSet = nullptr; + + /** + * Evaluated data about this change. + * 关于这次变化的结果 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayModifierEvaluatedData EvaluatedData; + + /** + * Any modified attributes. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TArray ModifiedAttributes; + + /** + * Map of set by caller magnitudes + * 执行过程中设置的临时值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TMap SetByCallerNameMagnitudes; + + /** + * Map of set by caller magnitudes + * 执行过程中设置的临时值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TMap SetByCallerTagMagnitudes; + + /** + * The context of The effect spec that the mod came from + * 触发这次修改的效果实例的上下文。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayEffectContextHandle ContextHandle; + + /** + * The instigator actor within context. + * 上下文中的Instigator Actor. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TWeakObjectPtr InstigatorActor = nullptr; + + /** + * The target actor within context. + * 上下文中的目标Actor。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TWeakObjectPtr TargetActor = nullptr; + + /** + * Target we intend to apply to + * 该效果的施加对象。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + TObjectPtr TargetAsc = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayTagContainer AggregatedSourceTags = FGameplayTagContainer::EmptyContainer; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GGA") + FGameplayTagContainer AggregatedTargetTags = FGameplayTagContainer::EmptyContainer; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributesHelper.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributesHelper.h new file mode 100644 index 0000000..43cf884 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GGA_GameplayAttributesHelper.h @@ -0,0 +1,140 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "GameplayEffectTypes.h" +#include "GameplayTagContainer.h" +#include "GGA_GameplayAttributeStructLibrary.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGA_GameplayAttributesHelper.generated.h" + +/** + * + */ +UCLASS(meta=(DisplayName="GGA Gameplay Attribute Function Library")) +class GENERICGAMEPLAYATTRIBUTES_API UGGA_GameplayAttributesHelper : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Gets all gameplay attributes of all attribute sets (attributes declared in 'UAbilitySystemComponent' not included). + * 获取项目中的所有GameplayAttribute. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGA|GameplayAttribute") + static const TArray& GetAllGameplayAttributes(); + +private: + static TArray FindGameplayAttributes(); + +public: + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayAttribute") + static void RegisterTagToAttribute(FGameplayTag Tag, FGameplayAttribute Attribute); + + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayAttribute") + static void UnregisterTagToAttribute(FGameplayTag Tag, FGameplayAttribute Attribute); + + /** + * Convert gameplay tag to gameplay attribute. + * @param Tag The tag to query. + * @return The gameplay attribute associated with Tag. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static FGameplayAttribute TagToAttribute(FGameplayTag Tag); + + /** + * Convert gameplay tags to gameplay attributes. + * @param Tags The attribute tags to query. + * @return The gameplay attributes associated with Tags. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static TArray TagsToAttributes(TArray Tags); + + /** + * Convert gameplay attribute to gameplay tag. + * @param Attribute The attribute to query. + * @return The gameplay tag associated with Attribute. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static FGameplayTag AttributeToTag(FGameplayAttribute Attribute); + + /** + * Convert gameplay attributes to gameplay tags. + * @param Attributes The attributes to query. + * @return The gameplay tags associated with Attributes. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static TArray AttributesToTags(TArray Attributes); + + /** + * Check if tag is associated with attribute. + * @param Tag Tag to check + * @param Attribute Attribute to check + * @return true if they are associated. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static bool IsTagOfAttribute(FGameplayTag Tag, FGameplayAttribute Attribute); + + + /** + * Check if attribute is associated with tag . + * @param Attribute Attribute to check + * @param Tag Tag to check + * @return true if they are associated. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static bool IsAttributeOfTag(FGameplayAttribute Attribute, FGameplayTag Tag); + + /** + * Set float attribute for actor. + */ + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayAttribute") + static void SetFloatAttribute(const AActor* Actor, FGameplayAttribute Attribute, float NewValue); + + UFUNCTION(BlueprintCallable, Category = "GGA|GameplayAttribute", meta=(DisplayName="Set Float Attribute on Asc")) + static void SetFloatAttributeOnAbilitySystemComponent(UAbilitySystemComponent* AbilitySystem, FGameplayAttribute Attribute, float NewValue); + + /** Returns the percentage of Attributes from the ability system component belonging to Actor. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute", meta=(DisplayName="Get Float Attribute Percentage(With Tag)")) + static float GetFloatAttributePercentage(const AActor* Actor, FGameplayTag AttributeTagOne, FGameplayTag AttributeTagTwo, bool& bSuccessfullyFoundAttribute); + + /** Returns the percentage of Attributes from the ability system component belonging to Actor. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute", meta=(DisplayName="Get Float Attribute Percentage")) + static float GetFloatAttributePercentage_Native(const AActor* Actor, FGameplayAttribute AttributeOne, FGameplayAttribute AttributeTwo, bool& bSuccessfullyFoundAttribute); + + /** Returns the value of Attribute from the ability system component belonging to Actor. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static float GetFloatAttribute(const AActor* Actor, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static float GetFloatAttributeBase(const AActor* Actor, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute); + + /** Returns the value of Attribute from the ability system component AbilitySystem. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute", meta=(DisplayName="Get Float Attribute from Asc")) + static float GetFloatAttributeFromAbilitySystemComponent(const UAbilitySystemComponent* AbilitySystem, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute", meta=(DisplayName="Get Float Attribute Base from Asc")) + static float GetFloatAttributeBaseFromAbilitySystemComponent(const UAbilitySystemComponent* AbilitySystem, FGameplayTag AttributeTag, bool& bSuccessfullyFoundAttribute); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static FString GetDebugString(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static FGameplayAttribute GetAttributeFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static EGameplayModOp::Type GetModifierOpFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGA|GameplayAttribute") + static float GetMagnitudeFromEvaluatedData(const FGameplayModifierEvaluatedData& EvaluatedData); + + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category = "GGA|GameplayAttribute") + static float GetModifiedAttributeMagnitude(const TArray& ModifiedAttributes, FGameplayAttribute InAttribute); + +protected: + static TMap TagToAttributeMapping; + + static TMap AttributeToTagMapping; +}; diff --git a/Plugins/GCS/Source/GenericGameplayAttributes/Public/GenericGameplayAttributes.h b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GenericGameplayAttributes.h new file mode 100644 index 0000000..08bae61 --- /dev/null +++ b/Plugins/GCS/Source/GenericGameplayAttributes/Public/GenericGameplayAttributes.h @@ -0,0 +1,13 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FGenericGameplayAttributesModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/GenericInputSystem.Build.cs b/Plugins/GCS/Source/GenericInputSystem/GenericInputSystem.Build.cs new file mode 100644 index 0000000..c673325 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/GenericInputSystem.Build.cs @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericInputSystem : ModuleRules +{ + public GenericInputSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "EnhancedInput", + "GameplayTags" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "InputCore", + "GameplayDebugger" + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/Actions/GIPS_AsyncAction_ListenInputEvent.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/Actions/GIPS_AsyncAction_ListenInputEvent.cpp new file mode 100644 index 0000000..ab3de5e --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/Actions/GIPS_AsyncAction_ListenInputEvent.cpp @@ -0,0 +1,73 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Actions/GIPS_AsyncAction_ListenInputEvent.h" +#include "Engine/Engine.h" +#include "GIPS_InputSystemComponent.h" + +UGIPS_AsyncAction_ListenInputEvent* UGIPS_AsyncAction_ListenInputEvent::ListenInputEvent(UObject* WorldContextObject, UGIPS_InputSystemComponent* InputSystemComponent, + FGameplayTagContainer InputTagsToListen, TArray EventsToListen, bool bListenForBufferedInput, + bool bExactMatch) +{ + if (!IsValid(InputSystemComponent)) + { + FFrame::KismetExecutionMessage(TEXT("ListenInputEvent was passed a null InputSystemComponent"), ELogVerbosity::Error); + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + + UGIPS_AsyncAction_ListenInputEvent* Action = NewObject(); + Action->Input = InputSystemComponent; + Action->InputTags = InputTagsToListen; + Action->bForBufferedInput = bListenForBufferedInput; + Action->TriggerEvents = EventsToListen; + Action->RegisterWithGameInstance(World); + + return Action; +} + +void UGIPS_AsyncAction_ListenInputEvent::Activate() +{ + if (bForBufferedInput) + { + Input->OnFireBufferedInput.AddDynamic(this, &ThisClass::HandleInput); + } + else + { + Input->OnReceivedInput.AddDynamic(this, &ThisClass::HandleInput); + } +} + +void UGIPS_AsyncAction_ListenInputEvent::HandleInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (bExact ? InputTags.HasTagExact(InputTag) : InputTags.HasTag(InputTag)) + { + if (TriggerEvents.Contains(TriggerEvent)) + { + OnReceivedInput.Broadcast(ActionData, InputTag, TriggerEvent); + } + } +} + +void UGIPS_AsyncAction_ListenInputEvent::Cancel() +{ + if (Input.IsValid()) + { + if (bForBufferedInput) + { + if (Input->OnFireBufferedInput.IsAlreadyBound(this, &ThisClass::HandleInput)) + { + Input->OnFireBufferedInput.RemoveDynamic(this, &ThisClass::HandleInput); + } + } + else + { + if (Input->OnFireBufferedInput.IsAlreadyBound(this, &ThisClass::HandleInput)) + { + Input->OnFireBufferedInput.RemoveDynamic(this, &ThisClass::HandleInput); + } + } + Super::Cancel(); + } +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_GameplayDebugger.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_GameplayDebugger.cpp new file mode 100644 index 0000000..3114fe4 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_GameplayDebugger.cpp @@ -0,0 +1,280 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_GameplayDebugger.h" + +#if WITH_GAMEPLAY_DEBUGGER +#include "GIPS_InputConfig.h" +#include "GIPS_InputControlSetup.h" +#include "GIPS_InputFunctionLibrary.h" +#include "GIPS_InputSystemComponent.h" +#include "Engine/Canvas.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" + + +FGIPS_GameplayDebuggerCategory_Input::FGIPS_GameplayDebuggerCategory_Input() +{ + SetDataPackReplication(&DataPack); + const FName KeyNameOne{"One"}; + const FName KeyNameTwo{"Two"}; + const FName KeyNameThree{"Three"}; + const FName KeyNameFour{"Four"}; + + BindKeyPress(KeyNameOne, FGameplayDebuggerInputModifier::Shift, this, &FGIPS_GameplayDebuggerCategory_Input::OnShowInputBuffersToggle, EGameplayDebuggerInputMode::Local); + BindKeyPress(KeyNameTwo, FGameplayDebuggerInputModifier::Shift, this, &FGIPS_GameplayDebuggerCategory_Input::OnShowPassedInputEntriesToggle, EGameplayDebuggerInputMode::Local); + BindKeyPress(KeyNameThree, FGameplayDebuggerInputModifier::Shift, this, &FGIPS_GameplayDebuggerCategory_Input::OnShowBlockedInputEntriesToggle, EGameplayDebuggerInputMode::Local); + BindKeyPress(KeyNameFour, FGameplayDebuggerInputModifier::Shift, this, &FGIPS_GameplayDebuggerCategory_Input::OnShowBufferedInputEntriesToggle, EGameplayDebuggerInputMode::Local); +} + +void FGIPS_GameplayDebuggerCategory_Input::CollectData(APlayerController* OwnerPC, AActor* DebugActor) +{ + if (const UGIPS_InputSystemComponent* InputSystem = UGIPS_InputSystemComponent::GetInputSystemComponent(DebugActor)) + { + if (UGIPS_InputControlSetup* InputControlSetup = InputSystem->GetCurrentInputSetup()) + { + if (UGIPS_InputConfig* InputConfig = InputSystem->GetInputConfig()) + { + DataPack.ActorName = OwnerPC->GetPawn()->GetName(); + DataPack.InputConfig = InputConfig->GetName(); + DataPack.InputControlSetup = InputControlSetup->GetName(); + + TMap ActiveWindows = InputSystem->GetActiveBufferWindows(); + + for (const FGIPS_InputBufferWindow& Definition : InputConfig->InputBufferDefinitions) + { + FRepData::FInputBuffersDebug ItemData; + + ItemData.WindowName = UGIPS_InputFunctionLibrary::GetLastTagName(Definition.Tag); + ItemData.bIsActive = ActiveWindows.Contains(Definition.Tag); + ItemData.InputTagName = ItemData.bIsActive && ActiveWindows[Definition.Tag].InputTag.IsValid() + ? UGIPS_InputFunctionLibrary::GetLastTagName(ActiveWindows[Definition.Tag].InputTag) + : NAME_None; + ItemData.InputEvent = ItemData.bIsActive ? ActiveWindows[Definition.Tag].TriggerEvent : ETriggerEvent::None; + + DataPack.InputBuffers.Add(ItemData); + } + FGIPS_BufferedInput Input = InputSystem->GetLastBufferedInput(); + DataPack.BufferedInputTag = Input.InputTag; + } + } + } +} + +void FGIPS_GameplayDebuggerCategory_Input::DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext) +{ + // Draw the sub-category bindings inline with the category header + { + CanvasContext.CursorX += 200.0f; + CanvasContext.CursorY -= CanvasContext.GetLineHeight(); + const TCHAR* Active = TEXT("{green}"); + const TCHAR* Inactive = TEXT("{grey}"); + CanvasContext.Printf(TEXT("InputBuffers [%s%s{white}]\t PassedInputEntries [%s%s{white}]\t BlockedInputEntries [%s%s{white}]\t BufferedInputEntries [%s%s{white}]\t"), + bShowInputBuffers ? Active : Inactive, *GetInputHandlerDescription(0), + bShowPassedInputEntries ? Active : Inactive, *GetInputHandlerDescription(1), + bShowBlockedInputEntries ? Active : Inactive, *GetInputHandlerDescription(2), + bShowBufferedInputEntries ? Active : Inactive, *GetInputHandlerDescription(3) + ); + } + + if (LastDrawDataEndSize <= 0.0f) + { + // Default to the full frame size + LastDrawDataEndSize = CanvasContext.Canvas->SizeY - CanvasContext.CursorY - CanvasContext.CursorX; + } + + constexpr FLinearColor BackgroundColor(0.1f, 0.1f, 0.1f, 0.8f); + const FVector2D BackgroundPos{CanvasContext.CursorX, CanvasContext.CursorY}; + const FVector2D BackgroundSize(CanvasContext.Canvas->SizeX - (2.0f * CanvasContext.CursorX), LastDrawDataEndSize); + + // Draw a transparent dark background so that the text is easier to look at + FCanvasTileItem Background(FVector2D(0.0f), BackgroundSize, BackgroundColor); + Background.BlendMode = SE_BLEND_Translucent; + + CanvasContext.DrawItem(Background, BackgroundPos.X, BackgroundPos.Y); + + CanvasContext.Printf(TEXT("{white}Actor name: {yellow}%s \t{white}Config: {yellow}%s \t{white}Control: {yellow}%s"), *DataPack.ActorName, *DataPack.InputConfig, *DataPack.InputControlSetup); + + if (bShowInputBuffers) + { + DrawInputBuffers(CanvasContext, OwnerPC); + } + + DrawInputEntries(CanvasContext, OwnerPC); +} + +void FGIPS_GameplayDebuggerCategory_Input::FRepData::Serialize(FArchive& Ar) +{ + Ar << ActorName; + Ar << InputConfig; + Ar << InputControlSetup; +} + +TSharedRef FGIPS_GameplayDebuggerCategory_Input::MakeInstance() +{ + return MakeShareable(new FGIPS_GameplayDebuggerCategory_Input()); +} + +void FGIPS_GameplayDebuggerCategory_Input::OnShowInputBuffersToggle() +{ + bShowInputBuffers = !bShowInputBuffers; +} + +void FGIPS_GameplayDebuggerCategory_Input::OnShowPassedInputEntriesToggle() +{ + bShowPassedInputEntries = !bShowPassedInputEntries; +} + +void FGIPS_GameplayDebuggerCategory_Input::OnShowBlockedInputEntriesToggle() +{ + bShowBlockedInputEntries = !bShowBlockedInputEntries; +} + +void FGIPS_GameplayDebuggerCategory_Input::OnShowBufferedInputEntriesToggle() +{ + bShowBufferedInputEntries = !bShowBufferedInputEntries; +} + +void FGIPS_GameplayDebuggerCategory_Input::DrawInputBuffers(FGameplayDebuggerCanvasContext& CanvasContext, const APlayerController* OwnerPC) const +{ + const float CanvasWidth = CanvasContext.Canvas->SizeX; + + int32 NumActive = 0; + for (const FRepData::FInputBuffersDebug& ItemData : DataPack.InputBuffers) + { + NumActive += ItemData.bIsActive; + } + + // Measure the individual string sizes, so that we can size the columns properly + // We're picking a long-enough name for the object name sizes + constexpr float Padding = 10.0f; + static float WindowNameSize = 0.0f, ActiveNameSize = 0.0f, InputTagSize = 0.0f, InputEventSize = 0.0f; + + if (WindowNameSize <= 0.0f) + { + float TempSizeY = 0.0f; + + // We have to actually use representative strings because of the kerning + CanvasContext.MeasureString(TEXT("WindowName: Combo"), WindowNameSize, TempSizeY); + CanvasContext.MeasureString(TEXT("Active: False"), ActiveNameSize, TempSizeY); + CanvasContext.MeasureString(TEXT("InputTag: InputTag.Attack"), InputTagSize, TempSizeY); + CanvasContext.MeasureString(TEXT("InputEvent: Triggered"), InputEventSize, TempSizeY); + + WindowNameSize += Padding; + } + + const float ColumnWidth = WindowNameSize * 2 + ActiveNameSize + InputTagSize + InputEventSize; + const int NumColumns = FMath::Max(1, FMath::FloorToInt(CanvasWidth / ColumnWidth)); + + CanvasContext.Print(TEXT("Input Buffers:")); + CanvasContext.CursorX += 200.0f; + CanvasContext.CursorY -= CanvasContext.GetLineHeight(); + CanvasContext.Printf(TEXT("Legend: {yellow}Total [%d] {cyan}Active [%d]"), DataPack.InputBuffers.Num(), NumActive); + + CanvasContext.CursorX += Padding; + + for (const FRepData::FInputBuffersDebug& ItemData : DataPack.InputBuffers) + { + float CursorX = CanvasContext.CursorX; + float CursorY = CanvasContext.CursorY; + // Print positions manually to align them properly + CanvasContext.PrintAt(CursorX + WindowNameSize * 0, CursorY, ItemData.bIsActive ? FColor::Cyan : FColor::Yellow, ItemData.WindowName.ToString()); + CanvasContext.PrintAt(CursorX + WindowNameSize * 1, CursorY, FString::Printf(TEXT("{grey}active: {white}%.35s"), ItemData.bIsActive ? TEXT("True") : TEXT("False"))); + CanvasContext.PrintAt(CursorX + WindowNameSize * 2, CursorY, FString::Printf(TEXT("{grey}InputTag: {white}%s"), *ItemData.InputTagName.ToString())); + CanvasContext.PrintAt(CursorX + WindowNameSize * 3, CursorY, + FString::Printf(TEXT("{grey}InputEvent: {white}%s"), *UGIPS_InputFunctionLibrary::GetTriggerEventString(ItemData.InputEvent))); + + // PrintAt would have reset these values, restore them. + CanvasContext.CursorX = CursorX + (CanvasWidth / NumColumns); + CanvasContext.CursorY = CursorY; + + // If we're going to overflow, go to the next line... + if (CanvasContext.CursorX + ColumnWidth >= CanvasWidth) + { + CanvasContext.MoveToNewLine(); + CanvasContext.CursorX += Padding; + } + } + + // End the row with a newline + if (CanvasContext.CursorX != CanvasContext.DefaultX) + { + CanvasContext.MoveToNewLine(); + } + + CanvasContext.Printf(TEXT("{grey}Last triggered input: {white}%s"), *(DataPack.BufferedInputTag.IsValid() ? DataPack.BufferedInputTag.GetTagName().ToString() : TEXT("None"))); + + // End the category with a newline to separate from the other categories + CanvasContext.MoveToNewLine(); +} + +void FGIPS_GameplayDebuggerCategory_Input::DrawInputEntries(FGameplayDebuggerCanvasContext& CanvasContext, const APlayerController* OwnerPC) const +{ + const UGIPS_InputSystemComponent* InputSystem = UGIPS_InputSystemComponent::GetInputSystemComponent(FindLocalDebugActor()); + if (InputSystem == nullptr || (!bShowPassedInputEntries && !bShowBlockedInputEntries && !bShowBufferedInputEntries)) + return; + + const float CanvasWidth = CanvasContext.Canvas->SizeX; + + constexpr float Padding = 10.0f; + + constexpr float ColumnSize = 150; + constexpr float ColumnWidth = ColumnSize * 3; + const int NumColumns = FMath::Max(1, FMath::FloorToInt(CanvasContext.Canvas->SizeX / ColumnWidth)); + + auto DrawEntries = [&](const FString& Header, const TArray& Entries) + { + CanvasContext.Printf(TEXT("%s"), *Header); + + CanvasContext.CursorX += Padding; + + for (int i = 0; i < Entries.Num(); ++i) + { + const FGIPS_BufferedInput& Entry = Entries[i]; + float CursorX = CanvasContext.CursorX; + float CursorY = CanvasContext.CursorY; + // Print positions manually to align them properly + CanvasContext.PrintAt(CursorX + ColumnSize * 0, CursorY, i == Entries.Num() - 1 ? FColor::Cyan : FColor::Yellow, + Entry.InputTag.IsValid() ? *UGIPS_InputFunctionLibrary::GetLastTagName(Entry.InputTag).ToString() : TEXT("None")); + CanvasContext.PrintAt(CursorX + ColumnSize * 1, CursorY, FString::Printf(TEXT("{grey}TriggerEvent: {white}%s"), *UGIPS_InputFunctionLibrary::GetTriggerEventString(Entry.TriggerEvent))); + CanvasContext.PrintAt(CursorX + ColumnSize * 2, CursorY, FString::Printf(TEXT("{grey}ActionValue: {white}%s"), *Entry.ActionData.GetValue().ToString())); + + // PrintAt would have reset these values, restore them. + CanvasContext.CursorX = CursorX + (CanvasWidth / NumColumns); + CanvasContext.CursorY = CursorY; + + CanvasContext.MoveToNewLine(); + CanvasContext.CursorX += Padding; + } + + // End the row with a newline + if (CanvasContext.CursorX != CanvasContext.DefaultX) + { + CanvasContext.MoveToNewLine(); + } + }; + + if (bShowPassedInputEntries) + { + auto Entries = InputSystem->GetPassedInputEntries(); + DrawEntries(TEXT("Passed Inputs:"), Entries); + } + + if (bShowBlockedInputEntries) + { + auto Entries = InputSystem->GetBlockedInputEntries(); + DrawEntries(TEXT("Blocked Inputs:"), Entries); + } + + if (bShowBufferedInputEntries) + { + auto Entries = InputSystem->GetBufferedInputEntries(); + DrawEntries(TEXT("Buffered Inputs:"), Entries); + } + + // End the category with a newline to separate from the other categories + CanvasContext.MoveToNewLine(); +} + +#endif diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputChecker.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputChecker.cpp new file mode 100644 index 0000000..eca0221 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputChecker.cpp @@ -0,0 +1,94 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_InputChecker.h" +#include "GameFramework/Actor.h" +#include "GameplayTagAssetInterface.h" +#include "GIPS_InputSystemComponent.h" + +bool UGIPS_InputChecker::CheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const +{ + return DoCheckInput(IC, ActionData, InputTag, TriggerEvent); +} + +bool UGIPS_InputChecker::DoCheckInput_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, + const ETriggerEvent& TriggerEvent) const +{ + return true; +} + +FGameplayTagContainer UGIPS_InputChecker_TagRelationship::GetActorTags_Implementation(UGIPS_InputSystemComponent* IC) const +{ + FGameplayTagContainer Tags; + if (const IGameplayTagAssetInterface* TagAssetInterface = Cast(IC->GetOwner())) + { + TagAssetInterface->GetOwnedGameplayTags(Tags); + } + return Tags; +} + +bool UGIPS_InputChecker_TagRelationship::DoCheckInput_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, + const ETriggerEvent& TriggerEvent) const +{ + const FGameplayTagContainer ActorOwnedTags = GetActorTags(IC); + + for (const FGIPS_InputTagRelationship& Relationship : InputTagRelationships) + { + // TagQuery > TagRequirements. + if (Relationship.ActorTagQuery.IsEmpty() || !Relationship.ActorTagQuery.Matches(ActorOwnedTags)) + { + continue; + } + + bool bNotAllowed = false; + bool bBlocked = false; + + if (!Relationship.AllowedInputs.IsEmpty()) + { + int32 Index = Relationship.IndexOfAllowedInput(InputTag, TriggerEvent); + if (Index == INDEX_NONE) + { + bNotAllowed = true; + } + } + + if (!Relationship.BlockedInputs.IsEmpty()) + { + int32 Index = Relationship.IndexOfBlockedInput(InputTag, TriggerEvent); + if (Index != INDEX_NONE) + { + bBlocked = true; + } + } + + if (bNotAllowed || bBlocked) + { + return false; + } + } + + return true; +} + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGIPS_InputChecker_TagRelationship::PreSave(FObjectPreSaveContext SaveContext) +{ + for (FGIPS_InputTagRelationship& InputTagRelationship : InputTagRelationships) + { + InputTagRelationship.EditorFriendlyName = InputTagRelationship.ActorTagQuery.GetDescription(); + // TArray TagArray; + // InputTagRelationship.InputTagsAllowed.GetGameplayTagArray(TagArray); + // InputTagRelationship.AllowedInputs.Empty(); + // for (FGameplayTag Tag : TagArray) + // { + // FGIPS_AllowedInput AllowedInput; + // AllowedInput.TriggerEvents.Empty(); + // AllowedInput.InputTag = Tag; + // InputTagRelationship.AllowedInputs.Add(AllowedInput); + // } + } + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputConfig.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputConfig.cpp new file mode 100644 index 0000000..d175633 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputConfig.cpp @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_InputConfig.h" + + + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" +#include "Misc/DataValidation.h" + +void UGIPS_InputConfig::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); +} + +EDataValidationResult UGIPS_InputConfig::IsDataValid(FDataValidationContext& Context) const +{ + if (InputActionMappings.IsEmpty()) + { + Context.AddError(FText::FromString(TEXT("InputActionMappings can't be empty!"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputControlSetup.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputControlSetup.cpp new file mode 100644 index 0000000..8fda252 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputControlSetup.cpp @@ -0,0 +1,133 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIPS_InputControlSetup.h" + +#include "GIPS_LogChannels.h" +#include "GIPS_InputChecker.h" +#include "GIPS_InputSystemComponent.h" +#include "GIPS_InputFunctionLibrary.h" +#include "Misc/DataValidation.h" + +void UGIPS_InputControlSetup::HandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) const +{ + TArray> Processors = FilterInputProcessors(InputTag, TriggerEvent); + for (int32 i = 0; i < Processors.Num(); i++) + { + UGIPS_InputProcessor* Processor = Processors[i]; + if (Processor->CanHandleInput(IC, ActionData, InputTag, TriggerEvent)) + { + Processor->HandleInput(IC, ActionData, InputTag, TriggerEvent); + if (InputProcessorExecutionType == EGIPS_InputProcessorExecutionType::FirstOnly) + { + return; + } + } + else + { + if (ShouldDebug(InputTag, TriggerEvent)) + { + GIPS_OWNED_CLOG(IC, Verbose, "Input:%s can't be handled by processor:%s at index(%d), TriggerEvent:%s", *InputTag.ToString(), *Processor->GetClass()->GetName(), + i, *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent)); + } + } + } +} + +bool UGIPS_InputControlSetup::CheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (InternalCheckInput(IC, ActionData, InputTag, TriggerEvent)) + { + if (ShouldDebug(InputTag, TriggerEvent)) + { + GIPS_OWNED_CLOG(IC, Verbose, "Input:%s passed,TriggerEvent:%s", *InputTag.ToString(), *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent)); + } + IC->RegisterPassedInputEntry({InputTag, ActionData, TriggerEvent}); + return true; + } + + if (bEnableInputBuffer) + { + if (IC->TrySaveInput(ActionData, InputTag, TriggerEvent)) + { + if (ShouldDebug(InputTag, TriggerEvent)) + { + GIPS_OWNED_CLOG(IC, Verbose, "Input:%s buffered,TriggerEvent:%s", *InputTag.ToString(), *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent)); + } + IC->RegisterBufferedInputEntry({InputTag, ActionData, TriggerEvent}); + return false; + } + } + + if (ShouldDebug(InputTag, TriggerEvent)) + { + GIPS_OWNED_CLOG(IC, Verbose, "Input:%s blocked,TriggerEvent:%s", *InputTag.ToString(), *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent)); + } + + IC->RegisterBlockedInputEntry({InputTag, ActionData, TriggerEvent}); + + return false; +} + +bool UGIPS_InputControlSetup::ShouldDebug(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const +{ + return bEnableInputDebug && (DebugInputTags.IsEmpty() || DebugInputTags.HasTagExact(InputTag)) && (DebugTriggerEvents.IsEmpty() || DebugTriggerEvents.Contains(TriggerEvent)); +} + +bool UGIPS_InputControlSetup::InternalCheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (InputCheckers.IsEmpty()) + { + return true; + } + + int32 AlwaysAllowedInputIndex = INDEX_NONE; + + for (int32 i = 0; i < AlwaysAllowedInputs.Num(); i++) + { + if (AlwaysAllowedInputs[i].InputTag == InputTag && (AlwaysAllowedInputs[i].TriggerEvents.IsEmpty() || AlwaysAllowedInputs[i].TriggerEvents.Contains(TriggerEvent))) + { + AlwaysAllowedInputIndex = i; + } + } + if (AlwaysAllowedInputIndex != INDEX_NONE) + { + return true; + } + + for (const UGIPS_InputChecker* Checker : InputCheckers) + { + if (Checker == nullptr) + continue; + if (!Checker->CheckInput(IC, ActionData, InputTag, TriggerEvent)) + return false; + } + return true; +} + +TArray> UGIPS_InputControlSetup::FilterInputProcessors(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const +{ + return InputProcessors.FilterByPredicate([&](TObjectPtr Processor) + { + return Processor && !Processor->InputTags.IsEmpty() && Processor->InputTags.HasTagExact(InputTag) && Processor->TriggerEvents.Contains(TriggerEvent); + }); +} + +#if WITH_EDITOR +EDataValidationResult UGIPS_InputControlSetup::IsDataValid(FDataValidationContext& Context) const +{ + for (int32 i = 0; i < InputProcessors.Num(); i++) + { + if (InputProcessors[i] == nullptr) + { + Context.AddError(FText::FromString(FString::Format(TEXT("Invalid processor at index:{0}"), {i}))); + return EDataValidationResult::Invalid; + } + if (InputProcessors[i]->InputTags.IsEmpty()) + { + Context.AddWarning(FText::FromString(FString::Format(TEXT("Invalid processor at index:{0} has empty InputTags!!!"), {i}))); + return EDataValidationResult::Invalid; + } + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputFunctionLibrary.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputFunctionLibrary.cpp new file mode 100644 index 0000000..8255aa8 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputFunctionLibrary.cpp @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_InputFunctionLibrary.h" + +#include "GameplayTagsManager.h" + +FInputActionValue UGIPS_InputFunctionLibrary::GetInputActionValue(const FInputActionInstance& ActionDataData) +{ + return ActionDataData.GetValue(); +} + +FName UGIPS_InputFunctionLibrary::GetLastTagName(FGameplayTag Tag) +{ + if (!Tag.IsValid()) + { + return FName(TEXT("Invalid Tag")); + } + + TArray TagNames; + + UGameplayTagsManager::Get().SplitGameplayTagFName(Tag, TagNames); + + if (TagNames.IsEmpty()) + { + return FName(TEXT("Invalid Tag")); + } + + return TagNames.Last(); +} + +FString UGIPS_InputFunctionLibrary::GetSimpleStringOfTags(FGameplayTagContainer Tags) +{ + return Tags.ToStringSimple(); +} + +TArray UGIPS_InputFunctionLibrary::GetLastTagNameArray(FGameplayTagContainer Tags) +{ + TArray TagArray; + Tags.GetGameplayTagArray(TagArray); + + TArray NameArray; + for (const FGameplayTag& Tag : TagArray) + { + NameArray.Add(GetLastTagName(Tag)); + } + + return NameArray; +} + +FString UGIPS_InputFunctionLibrary::GetLastTagNameString(FGameplayTagContainer Tags) +{ + TArray TagArray; + Tags.GetGameplayTagArray(TagArray); + + FString Output; + for (const FGameplayTag& Tag : TagArray) + { + Output.Append(FString::Format(TEXT(" ({0}) "), {GetLastTagName(Tag).ToString()})); + } + + return Output; +} + +FString UGIPS_InputFunctionLibrary::GetTagQueryDescription(const FGameplayTagQuery& TagQuery) +{ + if (TagQuery.IsEmpty()) + { + return TEXT("Empty Query"); + } + + return TagQuery.GetDescription(); +} + +FString UGIPS_InputFunctionLibrary::GetTriggerEventString(ETriggerEvent TriggerEvent) +{ + switch (TriggerEvent) + { + case ETriggerEvent::Started: + return TEXT("Start"); + case ETriggerEvent::Triggered: + return TEXT("Triggered"); + case ETriggerEvent::Canceled: + return TEXT("Canceled"); + case ETriggerEvent::Ongoing: + return TEXT("Ongoing"); + case ETriggerEvent::Completed: + return TEXT("Completed"); + case ETriggerEvent::None: + default: + return TEXT("None"); + } +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputProcessor.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputProcessor.cpp new file mode 100644 index 0000000..e722a95 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputProcessor.cpp @@ -0,0 +1,90 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_InputProcessor.h" + +#include "EnhancedInputComponent.h" +#include "GIPS_InputChecker.h" +#include "GIPS_InputFunctionLibrary.h" + + +UGIPS_InputProcessor::UGIPS_InputProcessor() +{ +} + +bool UGIPS_InputProcessor::CanHandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const +{ + return CheckCanHandleInput(IC, ActionData, InputTag, TriggerEvent); +} + +void UGIPS_InputProcessor::HandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const +{ + switch (TriggerEvent) + { + case ETriggerEvent::Triggered: + return HandleInputTriggered(IC, ActionData, InputTag); + case ETriggerEvent::Started: + return HandleInputStarted(IC, ActionData, InputTag); + case ETriggerEvent::Ongoing: + return HandleInputOngoing(IC, ActionData, InputTag); + case ETriggerEvent::Canceled: + return HandleInputCanceled(IC, ActionData, InputTag); + case ETriggerEvent::Completed: + return HandleInputCompleted(IC, ActionData, InputTag); + default: + return; + } +} + +FInputActionValue UGIPS_InputProcessor::GetInputActionValue(const FInputActionInstance& ActionData) const +{ + const FInputActionValue Value = ActionData.GetValue(); + return Value; +} + +bool UGIPS_InputProcessor::CheckCanHandleInput_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const +{ + return true; +} + +FString UGIPS_InputProcessor::GetEditorFriendlyName_Implementation() const +{ + return TEXT(""); +} + +void UGIPS_InputProcessor::HandleInputCanceled_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ +} + +void UGIPS_InputProcessor::HandleInputCompleted_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ +} + +void UGIPS_InputProcessor::HandleInputOngoing_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ +} + +void UGIPS_InputProcessor::HandleInputStarted_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ +} + +void UGIPS_InputProcessor::HandleInputTriggered_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ +} + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +FString UGIPS_InputProcessor::NativeGetEditorFriendlyName() const +{ + FString BPOverride = GetEditorFriendlyName(); + + return FString::Format(TEXT("Input:{0} {1}"), {UGIPS_InputFunctionLibrary::GetLastTagNameString(InputTags),BPOverride}); +} + +void UGIPS_InputProcessor::PreSave(FObjectPreSaveContext SaveContext) +{ + EditorFriendlyName = NativeGetEditorFriendlyName(); + UObject::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputSystemComponent.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputSystemComponent.cpp new file mode 100644 index 0000000..7dbfc5a --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputSystemComponent.cpp @@ -0,0 +1,546 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIPS_InputSystemComponent.h" +#include "EnhancedInputSubsystems.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "Engine/LocalPlayer.h" +#include "GIPS_LogChannels.h" +#include "GIPS_InputConfig.h" +#include "GIPS_InputControlSetup.h" +#include "GIPS_InputFunctionLibrary.h" +#include "Engine/World.h" +#include "Misc/DataValidation.h" + +UGIPS_InputSystemComponent::UGIPS_InputSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void UGIPS_InputSystemComponent::OnRegister() +{ + Super::OnRegister(); + + const UWorld* World = GetWorld(); + + if (World->IsGameWorld()) + { + APlayerController* PCOwner = GetOwner(); + + APawn* PawnOwner = GetOwner(); + + OwnerType = PCOwner ? EGIPS_OwnerType::PC : EGIPS_OwnerType::Pawn; + + if (OwnerType == EGIPS_OwnerType::Pawn) + { + if (ensure(PawnOwner)) + { + PawnOwner->ReceiveRestartedDelegate.AddDynamic(this, &UGIPS_InputSystemComponent::OnPawnRestarted); + PawnOwner->ReceiveControllerChangedDelegate.AddDynamic(this, &UGIPS_InputSystemComponent::OnControllerChanged); + + // If our pawn has an input component we were added after restart + if (PawnOwner->InputComponent) + { + OnPawnRestarted(PawnOwner); + } + } + } + + if (OwnerType == EGIPS_OwnerType::PC) + { + if (ensure(PCOwner)) + { + // TODO 支持放到PC上。 + } + } + } +} + +void UGIPS_InputSystemComponent::OnUnregister() +{ + const UWorld* World = GetWorld(); + if (World && World->IsGameWorld()) + { + CleanupInputComponent(); + + if (OwnerType == EGIPS_OwnerType::Pawn) + { + APawn* PawnOwner = GetOwner(); + PawnOwner->ReceiveRestartedDelegate.RemoveAll(this); + PawnOwner->ReceiveControllerChangedDelegate.RemoveAll(this); + } + + if (OwnerType == EGIPS_OwnerType::PC) + { + APlayerController* PCOwner = GetOwner(); + } + } + + Super::OnUnregister(); +} + +APawn* UGIPS_InputSystemComponent::GetControlledPawn() const +{ + if (OwnerType == EGIPS_OwnerType::Pawn) + { + return GetOwner(); + } + if (OwnerType == EGIPS_OwnerType::PC) + { + APlayerController* PC = GetOwner(); + return PC ? PC->GetPawn() : nullptr; + } + return nullptr; +} + +UGIPS_InputSystemComponent* UGIPS_InputSystemComponent::GetInputSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +bool UGIPS_InputSystemComponent::FindInputSystemComponent(const AActor* Actor, UGIPS_InputSystemComponent*& Component) +{ + Component = GetInputSystemComponent(Actor); + return Component != nullptr; +} + +void UGIPS_InputSystemComponent::OnSetupPlayerInputComponent_Implementation(UEnhancedInputComponent* NewInputComponent) +{ + BindInputActions(); +} + +void UGIPS_InputSystemComponent::OnCleanupPlayerInputComponent_Implementation(UEnhancedInputComponent* PrevInputComponent) +{ +} + +void UGIPS_InputSystemComponent::OnPawnRestarted(APawn* Pawn) +{ + if (ensure(Pawn && Pawn == GetOwner()) && Pawn->InputComponent) + { + GIPS_CLOG(Verbose, "cleanup and setup input for Pawn: %s", Pawn ? *Pawn->GetName() : TEXT("NONE")) + CleanupInputComponent(); + + if (Pawn->InputComponent) + { + SetupInputComponent(Pawn->InputComponent); + } + } +} + +void UGIPS_InputSystemComponent::OnControllerChanged(APawn* Pawn, AController* OldController, AController* NewController) +{ + // Only handle releasing, restart is a better time to handle binding + if (ensure(Pawn && Pawn == GetOwner()) && OldController) + { + GIPS_CLOG(Verbose, "cleanup input component due to controller change. %s", Pawn ? *Pawn->GetName() : TEXT("NONE")) + CleanupInputComponent(OldController); + } +} + +void UGIPS_InputSystemComponent::CleanInputActionValueBindings() +{ + for (auto& Binding : InputActionValueBindings) + { + InputComponent->RemoveActionValueBinding(Binding.Value); + GIPS_CLOG(Verbose, "Clean input action value binding for InputTag:{%s}", *Binding.Key.ToString()); + } + InputActionValueBindings.Empty(); +} + +void UGIPS_InputSystemComponent::SetupInputActionValueBindings() +{ + check(InputConfig); + for (auto& Mapping : InputConfig->InputActionMappings) + { + if (Mapping.Value.bValueBinding) + { + FEnhancedInputActionValueBinding& Binding = InputComponent->BindActionValue(Mapping.Value.InputAction); + int32 BindingIndex = InputComponent->GetActionValueBindings().Find(Binding); + InputActionValueBindings.Emplace(Mapping.Key, BindingIndex); + GIPS_CLOG(Verbose, "Setup input action value binding for InputTag:{%s} ad index:{%d}", *Mapping.Key.ToString(), BindingIndex); + } + } +} + +void UGIPS_InputSystemComponent::SetupInputComponent(UInputComponent* NewInputComponent) +{ + InputComponent = Cast(NewInputComponent); + + if (ensureMsgf(InputComponent, TEXT("Project must use EnhancedInputComponent to support PlayerControlsComponent"))) + { + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + + if (Subsystem && InputMappingContext) + { + Subsystem->AddMappingContext(InputMappingContext, InputPriority); + } + + CleanInputActionValueBindings(); + + SetupInputActionValueBindings(); + + GIPS_CLOG(Verbose, "Setup for Pawn/PC: %s", GetOwner() ? *GetOwner()->GetName() : TEXT("NONE")) + OnSetupPlayerInputComponent(InputComponent); + SetupInputComponentEvent.Broadcast(InputComponent); + } +} + +void UGIPS_InputSystemComponent::CleanupInputComponent(AController* OldController) +{ + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(OldController); + if (Subsystem && InputComponent) + { + OnCleanupPlayerInputComponent(InputComponent); + CleanupInputComponentEvent.Broadcast(InputComponent); + + if (InputMappingContext) + { + Subsystem->RemoveMappingContext(InputMappingContext); + } + CleanInputActionValueBindings(); + } + InputComponent = nullptr; +} + +UEnhancedInputLocalPlayerSubsystem* UGIPS_InputSystemComponent::GetEnhancedInputSubsystem(AController* OldController) const +{ + if (OwnerType == EGIPS_OwnerType::Pawn && !GetOwner()) + { + return nullptr; + } + const APawn* PawnOwner = GetOwner(); + + const APlayerController* PC = PawnOwner ? PawnOwner->GetController() : GetOwner(); + if (!PC) + { + PC = Cast(OldController); + if (!PC) + { + return nullptr; + } + } + + const ULocalPlayer* LP = PC->GetLocalPlayer(); + if (!LP) + { + return nullptr; + } + + return LP->GetSubsystem(); +} + +void UGIPS_InputSystemComponent::BindInputActions() +{ + check(InputConfig); + + for (auto& Pair : InputConfig->InputActionMappings) + { + // Generic binding. + InputComponent->BindAction(Pair.Value.InputAction, ETriggerEvent::Triggered, this, &ThisClass::InputActionCallback, Pair.Key, ETriggerEvent::Triggered); + InputComponent->BindAction(Pair.Value.InputAction, ETriggerEvent::Started, this, &ThisClass::InputActionCallback, Pair.Key, ETriggerEvent::Started); + InputComponent->BindAction(Pair.Value.InputAction, ETriggerEvent::Ongoing, this, &ThisClass::InputActionCallback, Pair.Key, ETriggerEvent::Ongoing); + InputComponent->BindAction(Pair.Value.InputAction, ETriggerEvent::Completed, this, &ThisClass::InputActionCallback, Pair.Key, ETriggerEvent::Completed); + InputComponent->BindAction(Pair.Value.InputAction, ETriggerEvent::Canceled, this, &ThisClass::InputActionCallback, Pair.Key, ETriggerEvent::Canceled); + } +} + +UGIPS_InputControlSetup* UGIPS_InputSystemComponent::GetCurrentInputSetup() const +{ + if (InputControlSetups.IsValidIndex(InputControlSetups.Num() - 1)) + { + return InputControlSetups[InputControlSetups.Num() - 1]; + } + return nullptr; +} + +UGIPS_InputConfig* UGIPS_InputSystemComponent::GetInputConfig() const +{ + return InputConfig; +} + +void UGIPS_InputSystemComponent::PushInputSetup(UGIPS_InputControlSetup* NewSetup) +{ + if (!InputControlSetups.Contains(NewSetup)) + { + InputControlSetups.Push(NewSetup); + } +} + +void UGIPS_InputSystemComponent::PopInputSetup() +{ + if (InputControlSetups.Num() > 1) + { + InputControlSetups.Pop(); + } +} + +bool UGIPS_InputSystemComponent::CheckInputAllowed(FGameplayTag InputTag, ETriggerEvent TriggerEvent) +{ + FInputActionInstance ActionData; + return CheckInputAllowed(ActionData, InputTag, TriggerEvent); +} + +bool UGIPS_InputSystemComponent::CheckInputAllowed(const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) +{ + if (UGIPS_InputControlSetup* Setup = GetCurrentInputSetup()) + { + return Setup->CheckInput(this, ActionData, InputTag, TriggerEvent); + } + return true; +} + +void UGIPS_InputSystemComponent::InputActionCallback(const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) +{ + if (InputTag.IsValid()) + { + GIPS_CLOG(VeryVerbose, "Input(%s) triggered with event(%s) and value(%s)", *UGIPS_InputFunctionLibrary::GetLastTagName(InputTag).ToString(), *InputTag.ToString(), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent), *ActionData.GetValue().ToString()); + + if (!bProcessingInputExternally && CheckInputAllowed(ActionData, InputTag, TriggerEvent)) + { + ProcessInput(ActionData, InputTag, TriggerEvent); + } + LastInputActionValues.Emplace(InputTag, ActionData.GetValue()); + OnReceivedInput.Broadcast(ActionData, InputTag, TriggerEvent); + } +} + +void UGIPS_InputSystemComponent::ProcessInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (UGIPS_InputControlSetup* Setup = GetCurrentInputSetup()) + { + Setup->HandleInput(this, ActionData, InputTag, TriggerEvent); + } +} + +UInputAction* UGIPS_InputSystemComponent::GetInputActionOfInputTag(FGameplayTag InputTag) const +{ + if (InputTag.IsValid() && InputConfig->InputActionMappings.Contains(InputTag)) + return InputConfig->InputActionMappings[InputTag].InputAction; + + return nullptr; +} + +FInputActionValue UGIPS_InputSystemComponent::GetInputActionValueOfInputTag(FGameplayTag InputTag) const +{ + if (InputComponent) + { + if (UInputAction* IA = GetInputActionOfInputTag(InputTag)) + { + return InputComponent->GetBoundActionValue(IA); + } + } + return FInputActionValue(); +} + +FInputActionValue UGIPS_InputSystemComponent::GetLastInputActionValueOfInputTag(FGameplayTag InputTag) const +{ + if (InputTag.IsValid() && LastInputActionValues.Contains(InputTag)) + { + return LastInputActionValues[InputTag]; + } + + return FInputActionValue(); +} + +void UGIPS_InputSystemComponent::RegisterPassedInputEntry(const FGIPS_BufferedInput& InputEntry) +{ + if (PassedInputEntries.Num() >= MaxInputEntriesNum) + { + PassedInputEntries.RemoveAtSwap(0); + } + PassedInputEntries.Add(InputEntry); +} + +void UGIPS_InputSystemComponent::RegisterBlockedInputEntry(const FGIPS_BufferedInput& InputEntry) +{ + if (BlockedInputEntries.Num() >= MaxInputEntriesNum) + { + BlockedInputEntries.RemoveAtSwap(0); + } + BlockedInputEntries.Add(InputEntry); +} + +void UGIPS_InputSystemComponent::RegisterBufferedInputEntry(const FGIPS_BufferedInput& InputEntry) +{ + if (BufferedInputEntries.Num() >= MaxInputEntriesNum) + { + BufferedInputEntries.RemoveAtSwap(0); + } + BufferedInputEntries.Add(InputEntry); +} + +#pragma region InputBuffer + +bool UGIPS_InputSystemComponent::TrySaveInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (ActiveBufferWindows.IsEmpty()) + { + // No any buffer window. + return false; + } + + TArray ActiveBufferWindowNames; + ActiveBufferWindows.GetKeys(ActiveBufferWindowNames); + + // To see if any active buffer window can accept this input. + int32 Counter{0}; + for (FGameplayTag& ActiveBufferWindowName : ActiveBufferWindowNames) + { + if (TrySaveAsBufferedInput(ActiveBufferWindowName, ActionData, InputTag, TriggerEvent)) + { + Counter++; + } + } + + return Counter > 0; +} + +void UGIPS_InputSystemComponent::FireBufferedInput() +{ + ProcessInput(CurrentBufferedInput.ActionData, CurrentBufferedInput.InputTag, CurrentBufferedInput.TriggerEvent); + OnFireBufferedInput.Broadcast(CurrentBufferedInput.ActionData, CurrentBufferedInput.InputTag, CurrentBufferedInput.TriggerEvent); + ResetBufferedInput(); + CloseActiveInputBufferWindows(); +} + +void UGIPS_InputSystemComponent::OpenInputBufferWindow(FGameplayTag BufferWindowName) +{ + if (!BufferWindowName.IsValid()) + { + GIPS_CLOG(Warning, "invalid buffer name."); + return; + } + + if (ActiveBufferWindows.Contains(BufferWindowName)) + { + GIPS_CLOG(Warning, "Can't Open buffer window(%s) as it already active!", *BufferWindowName.ToString()); + return; + } + + if (!ActiveBufferWindows.Contains(BufferWindowName)) + { + if (const FGIPS_InputBufferWindow* Window = InputConfig->InputBufferDefinitions.FindByKey(BufferWindowName)) + { + ActiveBufferWindows.FindOrAdd(BufferWindowName); + GIPS_CLOG(Verbose, "Open buffer window:%s", *BufferWindowName.ToString()); + InputBufferWindowStateChangedEvent.Broadcast(BufferWindowName, true); + } + } +} + +void UGIPS_InputSystemComponent::CloseInputBufferWindow(FGameplayTag BufferWindowName) +{ + if (ActiveBufferWindows.Contains(BufferWindowName)) + { + CurrentBufferedInput = ActiveBufferWindows[BufferWindowName]; + if (CurrentBufferedInput.InputTag.IsValid()) + { + GIPS_CLOG(Verbose, "Fire buffered input(:%s,TriggerEvent:%s) from Window(%s)", *CurrentBufferedInput.InputTag.ToString(), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(CurrentBufferedInput.TriggerEvent), *BufferWindowName.ToString()); + FireBufferedInput(); + } + ActiveBufferWindows.Remove(BufferWindowName); + GIPS_CLOG(Verbose, "Close buffer window:%s", *BufferWindowName.ToString()); + InputBufferWindowStateChangedEvent.Broadcast(BufferWindowName, false); + } +} + +void UGIPS_InputSystemComponent::CloseActiveInputBufferWindows() +{ + ActiveBufferWindows.Empty(); +} + +FGIPS_BufferedInput UGIPS_InputSystemComponent::GetLastBufferedInput() const +{ + return LastBufferedInput; +} + +TMap UGIPS_InputSystemComponent::GetActiveBufferWindows() const +{ + return ActiveBufferWindows; +} + +void UGIPS_InputSystemComponent::ResetBufferedInput() +{ + LastBufferedInput = CurrentBufferedInput; + CurrentBufferedInput = FGIPS_BufferedInput(); +} + +bool UGIPS_InputSystemComponent::TrySaveAsBufferedInput(const FGameplayTag BufferWindowName, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) +{ + if (!ActiveBufferWindows.Contains(BufferWindowName)) + return false; + + FGIPS_BufferedInput& BufferedInput = ActiveBufferWindows[BufferWindowName]; + const FGIPS_InputBufferWindow* Definition = InputConfig->InputBufferDefinitions.FindByKey(BufferWindowName); + + if (Definition == nullptr) + return false; + + const int32 AllowedInputIndex = Definition->IndexOfAllowedInput(InputTag, TriggerEvent); + + if (AllowedInputIndex == INDEX_NONE) + return false; + + // Instance buffering. + if (Definition->BufferType == EGIPS_InputBufferType::Instant) + { + BufferedInput.InputTag = InputTag; + BufferedInput.ActionData = ActionData; + BufferedInput.TriggerEvent = TriggerEvent; + + CurrentBufferedInput = BufferedInput; + GIPS_CLOG(Verbose, "Instantly fire buffered input(%s,TriggerEvent:%s) from Window(%s)", *InputTag.ToString(), *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent), + *BufferWindowName.ToString()); + FireBufferedInput(); + ActiveBufferWindows.Remove(BufferWindowName); + + return true; + } + + if (BufferedInput.InputTag.IsValid() && Definition->BufferType == EGIPS_InputBufferType::HighestPriority) + { + const int32 ExistingInputIndex = Definition->IndexOfAllowedInput(BufferedInput.InputTag, BufferedInput.TriggerEvent); + if (ExistingInputIndex != INDEX_NONE && AllowedInputIndex < ExistingInputIndex) + { + GIPS_CLOG(Verbose, "Record new buffered input(%s,TriggerEvent:%s) in Window(%s),Before was input(%s,TriggerEvent:%s)", *InputTag.ToString(), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent), *BufferWindowName.ToString(), + *BufferedInput.InputTag.ToString(), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(BufferedInput.TriggerEvent)); + BufferedInput.InputTag = InputTag; + BufferedInput.ActionData = ActionData; + BufferedInput.TriggerEvent = TriggerEvent; + return true; + } + } + + GIPS_CLOG(Verbose, "Record buffered input(%s,TriggerEvent:%s) in Window(%s)", *InputTag.ToString(), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent), *BufferWindowName.ToString()); + BufferedInput.InputTag = InputTag; + BufferedInput.ActionData = ActionData; + BufferedInput.TriggerEvent = TriggerEvent; + return true; +} + +#pragma endregion + +#pragma region DataValidation +#if WITH_EDITOR +EDataValidationResult UGIPS_InputSystemComponent::IsDataValid(FDataValidationContext& Context) const +{ + if (!InputConfig) + { + Context.AddError(FText::FromString(TEXT("InputConfig is required."))); + return EDataValidationResult::Invalid; + } + + if (InputControlSetups.IsEmpty()) + { + Context.AddError(FText::FromString(TEXT("At least one InputControlSetup is required."))); + return EDataValidationResult::Invalid; + } + + return Super::IsDataValid(Context); +} +#endif +#pragma endregion diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputTypes.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputTypes.cpp new file mode 100644 index 0000000..6077ff2 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_InputTypes.cpp @@ -0,0 +1,25 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_InputTypes.h" + +#include "GIPS_InputFunctionLibrary.h" + +bool FGIPS_InputBufferWindow::operator==(const FGameplayTag& OtherTag) const +{ + return Tag == OtherTag; +} + +bool FGIPS_InputBufferWindow::operator!=(const FGameplayTag& OtherTag) const +{ + return Tag != OtherTag; +} + +FString FGIPS_BufferedInput::ToString() const +{ + return FString::Format(TEXT("Tag:{0},Event:{1},Source:{2}"), { + InputTag.IsValid() ? *UGIPS_InputFunctionLibrary::GetLastTagName(InputTag).ToString() : TEXT("None"), + *UGIPS_InputFunctionLibrary::GetTriggerEventString(TriggerEvent), + ActionData.GetSourceAction() ? ActionData.GetSourceAction()->GetName() : TEXT("None"), + }); +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_LogChannels.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_LogChannels.cpp new file mode 100644 index 0000000..505c6bc --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GIPS_LogChannels.cpp @@ -0,0 +1,36 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIPS_LogChannels.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" + +DEFINE_LOG_CATEGORY(LogGIPS); + +FString GetGIPSLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + Role = Component->GetOwnerRole(); + Name = Component->GetOwner()->GetName(); + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Private/GenericInputSystem.cpp b/Plugins/GCS/Source/GenericInputSystem/Private/GenericInputSystem.cpp new file mode 100644 index 0000000..6d43bc7 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Private/GenericInputSystem.cpp @@ -0,0 +1,39 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericInputSystem.h" + +#if WITH_GAMEPLAY_DEBUGGER +#include "GameplayDebugger.h" +#include "GIPS_GameplayDebugger.h" +#endif // WITH_GAMEPLAY_DEBUGGER + +#define LOCTEXT_NAMESPACE "FGenericInputSystemModule" + +void FGenericInputSystemModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +#if WITH_GAMEPLAY_DEBUGGER + IGameplayDebugger& GameplayDebuggerModule = IGameplayDebugger::Get(); + GameplayDebuggerModule.RegisterCategory("GenericInputSystem", IGameplayDebugger::FOnGetCategory::CreateStatic(&FGIPS_GameplayDebuggerCategory_Input::MakeInstance)); + GameplayDebuggerModule.NotifyCategoriesChanged(); +#endif // WITH_GAMEPLAY_DEBUGGER +} + +void FGenericInputSystemModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +#if WITH_GAMEPLAY_DEBUGGER + if (IGameplayDebugger::IsAvailable()) + { + IGameplayDebugger& GameplayDebuggerModule = IGameplayDebugger::Get(); + GameplayDebuggerModule.UnregisterCategory("GenericInputSystem"); + GameplayDebuggerModule.NotifyCategoriesChanged(); + } +#endif // WITH_GAMEPLAY_DEBUGGER +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericInputSystemModule, GenericInputSystem) \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/Actions/GIPS_AsyncAction_ListenInputEvent.h b/Plugins/GCS/Source/GenericInputSystem/Public/Actions/GIPS_AsyncAction_ListenInputEvent.h new file mode 100644 index 0000000..e7455ce --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/Actions/GIPS_AsyncAction_ListenInputEvent.h @@ -0,0 +1,99 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "Engine/CancellableAsyncAction.h" +#include "GIPS_InputTypes.h" +#include "GIPS_AsyncAction_ListenInputEvent.generated.h" + +class UGIPS_InputSystemComponent; + +/** + * Async action for listening to input events. + * 用于监听输入事件的异步动作。 + */ +UCLASS() +class GENERICINPUTSYSTEM_API UGIPS_AsyncAction_ListenInputEvent : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Creates an async action to listen for input events. + * 创建一个异步动作以监听输入事件。 + * @param WorldContextObject The world context object. 世界上下文对象。 + * @param InputSystemComponent The input system component. 输入系统组件。 + * @param InputTagsToListen Tags to listen for. 要监听的标签。 + * @param EventsToListen Trigger events to listen for. 要监听的触发事件。 + * @param bListenForBufferedInput Whether to listen for buffered inputs. 是否监听缓冲输入。 + * @param bExactMatch Whether to require exact tag matching. 是否要求精确标签匹配。 + * @return The async action instance. 异步动作实例。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input", meta=(WorldContext = "WorldContextObject", BlueprintInternalUseOnly="true")) + static UGIPS_AsyncAction_ListenInputEvent* ListenInputEvent(UObject* WorldContextObject, UGIPS_InputSystemComponent* InputSystemComponent,UPARAM(meta=(Categories="InputTag,GIPS.InputTag")) + FGameplayTagContainer InputTagsToListen, + TArray EventsToListen, bool bListenForBufferedInput = false, bool bExactMatch = true); + + /** + * Activates the async action. + * 激活异步动作。 + */ + virtual void Activate() override; + + /** + * Event triggered when an input is received. + * 接收到输入时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIPS_ReceivedInputSignature OnReceivedInput; + + /** + * Handles the input event. + * 处理输入事件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + */ + UFUNCTION() + void HandleInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + +private: + /** + * Cancels the async action. + * 取消异步动作。 + */ + virtual void Cancel() override; + + /** + * Tags to listen for. + * 要监听的标签。 + */ + FGameplayTagContainer InputTags; + + /** + * Trigger events to listen for. + * 要监听的触发事件。 + */ + TArray TriggerEvents; + + /** + * Whether to listen for buffered inputs. + * 是否监听缓冲输入。 + */ + bool bForBufferedInput{false}; + + /** + * Whether to require exact tag matching. + * 是否要求精确标签匹配。 + */ + bool bExact{true}; + + /** + * Weak reference to the input system component. + * 输入系统组件的弱引用。 + */ + TWeakObjectPtr Input; +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_GameplayDebugger.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_GameplayDebugger.h new file mode 100644 index 0000000..878ab82 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_GameplayDebugger.h @@ -0,0 +1,69 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once +#include "GameplayTagContainer.h" + +#if WITH_GAMEPLAY_DEBUGGER + +#include "CoreMinimal.h" +#include "GameplayDebuggerCategory.h" +#include "InputTriggers.h" + +class APlayerController; +class AActor; + +class FGIPS_GameplayDebuggerCategory_Input : public FGameplayDebuggerCategory +{ +public: + FGIPS_GameplayDebuggerCategory_Input(); + void CollectData(APlayerController* OwnerPC, AActor* DebugActor) override; + + void DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext) override; + + static TSharedRef MakeInstance(); + + void OnShowInputBuffersToggle(); + void OnShowPassedInputEntriesToggle(); + void OnShowBlockedInputEntriesToggle(); + void OnShowBufferedInputEntriesToggle(); + +protected: + void DrawInputBuffers(FGameplayDebuggerCanvasContext& CanvasContext, const APlayerController* OwnerPC) const; + void DrawInputEntries(FGameplayDebuggerCanvasContext& CanvasContext, const APlayerController* OwnerPC) const; + + struct FRepData + { + FString ActorName; + FString InputConfig; + FString InputControlSetup; + + FGameplayTag BufferedInputTag; + + struct FInputBuffersDebug + { + FName WindowName; + bool bIsActive; + FName InputTagName; + ETriggerEvent InputEvent; + }; + + TArray InputBuffers; + + void Serialize(FArchive&Ar); + }; + + FRepData DataPack; + +private: + + // Save off the last expected draw size so that we can draw a border around it next frame (and hope we're the same size) + float LastDrawDataEndSize = 0.0f; + + bool bShowInputBuffers = true; + bool bShowPassedInputEntries = true; + bool bShowBlockedInputEntries = true; + bool bShowBufferedInputEntries = true; +}; + + +#endif // WITH_GAMEPLAY_DEBUGG diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputChecker.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputChecker.h new file mode 100644 index 0000000..6845e7b --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputChecker.h @@ -0,0 +1,97 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIPS_InputTypes.h" +#include "InputAction.h" +#include "UObject/Object.h" +#include "GIPS_InputChecker.generated.h" + +class UGIPS_InputSystemComponent; + +/** + * Base class for input validation, inheritable via Blueprint or C++. + * 输入验证的基类,可通过蓝图或C++继承。 + */ +UCLASS(Abstract, Blueprintable, BlueprintType, EditInlineNew, DefaultToInstanced, CollapseCategories, Const) +class GENERICINPUTSYSTEM_API UGIPS_InputChecker : public UObject +{ + GENERATED_BODY() + +public: + /** + * Checks if an input is valid. + * 检查输入是否有效。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is valid, false otherwise. 如果输入有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + bool CheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) FGameplayTag InputTag, + ETriggerEvent TriggerEvent) const; + +protected: + /** + * Blueprint-implementable input validation logic. + * 可通过蓝图实现的输入验证逻辑。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is valid, false otherwise. 如果输入有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIPS|Input", meta=(DisplayName="DoCheckInput")) + bool DoCheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const; +}; + +/** + * Input checker for tag-based relationships. + * 基于标签关系的输入检查器。 + */ +UCLASS() +class GENERICINPUTSYSTEM_API UGIPS_InputChecker_TagRelationship : public UGIPS_InputChecker +{ + GENERATED_BODY() + +protected: + /** + * List of input tag relationships for validation. + * 用于验证的输入标签关系列表。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(TitleProperty="EditorFriendlyName", ShowOnlyInnerProperties)) + TArray InputTagRelationships; + + /** + * Gets the actor's tags for validation. + * 获取用于验证的演员标签。 + * @param IC The input system component. 输入系统组件。 + * @return The actor's gameplay tag container. 演员的游戏标签容器。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input", BlueprintNativeEvent) + FGameplayTagContainer GetActorTags(UGIPS_InputSystemComponent* IC) const; + virtual FGameplayTagContainer GetActorTags_Implementation(UGIPS_InputSystemComponent* IC) const; + + /** + * Implementation of input validation logic. + * 输入验证逻辑的实现。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is valid, false otherwise. 如果输入有效则返回true,否则返回false。 + */ + virtual bool DoCheckInput_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, + const ETriggerEvent& TriggerEvent) const override; + +#if WITH_EDITOR + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputConfig.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputConfig.h new file mode 100644 index 0000000..ec90359 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputConfig.h @@ -0,0 +1,54 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIPS_InputTypes.h" +#include "Engine/DataAsset.h" +#include "GIPS_InputConfig.generated.h" + +class UInputAction; + +/** + * Configuration data asset for the input system component. + * 输入系统组件的配置数据资产。 + */ +UCLASS(Const) +class GENERICINPUTSYSTEM_API UGIPS_InputConfig : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Mapping of input tags to input action settings. + * 输入标签到输入动作设置的映射。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(Categories="InputTag,GIPS.InputTag", ForceInlineRow)) + TMap InputActionMappings; + + /** + * List of defined input buffer windows. + * 定义的输入缓冲窗口列表。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(DisplayName="Input Buffer Windows", TitleProperty="Tag", NoElementDuplicate)) + TArray InputBufferDefinitions; + +protected: +#if WITH_EDITOR + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates the data asset in the editor. + * 在编辑器中验证数据资产。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputControlSetup.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputControlSetup.h new file mode 100644 index 0000000..01db5c0 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputControlSetup.h @@ -0,0 +1,143 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIPS_InputTypes.h" +#include "GIPS_InputControlSetup.generated.h" + +struct FInputActionInstance; +class UGIPS_InputChecker; +class UGIPS_InputProcessor; +class UGIPS_InputSystemComponent; + +/** + * Data asset for defining input checkers and processors. + * 定义输入检查器和处理器的数据资产。 + */ +UCLASS(BlueprintType, Const) +class GENERICINPUTSYSTEM_API UGIPS_InputControlSetup : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Handles input actions for the input system. + * 为输入系统处理输入动作。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + */ + void HandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent) const; + + /** + * Checks if an input is allowed. + * 检查输入是否被允许。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is allowed, false otherwise. 如果输入被允许则返回true,否则返回false。 + */ + bool CheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + +protected: + /** + * Determines if debugging is enabled for the input. + * 确定是否为输入启用调试。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if debugging is enabled, false otherwise. 如果启用调试则返回true,否则返回false。 + */ + bool ShouldDebug(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const; + + /** + * Internal logic for checking input validity. + * 检查输入有效性的内部逻辑。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is valid, false otherwise. 如果输入有效则返回true,否则返回false。 + */ + virtual bool InternalCheckInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + + /** + * Filters input processors based on the input tag and trigger event. + * 根据输入标签和触发事件过滤输入处理器。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return Array of filtered input processors. 过滤后的输入处理器数组。 + */ + TArray> FilterInputProcessors(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const; + + /** + * List of input events will bypass all the input checkers. + * 此列表中的输入时间会绕过所有的输入检查器。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(TitleProperty="InputTag")) + TArray AlwaysAllowedInputs; + + /** + * List of input checkers to validate input events. + * 验证输入事件的一组输入检查器。 + */ + UPROPERTY(EditAnywhere, Instanced, Category="GIPS|Input") + TArray> InputCheckers; + + /** + * If enabled, disallowed inputs are attempted to be stored in the input buffer. + * 如果启用,不允许的输入将尝试存储在输入缓冲区中。 + * @attention Input buffering may not be needed for some setups, like UI. 对于某些设置(如UI),可能不需要输入缓冲。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + bool bEnableInputBuffer{false}; + + /** + * Controls the execution order of input processors for a single input event. + * 控制单个输入事件的处理器执行顺序。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + EGIPS_InputProcessorExecutionType InputProcessorExecutionType{EGIPS_InputProcessorExecutionType::MatchAll}; + + /** + * List of input processors to handle allowed input events sequentially. + * 处理允许的输入事件的一组输入处理器,按顺序执行。 + */ + UPROPERTY(EditAnywhere, Instanced, Category="GIPS|Input", meta=(TitleProperty="EditorFriendlyName", ShowOnlyInnerProperties)) + TArray> InputProcessors; + + /** + * Enables debug logging for input events. + * 为输入事件启用调试日志。 + * @attention Requires LogGIPS to be set to VeryVerbose to take effect. 需要将LogGIPS设置为VeryVerbose才能生效。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Debug") + bool bEnableInputDebug{false}; + + /** + * Input tags to debug (logs all tags if empty). + * 要调试的输入标签(如果为空则记录所有标签)。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Debug", meta=(EditCondition="bEnableInputDebug", Categories="InputTag,GIPS.InputTag")) + FGameplayTagContainer DebugInputTags{}; + + /** + * Trigger events to debug (logs all events if empty). + * 要调试的触发事件(如果为空则记录所有事件)。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Debug", meta=(EditCondition="bEnableInputDebug")) + TArray DebugTriggerEvents{ETriggerEvent::Started, ETriggerEvent::Completed}; + +public: +#if WITH_EDITOR + /** + * Validates the data asset in the editor. + * 在编辑器中验证数据资产。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputFunctionLibrary.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputFunctionLibrary.h new file mode 100644 index 0000000..69f84c5 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputFunctionLibrary.h @@ -0,0 +1,81 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "InputActionValue.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GIPS_InputFunctionLibrary.generated.h" + +/** + * Utility functions for the Generic Input System. + * 通用输入系统的实用功能。 + */ +UCLASS() +class GENERICINPUTSYSTEM_API UGIPS_InputFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Retrieves the input action value from the action instance. + * 从输入动作实例中获取输入动作值。 + * @param ActionDataData The input action instance data. 输入动作实例数据。 + * @return The input action value. 输入动作值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIPS|Input", meta = (BlueprintAutocast)) + static FInputActionValue GetInputActionValue(const FInputActionInstance& ActionDataData); + + /** + * Gets the last tag name from a gameplay tag. + * 从游戏标签中获取最后一个标签名称。 + * @param Tag The gameplay tag. 游戏标签。 + * @return The last tag name. 最后一个标签名称。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIPS|Utilities", meta=(DisplayName="Get Last Tag Name(GIPS)")) + static FName GetLastTagName(FGameplayTag Tag); + + /** + * Converts a gameplay tag container to a simple string. + * 将游戏标签容器转换为简单字符串。 + * @param Tags The gameplay tag container. 游戏标签容器。 + * @return The string representation of the tags. 标签的字符串表示。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIPS|Utilities", meta=(DisplayName="Get Simple String Of Tags(GIPS)")) + static FString GetSimpleStringOfTags(FGameplayTagContainer Tags); + + /** + * Gets an array of last tag names from a tag container. + * 从标签容器中获取最后一个标签名称的数组。 + * @param Tags The gameplay tag container. 游戏标签容器。 + * @return Array of last tag names. 最后一个标签名称的数组。 + */ + static TArray GetLastTagNameArray(FGameplayTagContainer Tags); + + /** + * Gets a string of last tag names from a tag container. + * 从标签容器中获取最后一个标签名称的字符串。 + * @param Tags The gameplay tag container. 游戏标签容器。 + * @return String of last tag names. 最后一个标签名称的字符串。 + */ + static FString GetLastTagNameString(FGameplayTagContainer Tags); + + /** + * Gets a description of a gameplay tag query. + * 获取游戏标签查询的描述。 + * @param TagQuery The gameplay tag query. 游戏标签查询。 + * @return The description of the tag query. 标签查询的描述。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIPS|Utilities", meta=(DisplayName="Get TagQuery Description(GIPS)")) + static FString GetTagQueryDescription(const FGameplayTagQuery& TagQuery); + + /** + * Converts a trigger event to a string. + * 将触发事件转换为字符串。 + * @param TriggerEvent The trigger event. 触发事件。 + * @return The string representation of the trigger event. 触发事件的字符串表示。 + */ + static FString GetTriggerEventString(ETriggerEvent TriggerEvent); +}; \ No newline at end of file diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputProcessor.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputProcessor.h new file mode 100644 index 0000000..6af8a75 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputProcessor.h @@ -0,0 +1,193 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "InputActionValue.h" +#include "GIPS_InputProcessor.generated.h" + +class UGIPS_InputChecker; +struct FInputActionInstance; +enum class ETriggerEvent : uint8; +class UGIPS_InputSystemComponent; + +/** + * Base class for processing input actions. + * 处理输入动作的基类。 + */ +UCLASS(EditInlineNew, DefaultToInstanced, CollapseCategories, Blueprintable, Const, HideDropdown) +class GENERICINPUTSYSTEM_API UGIPS_InputProcessor : public UObject +{ + GENERATED_BODY() + +public: + UGIPS_InputProcessor(); + + /** + * Indicates if the processor supports networking. + * 指示处理器是否支持网络。 + * @return True if networking is supported, false otherwise. 如果支持网络则返回true,否则返回false。 + */ + virtual bool IsSupportedForNetworking() const override { return true; } + + /** + * Checks if the processor can handle the given input. + * 检查处理器是否可以处理给定的输入。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input can be handled, false otherwise. 如果可以处理输入则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input", meta=(AutoCreateRefTerm="ActionData")) + bool CanHandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag, + ETriggerEvent TriggerEvent) const; + + /** + * Handles the input action. + * 处理输入动作。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input", meta=(AutoCreateRefTerm="ActionData")) + void HandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag, + ETriggerEvent TriggerEvent) const; + + /** + * Tags that this processor responds to (if empty, responds to all inputs). + * 处理器响应的输入标签(如果为空,则响应所有输入)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="InputProcessor", meta=(Categories="InputTag,GIPS.InputTag", DisplayPriority=0)) + FGameplayTagContainer InputTags; + + /** + * Trigger events that this processor responds to. + * 处理器响应的触发事件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="InputProcessor", meta=(DisplayPriority=1)) + TArray TriggerEvents{ETriggerEvent::Started}; + +protected: + /** + * Gets the input action value (deprecated). + * 获取输入动作值(已弃用)。 + * @param ActionData The input action data. 输入动作数据。 + * @return The input action value. 输入动作值。 + */ + UFUNCTION(BlueprintPure, Category="InputProcessor", meta=(DeprecatedFunction, DeprecationMessage="Use GetInputActionValueOfInputTag")) + FInputActionValue GetInputActionValue(const FInputActionInstance& ActionData) const; + + /** + * Blueprint-implementable check for input handling. + * 可通过蓝图实现的输入处理检查。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input can be handled, false otherwise. 如果可以处理输入则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + bool CheckCanHandleInput(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const; + + /** + * Native implementation of input handling check. + * 输入处理检查的原生实现。 + */ + virtual bool CheckCanHandleInput_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent) const; + + /** + * Handles the triggered input event. + * 处理触发的输入事件。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + void HandleInputTriggered(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const; + + /** + * Handles the started input event. + * 处理开始的输入事件。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + void HandleInputStarted(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const; + + /** + * Handles the ongoing input event. + * 处理持续的输入事件。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + void HandleInputOngoing(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const; + + /** + * Handles the canceled input event. + * 处理取消的输入事件。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + void HandleInputCanceled(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const; + + /** + * Handles the completed input event. + * 处理完成的输入事件。 + * @param IC The input system component. 输入系统组件。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + void HandleInputCompleted(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const; + + /** + * Gets a friendly name for the editor. + * 获取编辑器友好的名称。 + * @return The editor-friendly name. 编辑器友好的名称。 + */ + UFUNCTION(BlueprintNativeEvent, Category="InputProcessor") + FString GetEditorFriendlyName() const; + +#if WITH_EDITORONLY_DATA + /** + * Friendly name for displaying in the editor. + * 在编辑器中显示的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; + + /** + * Description for developers in the editor. + * 编辑器中用于开发者的描述。 + */ + UPROPERTY(EditAnywhere, Category = "Editor") + FString DevDescription; +#endif + +#if WITH_EDITOR + /** + * Native implementation to get editor-friendly name. + * 获取编辑器友好名称的原生实现。 + * @return The editor-friendly name. 编辑器友好的名称。 + */ + FString NativeGetEditorFriendlyName() const; + + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputSystemComponent.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputSystemComponent.h new file mode 100644 index 0000000..46908f5 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputSystemComponent.h @@ -0,0 +1,550 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "EnhancedInputComponent.h" +#include "GameplayTagContainer.h" +#include "GIPS_InputProcessor.h" +#include "GIPS_InputTypes.h" +#include "GIPS_InputSystemComponent.generated.h" + +class UGIPS_InputControlSetup; +class UInputMappingContext; +class UGIPS_InputConfig; +class UEnhancedInputLocalPlayerSubsystem; + +/** + * Core component for handling input in the Generic Input System. + * 通用输入系统中处理输入的核心组件。 + * @attention Must be attached to a Pawn or PlayerController. 必须挂载到Pawn或PlayerController上。 + */ +UCLASS(ClassGroup=GIPS, Blueprintable, meta=(BlueprintSpawnableComponent), AutoExpandCategories=("GIPS")) +class GENERICINPUTSYSTEM_API UGIPS_InputSystemComponent : public UActorComponent +{ + GENERATED_BODY() + + friend UGIPS_InputControlSetup; + +public: + enum class EGIPS_OwnerType:uint8 + { + Pawn, + PC, + }; + +public: + UGIPS_InputSystemComponent(const FObjectInitializer& ObjectInitializer); + + //~ Begin UActorComponent interface + /** + * Called when the component is registered. + * 组件注册时调用。 + */ + virtual void OnRegister() override; + + /** + * Called when the component is unregistered. + * 组件取消注册时调用。 + */ + virtual void OnUnregister() override; + //~ End UActorComponent interface + + /** + * Gets the Pawn associated with this component. + * 获取与此组件关联的Pawn。 + * @return The associated Pawn, or the Pawn controlled by the PlayerController if the owner is a PlayerController. 如果拥有者是Pawn则返回Pawn,如果是PlayerController则返回其控制的Pawn。 + */ + UFUNCTION(BlueprintPure, Category="GIPS|Input") + APawn* GetControlledPawn() const; + + /** + * Gets the input system component from an actor. + * 从演员获取输入系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @return The input system component, or nullptr if not found. 输入系统组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIPS|Input", Meta = (DefaultToSelf="Actor")) + static UGIPS_InputSystemComponent* GetInputSystemComponent(const AActor* Actor); + + /** + * Finds the input system component on an actor. + * 在演员上查找输入系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @param Component The found component (output). 找到的组件(输出)。 + * @return True if the component was found, false otherwise. 如果找到组件则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GIPS|Input", Meta = (DefaultToSelf="Actor", ExpandBoolAsExecs="ReturnValue")) + static bool FindInputSystemComponent(const AActor* Actor, UGIPS_InputSystemComponent*& Component); + + /** + * Event triggered when setting up the input component. + * 设置输入组件时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="GIPS|Input") + FGIPS_InputComponentSignature SetupInputComponentEvent; + + /** + * Event triggered when cleaning up the input component. + * 清理输入组件时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="GIPS|Input") + FGIPS_InputComponentSignature CleanupInputComponentEvent; + + /** + * Event triggered when the input buffer window state changes. + * 输入缓冲窗口状态改变时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="GIPS|Input") + FGIPS_InputBufferWindowStateChangedSignature InputBufferWindowStateChangedEvent; + +protected: + /** + * Sets up the player input component (Blueprint-implementable). + * 设置玩家输入组件(可通过蓝图实现)。 + * @param NewInputComponent The new input component. 新输入组件。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIPS|Input") + void OnSetupPlayerInputComponent(UEnhancedInputComponent* NewInputComponent); + virtual void OnSetupPlayerInputComponent_Implementation(UEnhancedInputComponent* NewInputComponent); + + /** + * Cleans up the player input component (Blueprint-implementable). + * 清理玩家输入组件(可通过蓝图实现)。 + * @param PrevInputComponent The previous input component. 前一个输入组件。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIPS|Input") + void OnCleanupPlayerInputComponent(UEnhancedInputComponent* PrevInputComponent); + virtual void OnCleanupPlayerInputComponent_Implementation(UEnhancedInputComponent* PrevInputComponent); + + /** + * Called when the pawn restarts. + * 当Pawn重启时调用。 + * @param Pawn The pawn that restarted. 重启的Pawn。 + */ + UFUNCTION() + virtual void OnPawnRestarted(APawn* Pawn); + + /** + * Called when the controller changes. + * 当控制器更改时调用。 + * @param Pawn The associated pawn. 关联的Pawn。 + * @param OldController The previous controller. 前一个控制器。 + * @param NewController The new controller. 新控制器。 + */ + UFUNCTION() + virtual void OnControllerChanged(APawn* Pawn, AController* OldController, AController* NewController); + + /** + * Cleans up input action value bindings. + * 清理输入动作值绑定。 + */ + void CleanInputActionValueBindings(); + + /** + * Sets up input action value bindings. + * 设置输入动作值绑定。 + */ + void SetupInputActionValueBindings(); + + /** + * Sets up the input component. + * 设置输入组件。 + * @param NewInputComponent The new input component. 新输入组件。 + */ + virtual void SetupInputComponent(UInputComponent* NewInputComponent); + + /** + * Cleans up the input component. + * 清理输入组件。 + * @param OldController The previous controller (optional). 前一个控制器(可选)。 + */ + virtual void CleanupInputComponent(AController* OldController = nullptr); + + /** + * Gets the enhanced input subsystem. + * 获取增强输入子系统。 + * @param OldController The previous controller (optional). 前一个控制器(可选)。 + * @return The enhanced input subsystem. 增强输入子系统。 + */ + UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem(AController* OldController = nullptr) const; + + /** + * The bound input component. + * 绑定的输入组件。 + */ + UPROPERTY(transient) + TObjectPtr InputComponent; + + /** + * The type of owner (Pawn or PlayerController). + * 拥有者类型(Pawn或PlayerController)。 + */ + EGIPS_OwnerType OwnerType = EGIPS_OwnerType::PC; + +protected: + /** + * Binds input actions to the component. + * 将输入动作绑定到组件。 + */ + void BindInputActions(); + + /** + * Input mapping context to be added to the enhanced input subsystem. + * 将添加到增强输入子系统的输入映射上下文。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIPS|Input") + TObjectPtr InputMappingContext; + + /** + * Priority for binding the input mapping context. + * 绑定输入映射上下文的优先级。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIPS|Input") + int32 InputPriority = 0; + + /** + * Main configuration for the input system. + * 输入系统的主配置。 + */ + UPROPERTY(EditDefaultsOnly, Category="GIPS|Input") + TObjectPtr InputConfig; + + /** + * Maximum number of input entries to keep. + * 保留的最大输入记录数。 + */ + UPROPERTY(EditDefaultsOnly, Category="GIPS|Input", meta=(ClampMin=0, ClampMax=10)) + int32 MaxInputEntriesNum{5}; + + /** + * If true, inputs are processed externally via OnReceivedInput event. + * 如果为true,输入通过OnReceivedInput事件在外部处理。 + */ + UPROPERTY(EditDefaultsOnly, Category="GIPS|Input") + bool bProcessingInputExternally{false}; + + /** + * List of input control setups, with the last one being the current setup. + * 输入控制设置列表,最后一个为当前设置。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + TArray> InputControlSetups; + +public: +#pragma region InputControl + /** + * Gets the current input control setup. + * 获取当前输入控制设置。 + * @return The last input control setup in the list. 列表中的最后一个输入控制设置。 + */ + UFUNCTION(BlueprintPure, Category="GIPS|Input") + UGIPS_InputControlSetup* GetCurrentInputSetup() const; + + /** + * Gets the input configuration. + * 获取输入配置。 + * @return The input configuration. 输入配置。 + */ + UFUNCTION(BlueprintPure, Category="GIPS|Input") + UGIPS_InputConfig* GetInputConfig() const; + + /** + * Sets a new input control setup as the current one. + * 将新的输入控制设置设为当前设置。 + * @param NewSetup The new input control setup. 新输入控制设置。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + virtual void PushInputSetup(UGIPS_InputControlSetup* NewSetup); + + /** + * Removes the current input setup, making the last one in the list the new current setup. + * 删除当前输入设置,使列表中的最后一个成为新的当前设置。 + * @attention Does nothing if only one setup exists. 如果只有一个设置,则无操作。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + virtual void PopInputSetup(); + +#pragma endregion + + /** + * Checks if an input is allowed. + * 检查输入是否被允许。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is allowed, false otherwise. 如果输入被允许则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool CheckInputAllowed(UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag, ETriggerEvent TriggerEvent); + + /** + * Checks if an input is allowed with action data. + * 使用动作数据检查输入是否被允许。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input is allowed, false otherwise. 如果输入被允许则返回true,否则返回false。 + */ + // UFUNCTION(BlueprintCallable, Category="GIPS|Input", meta=(ExpandBoolAsExecs="ReturnValue", AutoCreateRefTerm="ActionData")) + virtual bool CheckInputAllowed(const FInputActionInstance& ActionData, UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag, ETriggerEvent TriggerEvent); + + /** + * Processes an input action. + * 处理输入动作。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + */ + // UFUNCTION(BlueprintCallable, Category="GIPS|Input") + virtual void ProcessInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + + /** + * Gets the input action associated with a tag. + * 获取与标签关联的输入动作。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @return The associated input action. 关联的输入动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + UInputAction* GetInputActionOfInputTag(UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag) const; + + /** + * Gets the current input action value for a tag. + * 获取标签的当前输入动作值。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @return The current input action value. 当前输入动作值。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + FInputActionValue GetInputActionValueOfInputTag(UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag) const; + + /** + * Gets the last input action value for a tag. + * 获取标签的最后输入动作值。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @return The last input action value. 最后输入动作值。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + FInputActionValue GetLastInputActionValueOfInputTag(UPARAM(meta = (Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag) const; + + /** + * Gets the list of passed input entries. + * 获取通过的输入记录列表。 + * @return Array of passed input entries. 通过的输入记录数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + TArray GetPassedInputEntries() const { return PassedInputEntries; }; + + /** + * Registers a passed input entry. + * 注册通过的输入记录。 + * @param InputEntry The input entry to register. 要注册的输入记录。 + */ + virtual void RegisterPassedInputEntry(const FGIPS_BufferedInput& InputEntry); + + /** + * Gets the list of blocked input entries. + * 获取阻止的输入记录列表。 + * @return Array of blocked input entries. 阻止的输入记录数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + TArray GetBlockedInputEntries() const { return BlockedInputEntries; }; + + /** + * Registers a blocked input entry. + * 注册阻止的输入记录。 + * @param InputEntry The input entry to register. 要注册的输入记录。 + */ + virtual void RegisterBlockedInputEntry(const FGIPS_BufferedInput& InputEntry); + + /** + * Gets the list of buffered input entries. + * 获取缓冲的输入记录列表。 + * @return Array of buffered input entries. 缓冲的输入记录数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|Input") + TArray GetBufferedInputEntries() const { return BufferedInputEntries; }; + + /** + * Registers a buffered input entry. + * 注册缓冲的输入记录。 + * @param InputEntry The input entry to register. 要注册的输入记录。 + */ + virtual void RegisterBufferedInputEntry(const FGIPS_BufferedInput& InputEntry); + + /** + * Event triggered when an input action generates an event. + * 输入动作生成事件时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIPS_ReceivedInputSignature OnReceivedInput; + +protected: + /** + * Callback for input action events. + * 输入动作事件的回调。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + */ + UFUNCTION() + void InputActionCallback(const FInputActionInstance& ActionData, FGameplayTag InputTag, ETriggerEvent TriggerEvent); + + /** + * Mapping of input tags to action value bindings. + * 输入标签到动作值绑定的映射。 + */ + UPROPERTY(VisibleInstanceOnly, Category="GIPS|Input", meta=(ForceInlineRow)) + TMap InputActionValueBindings; + + /** + * Tracks the last action value for each input action. + * 跟踪每个输入动作的最后动作值。 + */ + UPROPERTY(VisibleInstanceOnly, Category="GIPS|Input", meta=(ForceInlineRow)) + TMap LastInputActionValues; + + /** + * List of passed input entries. + * 通过的输入记录列表。 + */ + UPROPERTY(VisibleAnywhere, Category="GIPS|Input") + TArray PassedInputEntries; + + /** + * List of blocked input entries. + * 阻止的输入记录列表。 + */ + UPROPERTY(VisibleAnywhere, Category="GIPS|Input") + TArray BlockedInputEntries; + + /** + * List of buffered input entries. + * 缓冲的输入记录列表。 + */ + UPROPERTY(VisibleAnywhere, Category="GIPS|Input") + TArray BufferedInputEntries; + +#pragma region InputBuffer + +public: + /** + * Attempts to save an input to the buffer. + * 尝试将输入保存到缓冲区。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input was saved, false otherwise. 如果输入被保存则返回true,否则返回false。 + */ + bool TrySaveInput(const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + + /** + * Fires buffered input actions. + * 触发缓冲的输入动作。 + */ + void FireBufferedInput(); + + /** + * Event triggered when buffered input is fired. + * 缓冲输入触发时的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIPS_ReceivedInputSignature OnFireBufferedInput; + + /** + * Opens an input buffer window to allow input saving. + * 开启输入缓冲窗口以允许保存输入。 + * @param BufferWindowName The name of the buffer window to open. 要开启的缓冲窗口名称。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|InputBuffer") + virtual void OpenInputBufferWindow(UPARAM(meta=(Categories="GIPS.InputBuffer")) + FGameplayTag BufferWindowName); + + /** + * Closes an input buffer window. + * 关闭输入缓冲窗口。 + * @param BufferWindowName The name of the buffer window to close. 要关闭的缓冲窗口名称。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|InputBuffer") + virtual void CloseInputBufferWindow(UPARAM(meta=(Categories="GIPS.InputBuffer")) + FGameplayTag BufferWindowName); + + /** + * Closes all active input buffer windows. + * 关闭所有激活的输入缓冲窗口。 + */ + UFUNCTION(BlueprintCallable, Category="GIPS|InputBuffer") + virtual void CloseActiveInputBufferWindows(); + + /** + * Gets the last buffered input. + * 获取最后缓冲的输入。 + * @return The last buffered input. 最后缓冲的输入。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIPS|InputBuffer") + FGIPS_BufferedInput GetLastBufferedInput() const; + + /** + * Gets all currently active buffer windows. + * 获取当前所有激活的缓冲窗口。 + * @return Map of active buffer windows. 激活的缓冲窗口映射。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIPS|InputBuffer") + TMap GetActiveBufferWindows() const; + +protected: + /** + * Resets the buffered input. + * 重置缓冲输入。 + */ + void ResetBufferedInput(); + + /** + * Attempts to save an input as a buffered input. + * 尝试将输入保存为缓冲输入。 + * @param BufferWindowName The buffer window name. 缓冲窗口名称。 + * @param ActionData The input action data. 输入动作数据。 + * @param InputTag The gameplay tag for the input. 输入的游戏标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return True if the input was saved, false otherwise. 如果输入被保存则返回true,否则返回false。 + */ + bool TrySaveAsBufferedInput(const FGameplayTag BufferWindowName, const FInputActionInstance& ActionData, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent); + + /** + * Map of currently active input buffer windows. + * 当前激活的输入缓冲窗口映射。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="GIPS|InputBuffer", meta=(ForceInlineRow)) + TMap ActiveBufferWindows; + + /** + * The last fired buffered input. + * 最后触发的缓冲输入。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="GIPS|InputBuffer") + FGIPS_BufferedInput LastBufferedInput; + + /** + * The current buffered input. + * 当前缓冲输入。 + */ + UPROPERTY() + FGIPS_BufferedInput CurrentBufferedInput; + +#pragma endregion + +#pragma region DataValidation +#if WITH_EDITOR + /** + * Validates the component data in the editor. + * 在编辑器中验证组件数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +#pragma endregion +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputTypes.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputTypes.h new file mode 100644 index 0000000..c9aaf7a --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_InputTypes.h @@ -0,0 +1,372 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "InputTriggers.h" +#include "Templates/TypeHash.h" +#include "UObject/Object.h" +#include "GIPS_InputTypes.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIPS_InputComponentSignature, UEnhancedInputComponent*, InputComponent); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGIPS_InputBufferWindowStateChangedSignature, FGameplayTag, BufferWindowName, bool, State); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIPS_ReceivedInputSignature, const FInputActionInstance&, ActionData, const FGameplayTag&, InputTag, ETriggerEvent, TriggerEvent); + +enum class ETriggerEvent : uint8; +class UGIPS_InputProcessor; + +/** + * Defines the type of input buffering behavior. + * 定义输入缓冲行为的类型。 + */ +UENUM() +enum class EGIPS_InputBufferType : uint8 +{ + /** + * Only the last input is executed when the buffer window closes. + * 仅在缓冲窗口关闭时执行最后按下的输入。 + */ + LastInput, + + /** + * Inputs are executed immediately during the buffer window. + * 在缓冲窗口期间立即执行输入。 + */ + Instant, + + /** + * Inputs with higher priority (earlier in the list) are executed first. + * 优先级较高的输入(列表中较早的)优先执行。 + */ + HighestPriority +}; + +/** + * Defines how input processors are executed for a single input event. + * 定义单个输入事件的处理器执行方式。 + */ +UENUM() +enum class EGIPS_InputProcessorExecutionType : uint8 +{ + /** + * All valid processors are executed sequentially in top-to-bottom order. + * 所有有效处理器按从上到下的顺序依次执行。 + */ + MatchAll, + + /** + * Only the first valid input processor is executed. + * 仅执行第一个有效的输入处理器。 + */ + FirstOnly +}; + +/** + * Struct for defining allowed inputs and their trigger events. + * 定义允许的输入及其触发事件的结构体。 + */ +USTRUCT() +struct GENERICINPUTSYSTEM_API FGIPS_AllowedInput +{ + GENERATED_BODY() + + /** + * The allowed input tag. + * 允许的输入标签。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag; + + /** + * Allowed trigger event types (all allowed if empty). + * 允许的触发事件类型(如果为空则允许所有)。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + TArray TriggerEvents{ETriggerEvent::Started}; + + /** + * Equality operator for allowed inputs. + * 允许输入的相等比较运算符。 + */ + friend bool operator==(const FGIPS_AllowedInput& Lhs, const FGIPS_AllowedInput& RHS) + { + return Lhs.InputTag == RHS.InputTag; + } + + /** + * Inequality operator for allowed inputs. + * 允许输入的不相等比较运算符。 + */ + friend bool operator!=(const FGIPS_AllowedInput& Lhs, const FGIPS_AllowedInput& RHS) + { + return !(Lhs == RHS); + } + +#if WITH_EDITORONLY_DATA + /** + * Description for developers in the editor. + * 编辑器中用于开发者的描述。 + */ + UPROPERTY(EditAnywhere, Category = "Editor") + FString DevDescription; +#endif +}; + +/** + * Struct for defining an input buffer window. + * 定义输入缓冲窗口的结构体。 + */ +USTRUCT() +struct GENERICINPUTSYSTEM_API FGIPS_InputBufferWindow +{ + GENERATED_BODY() + +public: + /** + * The tag for the buffer window. + * 缓冲窗口的标签。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(Categories="GIPS.InputBuffer")) + FGameplayTag Tag = FGameplayTag::EmptyTag; + + /** + * The type of input buffering behavior. + * 输入缓冲行为的类型。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + EGIPS_InputBufferType BufferType = EGIPS_InputBufferType::LastInput; + + /** + * List of allowed inputs for the buffer window. + * 缓冲窗口允许的输入列表。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(TitleProperty="InputTag")) + TArray AllowedInputs; + + /** + * Finds the index of an allowed input. + * 查找允许输入的索引。 + * @param InputTag The input tag to find. 要查找的输入标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return The index of the allowed input, or INDEX_NONE if not found. 允许输入的索引,如果未找到则返回INDEX_NONE。 + */ + int32 IndexOfAllowedInput(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const + { + for (int32 i = 0; i < AllowedInputs.Num(); i++) + { + if (AllowedInputs[i].InputTag == InputTag && (AllowedInputs[i].TriggerEvents.IsEmpty() || AllowedInputs[i].TriggerEvents.Contains(TriggerEvent))) + { + return i; + } + } + return INDEX_NONE; + } + + /** + * Equality operator for buffer window tags. + * 缓冲窗口标签的相等比较运算符。 + */ + bool operator==(const FGameplayTag& OtherTag) const; + + /** + * Inequality operator for buffer window tags. + * 缓冲窗口标签的不相等比较运算符。 + */ + bool operator!=(const FGameplayTag& OtherTag) const; +}; + +/** + * Struct for storing a single input entry. + * 存储单个输入记录的结构体。 + */ +USTRUCT(BlueprintType, meta=(DisplayName="GIPS Input Entry")) +struct GENERICINPUTSYSTEM_API FGIPS_BufferedInput +{ + GENERATED_BODY() + + /** + * The input tag for the entry. + * 输入记录的输入标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIPS|Input", meta=(Categories="InputTag,GIPS.InputTag")) + FGameplayTag InputTag{FGameplayTag::EmptyTag}; + + /** + * The input action data. + * 输入动作数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIPS|Input") + FInputActionInstance ActionData; + + /** + * The trigger event type. + * 触发事件类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIPS|Input") + ETriggerEvent TriggerEvent{ETriggerEvent::None}; + + /** + * Equality operator for buffered inputs. + * 缓冲输入的相等比较运算符。 + */ + friend bool operator==(const FGIPS_BufferedInput& Lhs, const FGIPS_BufferedInput& RHS) + { + return Lhs.InputTag == RHS.InputTag + && Lhs.TriggerEvent == RHS.TriggerEvent; + } + + /** + * Inequality operator for buffered inputs. + * 缓冲输入的不相等比较运算符。 + */ + friend bool operator!=(const FGIPS_BufferedInput& Lhs, const FGIPS_BufferedInput& RHS) + { + return !(Lhs == RHS); + } + + FGIPS_BufferedInput() = default; + + /** + * Constructor for buffered input. + * 缓冲输入的构造函数。 + */ + FGIPS_BufferedInput(const FGameplayTag& InputTag, const FInputActionInstance& ActionData, const ETriggerEvent TriggerEvent) + : InputTag(InputTag), + ActionData(ActionData), + TriggerEvent(TriggerEvent) + { + } + + /** + * Copy constructor for buffered input. + * 缓冲输入的复制构造函数。 + */ + FGIPS_BufferedInput(const FGIPS_BufferedInput& Other) + : InputTag(Other.InputTag), + ActionData(Other.ActionData), + TriggerEvent(Other.TriggerEvent) + { + } + + /** + * Assignment operator for buffered input. + * 缓冲输入的赋值运算符。 + */ + FGIPS_BufferedInput& operator=(const FGIPS_BufferedInput& Other) + { + if (this == &Other) + return *this; + InputTag = Other.InputTag; + ActionData = Other.ActionData; + TriggerEvent = Other.TriggerEvent; + return *this; + } + + /** + * Converts the buffered input to a string. + * 将缓冲输入转换为字符串。 + * @return The string representation of the buffered input. 缓冲输入的字符串表示。 + */ + FString ToString() const; +}; + +/** + * Struct for defining input action settings. + * 定义输入动作设置的结构体。 + */ +USTRUCT() +struct GENERICINPUTSYSTEM_API FGIPS_InputActionSetting +{ + GENERATED_BODY() + + /** + * The input action object. + * 输入动作对象。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + TObjectPtr InputAction; + + /** + * Enables value binding for the input action. + * 为输入动作启用值绑定。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + bool bValueBinding{true}; +}; + +/** + * Struct for defining relationships between actor states and input permissions. + * 定义演员状态与输入权限关系的结构体。 + */ +USTRUCT() +struct GENERICINPUTSYSTEM_API FGIPS_InputTagRelationship +{ + GENERATED_BODY() + + /** + * Query to check if the actor's tags allow the input. + * 检查演员标签是否允许输入的查询。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input") + FGameplayTagQuery ActorTagQuery; + + /** + * List of allowed inputs if the tag query is satisfied. + * 如果标签查询满足,则允许的输入列表。 + * @note Empty means no check on if input is allowed. 留空则不检查是否允许。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(TitleProperty="InputTag")) + TArray AllowedInputs; + + /** + * List of blocked inputs if the tag query is satisfied. + * 如果标签查询满足,则禁止的输入列表。 + * @note Empty means no check on if input is blocked. 留空则不检查是否阻挡。 + */ + UPROPERTY(EditAnywhere, Category="GIPS|Input", meta=(TitleProperty="InputTag")) + TArray BlockedInputs; + + /** + * Finds the index of an allowed input. + * 查找允许输入的索引。 + * @param InputTag The input tag to find. 要查找的输入标签。 + * @param TriggerEvent The trigger event type. 触发事件类型。 + * @return The index of the allowed input, or INDEX_NONE if not found. 允许输入的索引,如果未找到则返回INDEX_NONE。 + */ + int32 IndexOfAllowedInput(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const + { + for (int32 i = 0; i < AllowedInputs.Num(); i++) + { + if (AllowedInputs[i].InputTag == InputTag && (AllowedInputs[i].TriggerEvents.IsEmpty() || AllowedInputs[i].TriggerEvents.Contains(TriggerEvent))) + { + return i; + } + } + return INDEX_NONE; + } + + int32 IndexOfBlockedInput(const FGameplayTag& InputTag, const ETriggerEvent& TriggerEvent) const + { + for (int32 i = 0; i < BlockedInputs.Num(); i++) + { + if (BlockedInputs[i].InputTag == InputTag && (BlockedInputs[i].TriggerEvents.IsEmpty() || BlockedInputs[i].TriggerEvents.Contains(TriggerEvent))) + { + return i; + } + } + return INDEX_NONE; + } + +#if WITH_EDITORONLY_DATA + /** + * Friendly name for displaying in the editor. + * 在编辑器中显示的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_LogChannels.h b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_LogChannels.h new file mode 100644 index 0000000..e3ad59b --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GIPS_LogChannels.h @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" + +GENERICINPUTSYSTEM_API FString GetGIPSLogContextString(const UObject* ContextObject = nullptr); + + +GENERICINPUTSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGIPS, Log, All); + + +#define GIPS_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIPS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GIPS_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIPS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGIPSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GIPS_OWNED_CLOG(LogOwner,Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIPS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGIPSLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GCS/Source/GenericInputSystem/Public/GenericInputSystem.h b/Plugins/GCS/Source/GenericInputSystem/Public/GenericInputSystem.h new file mode 100644 index 0000000..0a05bb4 --- /dev/null +++ b/Plugins/GCS/Source/GenericInputSystem/Public/GenericInputSystem.h @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FGenericInputSystemModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GGS/Config/BaseGenericGameSystem.ini b/Plugins/GGS/Config/BaseGenericGameSystem.ini new file mode 100644 index 0000000..f30cbce --- /dev/null +++ b/Plugins/GGS/Config/BaseGenericGameSystem.ini @@ -0,0 +1 @@ +[CoreRedirects] diff --git a/Plugins/GGS/Config/FilterPlugin.ini b/Plugins/GGS/Config/FilterPlugin.ini new file mode 100644 index 0000000..386e260 --- /dev/null +++ b/Plugins/GGS/Config/FilterPlugin.ini @@ -0,0 +1,9 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll +/Config/* \ No newline at end of file diff --git a/Plugins/GGS/GenericGameSystem.uplugin b/Plugins/GGS/GenericGameSystem.uplugin new file mode 100644 index 0000000..5ddda4c --- /dev/null +++ b/Plugins/GGS/GenericGameSystem.uplugin @@ -0,0 +1,92 @@ +{ + "FileVersion": 3, + "Version": 7, + "VersionName": "1.5.1", + "FriendlyName": "GenericGameSystem", + "Description": "Common functionalities for Interaction, context based svf/vfx play, Camera modes management, and CommonUI extensions.", + "Category": "Gameplay", + "CreatedBy": "YuewuDev", + "CreatedByURL": "https://yuewu.dev/en", + "DocsURL": "https://www.yuewu.dev/en/wiki", + "MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/6904d4d1-c7af-4973-b10e-a88c4436dab5", + "SupportURL": "https://discord.com/invite/xMRXAB2", + "EngineVersion": "5.7.0", + "CanContainContent": false, + "Installed": true, + "Modules": [ + { + "Name": "GenericEffectsSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericCameraSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericUISystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericGameSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + } + ], + "Plugins": [ + { + "Name": "Niagara", + "Enabled": true + }, + { + "Name": "CommonUI", + "Enabled": true + }, + { + "Name": "EnhancedInput", + "Enabled": true + }, + { + "Name": "ModularGameplay", + "Enabled": true + }, + { + "Name": "TargetingSystem", + "Enabled": true + }, + { + "Name": "SmartObjects", + "Enabled": true + }, + { + "Name": "GameplayBehaviors", + "Enabled": true + }, + { + "Name": "GameplayBehaviorSmartObjects", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/GGS/Resources/Icon128.png b/Plugins/GGS/Resources/Icon128.png new file mode 100644 index 0000000..d617ef1 Binary files /dev/null and b/Plugins/GGS/Resources/Icon128.png differ diff --git a/Plugins/GGS/Source/GenericCameraSystem/GenericCameraSystem.Build.cs b/Plugins/GGS/Source/GenericCameraSystem/GenericCameraSystem.Build.cs new file mode 100644 index 0000000..b9b7dd9 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/GenericCameraSystem.Build.cs @@ -0,0 +1,54 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericCameraSystem : ModuleRules +{ + public GenericCameraSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "GameplayTags" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode.cpp new file mode 100644 index 0000000..76c99b3 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode.cpp @@ -0,0 +1,258 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GCMS_CameraMode.h" + +#include "GCMS_CameraSystemComponent.h" +#include "Components/CapsuleComponent.h" +#include "Engine/Canvas.h" +#include "GameFramework/Character.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode) + + +////////////////////////////////////////////////////////////////////////// +// FGMS_CameraModeView +////////////////////////////////////////////////////////////////////////// +FGCMS_CameraModeView::FGCMS_CameraModeView() + : Location(ForceInit) + , Rotation(ForceInit) + , ControlRotation(ForceInit) + , FieldOfView(80.0f) +{ +} + +void FGCMS_CameraModeView::Blend(const FGCMS_CameraModeView& Other, float OtherWeight) +{ + if (OtherWeight <= 0.0f) + { + return; + } + else if (OtherWeight >= 1.0f) + { + *this = Other; + return; + } + + Location = FMath::Lerp(Location, Other.Location, OtherWeight); + + const FRotator DeltaRotation = (Other.Rotation - Rotation).GetNormalized(); + Rotation = Rotation + (OtherWeight * DeltaRotation); + + const FRotator DeltaControlRotation = (Other.ControlRotation - ControlRotation).GetNormalized(); + ControlRotation = ControlRotation + (OtherWeight * DeltaControlRotation); + + SprintArmSocketOffset = FMath::Lerp(SprintArmSocketOffset, Other.SprintArmSocketOffset, OtherWeight); + SprintArmTargetOffset = FMath::Lerp(SprintArmTargetOffset, Other.SprintArmTargetOffset, OtherWeight); + SprintArmLength = FMath::Lerp(SprintArmLength, Other.SprintArmLength, OtherWeight); + + FieldOfView = FMath::Lerp(FieldOfView, Other.FieldOfView, OtherWeight); +} + + +////////////////////////////////////////////////////////////////////////// +// UGCMS_CameraMode +////////////////////////////////////////////////////////////////////////// +UGCMS_CameraMode::UGCMS_CameraMode() +{ + FieldOfView = 80.0f; + ViewPitchMin = -89.0f; + ViewPitchMax = 89.0f; + + BlendTime = 0.5f; + BlendFunction = EGCMS_CameraModeBlendFunction::EaseOut; + BlendExponent = 4.0f; + BlendAlpha = 1.0f; + BlendWeight = 1.0f; + ActiveTime = 0.0f; + MaxActiveTime = 0.0f; +} + +UWorld* UGCMS_CameraMode::GetWorld() const +{ + return HasAnyFlags(RF_ClassDefaultObject) ? nullptr : GetOuter()->GetWorld(); +} + +AActor* UGCMS_CameraMode::GetTargetActor() const +{ + if (UGCMS_CameraSystemComponent* Component = Cast(GetOuter())) + { + return Component->GetOwner(); + } + return nullptr; +} + +FVector UGCMS_CameraMode::GetPivotLocation_Implementation() const +{ + const AActor* TargetActor = GetTargetActor(); + check(TargetActor); + + if (const APawn* TargetPawn = Cast(TargetActor)) + { + // Height adjustments for characters to account for crouching. + if (const ACharacter* TargetCharacter = Cast(TargetPawn)) + { + const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject(); + check(TargetCharacterCDO); + + const UCapsuleComponent* CapsuleComp = TargetCharacter->GetCapsuleComponent(); + check(CapsuleComp); + + const UCapsuleComponent* CapsuleCompCDO = TargetCharacterCDO->GetCapsuleComponent(); + check(CapsuleCompCDO); + + const float DefaultHalfHeight = CapsuleCompCDO->GetUnscaledCapsuleHalfHeight(); + const float ActualHalfHeight = CapsuleComp->GetUnscaledCapsuleHalfHeight(); + const float HeightAdjustment = (DefaultHalfHeight - ActualHalfHeight) + TargetCharacterCDO->BaseEyeHeight; + + return TargetCharacter->GetActorLocation() + (FVector::UpVector * HeightAdjustment); + } + + return TargetPawn->GetPawnViewLocation(); + } + + return TargetActor->GetActorLocation(); +} + +FRotator UGCMS_CameraMode::GetPivotRotation_Implementation() const +{ + const AActor* TargetActor = GetTargetActor(); + check(TargetActor); + + if (const APawn* TargetPawn = Cast(TargetActor)) + { + return TargetPawn->GetViewRotation(); + } + + return TargetActor->GetActorRotation(); +} + +void UGCMS_CameraMode::UpdateCameraMode(float DeltaTime) +{ + ActiveTime += DeltaTime; + + if (MaxActiveTime > 0 && ActiveTime >= MaxActiveTime) + { + if (UGCMS_CameraSystemComponent* Component = Cast(GetOuter())) + { + Component->PushDefaultCameraMode(); + } + } + + UpdateView(DeltaTime); + UpdateBlending(DeltaTime); +} + +void UGCMS_CameraMode::UpdateView(float DeltaTime) +{ + FVector PivotLocation = GetPivotLocation(); + FRotator PivotRotation = GetPivotRotation(); + + PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax); + + OnUpdateView(DeltaTime, PivotLocation, PivotRotation); +} + +void UGCMS_CameraMode::SetBlendWeight(float Weight) +{ + BlendWeight = FMath::Clamp(Weight, 0.0f, 1.0f); + + // Since we're setting the blend weight directly, we need to calculate the blend alpha to account for the blend function. + const float InvExponent = (BlendExponent > 0.0f) ? (1.0f / BlendExponent) : 1.0f; + + switch (BlendFunction) + { + case EGCMS_CameraModeBlendFunction::Linear: + BlendAlpha = BlendWeight; + break; + + case EGCMS_CameraModeBlendFunction::EaseIn: + BlendAlpha = FMath::InterpEaseIn(0.0f, 1.0f, BlendWeight, InvExponent); + break; + + case EGCMS_CameraModeBlendFunction::EaseOut: + BlendAlpha = FMath::InterpEaseOut(0.0f, 1.0f, BlendWeight, InvExponent); + break; + + case EGCMS_CameraModeBlendFunction::EaseInOut: + BlendAlpha = FMath::InterpEaseInOut(0.0f, 1.0f, BlendWeight, InvExponent); + break; + + default: + checkf(false, TEXT("SetBlendWeight: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction); + break; + } +} + +UCameraComponent* UGCMS_CameraMode::GetAssociatedCamera() const +{ + if (UGCMS_CameraSystemComponent* Component = Cast(GetOuter())) + { + return Component->GetAssociatedCamera(); + } + return nullptr; +} + +USpringArmComponent* UGCMS_CameraMode::GetAssociatedSprintArm() const +{ + if (UGCMS_CameraSystemComponent* Component = Cast(GetOuter())) + { + return Component->GetAssociatedSprintArm(); + } + return nullptr; +} + +void UGCMS_CameraMode::UpdateBlending(float DeltaTime) +{ + if (BlendTime > 0.0f) + { + BlendAlpha += (DeltaTime / BlendTime); + BlendAlpha = FMath::Min(BlendAlpha, 1.0f); + } + else + { + BlendAlpha = 1.0f; + } + + const float Exponent = (BlendExponent > 0.0f) ? BlendExponent : 1.0f; + + switch (BlendFunction) + { + case EGCMS_CameraModeBlendFunction::Linear: + BlendWeight = BlendAlpha; + break; + + case EGCMS_CameraModeBlendFunction::EaseIn: + BlendWeight = FMath::InterpEaseIn(0.0f, 1.0f, BlendAlpha, Exponent); + break; + + case EGCMS_CameraModeBlendFunction::EaseOut: + BlendWeight = FMath::InterpEaseOut(0.0f, 1.0f, BlendAlpha, Exponent); + break; + + case EGCMS_CameraModeBlendFunction::EaseInOut: + BlendWeight = FMath::InterpEaseInOut(0.0f, 1.0f, BlendAlpha, Exponent); + break; + + default: + checkf(false, TEXT("UpdateBlending: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction); + break; + } +} + +void UGCMS_CameraMode::OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation) +{ + View.Location = PivotLocation; + View.Rotation = PivotRotation; + View.ControlRotation = View.Rotation; + View.FieldOfView = FieldOfView; +} + +void UGCMS_CameraMode::DrawDebug(UCanvas* Canvas) const +{ + check(Canvas); + + FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; + + DisplayDebugManager.SetDrawColor(FColor::White); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" GMS_CameraMode: %s (%f)"), *GetName(), BlendWeight)); +} diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraModeStack.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraModeStack.cpp new file mode 100644 index 0000000..2af537c --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraModeStack.cpp @@ -0,0 +1,257 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCMS_CameraModeStack.h" + +#include "Engine/Canvas.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraModeStack) + +////////////////////////////////////////////////////////////////////////// +// UGCMS_CameraModeStack +////////////////////////////////////////////////////////////////////////// +UGCMS_CameraModeStack::UGCMS_CameraModeStack() +{ + bIsActive = true; +} + +void UGCMS_CameraModeStack::ActivateStack() +{ + if (!bIsActive) + { + bIsActive = true; + + // Notify camera modes that they are being activated. + for (UGCMS_CameraMode* CameraMode : CameraModeStack) + { + check(CameraMode); + CameraMode->OnActivation(); + } + } +} + +void UGCMS_CameraModeStack::DeactivateStack() +{ + if (bIsActive) + { + bIsActive = false; + + // Notify camera modes that they are being deactivated. + for (UGCMS_CameraMode* CameraMode : CameraModeStack) + { + check(CameraMode); + CameraMode->OnDeactivation(); + } + } +} + +void UGCMS_CameraModeStack::PushCameraMode(TSubclassOf CameraModeClass) +{ + if (!CameraModeClass) + { + return; + } + + // get camera from pool. + UGCMS_CameraMode* NewCameraMode = GetCameraModeInstance(CameraModeClass); + check(NewCameraMode); + + int32 StackSize = CameraModeStack.Num(); + + if ((StackSize > 0) && (CameraModeStack[0] == NewCameraMode)) + { + // Already top of stack. + return; + } + + // See if it's already in the stack and remove it. + // Figure out how much it was contributing to the stack. + int32 ExistingStackIndex = INDEX_NONE; + float ExistingStackContribution = 1.0f; + + for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex) + { + if (CameraModeStack[StackIndex] == NewCameraMode) + { + ExistingStackIndex = StackIndex; + ExistingStackContribution *= NewCameraMode->GetBlendWeight(); + break; + } + else + { + ExistingStackContribution *= (1.0f - CameraModeStack[StackIndex]->GetBlendWeight()); + } + } + + // existing in stack, remove it before add. + if (ExistingStackIndex != INDEX_NONE) + { + CameraModeStack.RemoveAt(ExistingStackIndex); + StackSize--; + } + else + { + ExistingStackContribution = 0.0f; + } + + // Decide what initial weight to start with. + const bool bShouldBlend = ((NewCameraMode->GetBlendTime() > 0.0f) && (StackSize > 0)); + const float BlendWeight = (bShouldBlend ? ExistingStackContribution : 1.0f); + + NewCameraMode->SetBlendWeight(BlendWeight); + + // Add new entry to top of stack. + CameraModeStack.Insert(NewCameraMode, 0); + + // Make sure stack bottom is always weighted 100%. + CameraModeStack.Last()->SetBlendWeight(1.0f); + + // Let the camera mode know if it's being added to the stack. + if (ExistingStackIndex == INDEX_NONE) + { + NewCameraMode->OnActivation(); + } +} + +void UGCMS_CameraModeStack::PopCameraMode(TSubclassOf CameraModeClass) +{ +} + +bool UGCMS_CameraModeStack::EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView) +{ + if (!bIsActive) + { + return false; + } + + //Update camera modes. + UpdateStack(DeltaTime); + + //Blend values from camera modes. + BlendStack(OutCameraModeView); + + return true; +} + +UGCMS_CameraMode* UGCMS_CameraModeStack::GetCameraModeInstance(TSubclassOf CameraModeClass) +{ + check(CameraModeClass); + + // First see if we already created one. + for (UGCMS_CameraMode* CameraMode : CameraModeInstances) + { + if ((CameraMode != nullptr) && (CameraMode->GetClass() == CameraModeClass)) + { + return CameraMode; + } + } + + // Not found, so we need to create it. + UGCMS_CameraMode* NewCameraMode = NewObject(GetOuter(), CameraModeClass, NAME_None, RF_NoFlags); + check(NewCameraMode); + + CameraModeInstances.Add(NewCameraMode); + + return NewCameraMode; +} + +void UGCMS_CameraModeStack::UpdateStack(float DeltaTime) +{ + const int32 StackSize = CameraModeStack.Num(); + if (StackSize <= 0) + { + return; + } + + int32 RemoveCount = 0; + int32 RemoveIndex = INDEX_NONE; + + for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex) + { + UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex]; + check(CameraMode); + + CameraMode->UpdateCameraMode(DeltaTime); + + if (CameraMode->GetBlendWeight() >= 1.0f) + { + // Everything below this mode is now irrelevant and can be removed. + RemoveIndex = (StackIndex + 1); + RemoveCount = (StackSize - RemoveIndex); + break; + } + } + + if (RemoveCount > 0) + { + // Let the camera modes know they being removed from the stack. + for (int32 StackIndex = RemoveIndex; StackIndex < StackSize; ++StackIndex) + { + UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex]; + check(CameraMode); + + CameraMode->OnDeactivation(); + } + + CameraModeStack.RemoveAt(RemoveIndex, RemoveCount); + } +} + +void UGCMS_CameraModeStack::BlendStack(FGCMS_CameraModeView& OutCameraModeView) const +{ + const int32 StackSize = CameraModeStack.Num(); + if (StackSize <= 0) + { + return; + } + + // Start at the bottom and blend up the stack + const UGCMS_CameraMode* CameraMode = CameraModeStack[StackSize - 1]; + check(CameraMode); + + OutCameraModeView = CameraMode->GetCameraModeView(); + + for (int32 StackIndex = (StackSize - 2); StackIndex >= 0; --StackIndex) + { + CameraMode = CameraModeStack[StackIndex]; + check(CameraMode); + + OutCameraModeView.Blend(CameraMode->GetCameraModeView(), CameraMode->GetBlendWeight()); + } +} + +void UGCMS_CameraModeStack::DrawDebug(UCanvas* Canvas) const +{ + check(Canvas); + + FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; + + DisplayDebugManager.SetDrawColor(FColor::Green); + DisplayDebugManager.DrawString(FString(TEXT(" --- Camera Modes (Begin) ---"))); + + for (const UGCMS_CameraMode* CameraMode : CameraModeStack) + { + check(CameraMode); + CameraMode->DrawDebug(Canvas); + } + + DisplayDebugManager.SetDrawColor(FColor::Green); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" --- Camera Modes (End) ---"))); +} + +void UGCMS_CameraModeStack::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const +{ + if (CameraModeStack.Num() == 0) + { + OutWeightOfTopLayer = 1.0f; + OutTagOfTopLayer = FGameplayTag(); + return; + } + else + { + UGCMS_CameraMode* TopEntry = CameraModeStack.Last(); + check(TopEntry); + OutWeightOfTopLayer = TopEntry->GetBlendWeight(); + OutTagOfTopLayer = TopEntry->GetCameraTypeTag(); + } +} diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_ThirdPerson.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_ThirdPerson.cpp new file mode 100644 index 0000000..0405ce7 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_ThirdPerson.cpp @@ -0,0 +1,100 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// #include "GCMS_CameraMode_ThirdPerson.h" +// +// #include "GCMS_CameraAssistInterface.h" +// #include "GCMS_CameraMode.h" +// #include "Components/PrimitiveComponent.h" +// #include "GCMS_CameraPenetrationAvoidanceFeeler.h" +// #include "Curves/CurveVector.h" +// #include "Engine/Canvas.h" +// #include "GameFramework/Controller.h" +// #include "GameFramework/Character.h" +// #include "Math/RotationMatrix.h" +// +// #include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_ThirdPerson) +// +// +// UGCMS_CameraMode_ThirdPerson::UGCMS_CameraMode_ThirdPerson() +// { +// TargetOffsetCurve = nullptr; +// } +// +// void UGCMS_CameraMode_ThirdPerson::UpdateView_Implementation(float DeltaTime) +// { +// UpdateForTarget(DeltaTime); +// UpdateCrouchOffset(DeltaTime); +// +// FVector PivotLocation = GetPivotLocation() + CurrentCrouchOffset; +// FRotator PivotRotation = GetPivotRotation(); +// +// PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax); +// +// View.Location = PivotLocation; +// View.Rotation = PivotRotation; +// View.ControlRotation = View.Rotation; +// View.FieldOfView = FieldOfView; +// +// // Apply third person offset using pitch. +// if (!bUseRuntimeFloatCurves) +// { +// if (TargetOffsetCurve) +// { +// const FVector TargetOffset = TargetOffsetCurve->GetVectorValue(PivotRotation.Pitch); +// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset); +// } +// } +// else +// { +// FVector TargetOffset(0.0f); +// +// TargetOffset.X = TargetOffsetX.GetRichCurveConst()->Eval(PivotRotation.Pitch); +// TargetOffset.Y = TargetOffsetY.GetRichCurveConst()->Eval(PivotRotation.Pitch); +// TargetOffset.Z = TargetOffsetZ.GetRichCurveConst()->Eval(PivotRotation.Pitch); +// +// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset); +// } +// +// // Adjust final desired camera location to prevent any penetration +// UpdatePreventPenetration(DeltaTime); +// } +// +// void UGCMS_CameraMode_ThirdPerson::UpdateForTarget(float DeltaTime) +// { +// if (const ACharacter* TargetCharacter = Cast(GetTargetActor())) +// { +// if (TargetCharacter->bIsCrouched) +// { +// const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject(); +// const float CrouchedHeightAdjustment = TargetCharacterCDO->CrouchedEyeHeight - TargetCharacterCDO->BaseEyeHeight; +// +// SetTargetCrouchOffset(FVector(0.f, 0.f, CrouchedHeightAdjustment)); +// +// return; +// } +// } +// +// SetTargetCrouchOffset(FVector::ZeroVector); +// } +// +// void UGCMS_CameraMode_ThirdPerson::SetTargetCrouchOffset(FVector NewTargetOffset) +// { +// CrouchOffsetBlendPct = 0.0f; +// InitialCrouchOffset = CurrentCrouchOffset; +// TargetCrouchOffset = NewTargetOffset; +// } +// +// +// void UGCMS_CameraMode_ThirdPerson::UpdateCrouchOffset(float DeltaTime) +// { +// if (CrouchOffsetBlendPct < 1.0f) +// { +// CrouchOffsetBlendPct = FMath::Min(CrouchOffsetBlendPct + DeltaTime * CrouchOffsetBlendMultiplier, 1.0f); +// CurrentCrouchOffset = FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f); +// } +// else +// { +// CurrentCrouchOffset = TargetCrouchOffset; +// CrouchOffsetBlendPct = 1.0f; +// } +// } diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_WithPenetrationAvoidance.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_WithPenetrationAvoidance.cpp new file mode 100644 index 0000000..2f189ec --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraMode_WithPenetrationAvoidance.cpp @@ -0,0 +1,290 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCMS_CameraMode_WithPenetrationAvoidance.h" +#include "Engine/Canvas.h" +#include "GameFramework/Pawn.h" +#include "Components/PrimitiveComponent.h" +#include "GameFramework/Controller.h" +#include "Engine/HitResult.h" +#include "GCMS_CameraAssistInterface.h" +#include "GameFramework/CameraBlockingVolume.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_WithPenetrationAvoidance) + +namespace GMS_CameraMode_WithPenetrationAvoidance_Statics +{ + static const FName NAME_IgnoreCameraCollision = TEXT("IgnoreCameraCollision"); +} + + +UGCMS_CameraMode_WithPenetrationAvoidance::UGCMS_CameraMode_WithPenetrationAvoidance() +{ + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +00.0f, 0.0f), 1.00f, 1.00f, 14.f, 0)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+20.0f, +00.0f, 0.0f), 1.00f, 1.00f, 00.f, 4)); + PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(-20.0f, +00.0f, 0.0f), 0.50f, 0.50f, 00.f, 4)); +} + +void UGCMS_CameraMode_WithPenetrationAvoidance::UpdatePreventPenetration(float DeltaTime) +{ + if (!bPreventPenetration) + { + return; + } + + AActor* TargetActor = GetTargetActor(); + + APawn* TargetPawn = Cast(TargetActor); + AController* TargetController = TargetPawn ? TargetPawn->GetController() : nullptr; + IGCMS_CameraAssistInterface* TargetControllerAssist = Cast(TargetController); + + IGCMS_CameraAssistInterface* TargetActorAssist = Cast(TargetActor); + + TOptional OptionalPPTarget = TargetActorAssist ? TargetActorAssist->GetCameraPreventPenetrationTarget() : TOptional(); + AActor* PPActor = OptionalPPTarget.IsSet() ? OptionalPPTarget.GetValue() : TargetActor; + IGCMS_CameraAssistInterface* PPActorAssist = OptionalPPTarget.IsSet() ? Cast(PPActor) : nullptr; + + const UPrimitiveComponent* PPActorRootComponent = Cast(PPActor->GetRootComponent()); + if (PPActorRootComponent) + { + // Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming. + // Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible. + // Pick closest point on capsule to our aim line. + FVector ClosestPointOnLineToCapsuleCenter; + FVector SafeLocation = PPActor->GetActorLocation(); + FMath::PointDistToLine(SafeLocation, View.Rotation.Vector(), View.Location, ClosestPointOnLineToCapsuleCenter); + + // Adjust Safe distance height to be same as aim line, but within capsule. + float const PushInDistance = PenetrationAvoidanceFeelers[0].Extent + CollisionPushOutDistance; + float const MaxHalfHeight = PPActor->GetSimpleCollisionHalfHeight() - PushInDistance; + SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight); + + float DistanceSqr; + PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation); + // Push back inside capsule to avoid initial penetration when doing line checks. + if (PenetrationAvoidanceFeelers.Num() > 0) + { + SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance; + } + + // Then aim line to desired camera position + bool const bSingleRayPenetrationCheck = !bDoPredictiveAvoidance; + PreventCameraPenetration(bSingleRayPenetrationCheck, DeltaTime, PPActor, SafeLocation, View.Location, AimLineToDesiredPosBlockedPct); + + IGCMS_CameraAssistInterface* AssistArray[] = {TargetControllerAssist, TargetActorAssist, PPActorAssist}; + + if (AimLineToDesiredPosBlockedPct < ReportPenetrationPercent) + { + for (IGCMS_CameraAssistInterface* Assist : AssistArray) + { + if (Assist) + { + // camera is too close, tell the assists + Assist->OnCameraPenetratingTarget(); + } + } + } + } +} + +void UGCMS_CameraMode_WithPenetrationAvoidance::PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, FVector const& SafeLoc, FVector& CameraLoc, + float& DistBlockedPct) +{ +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.Reset(); +#endif + + float HardBlockedPct = DistBlockedPct; + float SoftBlockedPct = DistBlockedPct; + + FVector BaseRay = CameraLoc - SafeLoc; + FRotationMatrix BaseRayMatrix(BaseRay.Rotation()); + FVector BaseRayLocalUp, BaseRayLocalFwd, BaseRayLocalRight; + + BaseRayMatrix.GetScaledAxes(BaseRayLocalFwd, BaseRayLocalRight, BaseRayLocalUp); + + float DistBlockedPctThisFrame = 1.f; + + int32 const NumRaysToShoot = bSingleRayOnly ? FMath::Min(1, PenetrationAvoidanceFeelers.Num()) : PenetrationAvoidanceFeelers.Num(); + FCollisionQueryParams SphereParams(SCENE_QUERY_STAT(CameraPen), false, nullptr/*PlayerCamera*/); + + SphereParams.AddIgnoredActor(ViewTarget); + + //TODO ILyraCameraTarget.GetIgnoredActorsForCameraPentration(); + //if (IgnoreActorForCameraPenetration) + //{ + // SphereParams.AddIgnoredActor(IgnoreActorForCameraPenetration); + //} + + FCollisionShape SphereShape = FCollisionShape::MakeSphere(0.f); + UWorld* World = GetWorld(); + + for (int32 RayIdx = 0; RayIdx < NumRaysToShoot; ++RayIdx) + { + FGCMS_CameraPenetrationAvoidanceFeeler& Feeler = PenetrationAvoidanceFeelers[RayIdx]; + if (Feeler.FramesUntilNextTrace <= 0) + { + // calc ray target + FVector RayTarget; + { + FVector RotatedRay = BaseRay.RotateAngleAxis(Feeler.AdjustmentRot.Yaw, BaseRayLocalUp); + RotatedRay = RotatedRay.RotateAngleAxis(Feeler.AdjustmentRot.Pitch, BaseRayLocalRight); + RayTarget = SafeLoc + RotatedRay; + } + + // cast for world and pawn hits separately. this is so we can safely ignore the + // camera's target pawn + SphereShape.Sphere.Radius = Feeler.Extent; + ECollisionChannel TraceChannel = ECC_Camera; //(Feeler.PawnWeight > 0.f) ? ECC_Pawn : ECC_Camera; + + // do multi-line check to make sure the hits we throw out aren't + // masking real hits behind (these are important rays). + + // MT-> passing camera as actor so that camerablockingvolumes know when it's the camera doing traces + FHitResult Hit; + const bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, TraceChannel, SphereShape, SphereParams); +#if ENABLE_DRAW_DEBUG + if (World->TimeSince(LastDrawDebugTime) < 1.f) + { + DrawDebugSphere(World, SafeLoc, SphereShape.Sphere.Radius, 8, FColor::Red); + DrawDebugSphere(World, bHit ? Hit.Location : RayTarget, SphereShape.Sphere.Radius, 8, FColor::Red); + DrawDebugLine(World, SafeLoc, bHit ? Hit.Location : RayTarget, FColor::Red); + } +#endif // ENABLE_DRAW_DEBUG + + Feeler.FramesUntilNextTrace = Feeler.TraceInterval; + + const AActor* HitActor = Hit.GetActor(); + + if (bHit && HitActor) + { + bool bIgnoreHit = false; + + if (HitActor->ActorHasTag(GMS_CameraMode_WithPenetrationAvoidance_Statics::NAME_IgnoreCameraCollision)) + { + bIgnoreHit = true; + SphereParams.AddIgnoredActor(HitActor); + } + + // Ignore CameraBlockingVolume hits that occur in front of the ViewTarget. + if (!bIgnoreHit && HitActor->IsA()) + { + const FVector ViewTargetForwardXY = ViewTarget->GetActorForwardVector().GetSafeNormal2D(); + const FVector ViewTargetLocation = ViewTarget->GetActorLocation(); + const FVector HitOffset = Hit.Location - ViewTargetLocation; + const FVector HitDirectionXY = HitOffset.GetSafeNormal2D(); + const float DotHitDirection = FVector::DotProduct(ViewTargetForwardXY, HitDirectionXY); + if (DotHitDirection > 0.0f) + { + bIgnoreHit = true; + // Ignore this CameraBlockingVolume on the remaining sweeps. + SphereParams.AddIgnoredActor(HitActor); + } + else + { +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); +#endif + } + } + + if (!bIgnoreHit) + { + float const Weight = Cast(Hit.GetActor()) ? Feeler.PawnWeight : Feeler.WorldWeight; + float NewBlockPct = Hit.Time; + NewBlockPct += (1.f - NewBlockPct) * (1.f - Weight); + + // Recompute blocked pct taking into account pushout distance. + NewBlockPct = ((Hit.Location - SafeLoc).Size() - CollisionPushOutDistance) / (RayTarget - SafeLoc).Size(); + DistBlockedPctThisFrame = FMath::Min(NewBlockPct, DistBlockedPctThisFrame); + + // This feeler got a hit, so do another trace next frame + Feeler.FramesUntilNextTrace = 0; + +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); +#endif + } + } + + if (RayIdx == 0) + { + // don't interpolate toward this one, snap to it + // assumes ray 0 is the center/main ray + HardBlockedPct = DistBlockedPctThisFrame; + } + else + { + SoftBlockedPct = DistBlockedPctThisFrame; + } + } + else + { + --Feeler.FramesUntilNextTrace; + } + } + + if (bResetInterpolation) + { + DistBlockedPct = DistBlockedPctThisFrame; + } + else if (DistBlockedPct < DistBlockedPctThisFrame) + { + // interpolate smoothly out + if (PenetrationBlendOutTime > DeltaTime) + { + DistBlockedPct = DistBlockedPct + DeltaTime / PenetrationBlendOutTime * (DistBlockedPctThisFrame - DistBlockedPct); + } + else + { + DistBlockedPct = DistBlockedPctThisFrame; + } + } + else + { + if (DistBlockedPct > HardBlockedPct) + { + DistBlockedPct = HardBlockedPct; + } + else if (DistBlockedPct > SoftBlockedPct) + { + // interpolate smoothly in + if (PenetrationBlendInTime > DeltaTime) + { + DistBlockedPct = DistBlockedPct - DeltaTime / PenetrationBlendInTime * (DistBlockedPct - SoftBlockedPct); + } + else + { + DistBlockedPct = SoftBlockedPct; + } + } + } + + DistBlockedPct = FMath::Clamp(DistBlockedPct, 0.f, 1.f); + if (DistBlockedPct < (1.f - ZERO_ANIMWEIGHT_THRESH)) + { + CameraLoc = SafeLoc + (CameraLoc - SafeLoc) * DistBlockedPct; + } +} + +void UGCMS_CameraMode_WithPenetrationAvoidance::DrawDebug(UCanvas* Canvas) const +{ + Super::DrawDebug(Canvas); + +#if ENABLE_DRAW_DEBUG + FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; + for (int i = 0; i < DebugActorsHitDuringCameraPenetration.Num(); i++) + { + DisplayDebugManager.DrawString( + FString::Printf(TEXT("HitActorDuringPenetration[%d]: %s") + , i + , *DebugActorsHitDuringCameraPenetration[i]->GetName())); + } + + LastDrawDebugTime = GetWorld()->GetTimeSeconds(); +#endif +} diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraPenetrationAvoidanceFeeler.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraPenetrationAvoidanceFeeler.cpp new file mode 100644 index 0000000..eee657b --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraPenetrationAvoidanceFeeler.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GCMS_CameraPenetrationAvoidanceFeeler.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraPenetrationAvoidanceFeeler) \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraSystemComponent.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraSystemComponent.cpp new file mode 100644 index 0000000..0a90e05 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GCMS_CameraSystemComponent.cpp @@ -0,0 +1,188 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GCMS_CameraSystemComponent.h" +#include "GameFramework/HUD.h" +#include "DisplayDebugHelpers.h" +#include "Engine/Canvas.h" +#include "Engine/Engine.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "GCMS_CameraMode.h" +#include "GCMS_CameraModeStack.h" +#include "Camera/CameraComponent.h" +#include "GameFramework/SpringArmComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraSystemComponent) + + +UGCMS_CameraSystemComponent::UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + CameraModeStack = nullptr; + PrimaryComponentTick.bCanEverTick = true; + FieldOfViewOffset = 0.0f; +} + +void UGCMS_CameraSystemComponent::OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos) +{ + if (DisplayInfo.IsDisplayOn(TEXT("CAMERA"))) + { + if (const UGCMS_CameraSystemComponent* CameraComponent = GetCameraSystemComponent(HUD->GetCurrentDebugTargetActor())) + { + CameraComponent->DrawDebug(Canvas); + } + } +} + +void UGCMS_CameraSystemComponent::OnRegister() +{ + Super::OnRegister(); + + if (!CameraModeStack) + { + CameraModeStack = NewObject(this); + check(CameraModeStack); + } +} + +void UGCMS_CameraSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (CameraModeStack && CameraModeStack->IsStackActivate() && AssociatedCameraComponent && AssociatedSprintArmComponent) + { + UpdateCameraModes(); + FGCMS_CameraModeView CameraModeView; + CameraModeStack->EvaluateStack(DeltaTime, CameraModeView); + + // Keep player controller in sync with the latest view. + if (APawn* TargetPawn = Cast(GetTargetActor())) + { + if (APlayerController* PC = TargetPawn->GetController()) + { + PC->SetControlRotation(CameraModeView.ControlRotation); + } + } + + AssociatedCameraComponent->FieldOfView = CameraModeView.FieldOfView; + + AssociatedSprintArmComponent->TargetArmLength = CameraModeView.SprintArmLength; + + AssociatedSprintArmComponent->SocketOffset = CameraModeView.SprintArmSocketOffset; + AssociatedSprintArmComponent->TargetOffset = CameraModeView.SprintArmTargetOffset; + } +} + +UCameraComponent* UGCMS_CameraSystemComponent::GetAssociatedCamera() const +{ + return AssociatedCameraComponent; +} + +USpringArmComponent* UGCMS_CameraSystemComponent::GetAssociatedSprintArm() const +{ + return AssociatedSprintArmComponent; +} + +void UGCMS_CameraSystemComponent::Activate(bool bReset) +{ + Super::Activate(bReset); + if (CameraModeStack) + { + if (IsActive()) + { + CameraModeStack->ActivateStack(); + } + else + { + CameraModeStack->DeactivateStack(); + } + } +} + +void UGCMS_CameraSystemComponent::Deactivate() +{ + Super::Deactivate(); + if (CameraModeStack) + { + CameraModeStack->DeactivateStack(); + } +} + +void UGCMS_CameraSystemComponent::UpdateCameraModes() +{ + check(CameraModeStack); + + if (CameraModeStack->IsStackActivate()) + { + if (DetermineCameraModeDelegate.IsBound()) + { + if (const TSubclassOf CameraMode = DetermineCameraModeDelegate.Execute()) + { + CameraModeStack->PushCameraMode(CameraMode); + } + } + } +} + +void UGCMS_CameraSystemComponent::PushCameraMode(TSubclassOf NewCameraMode) +{ + if (CameraModeStack->IsStackActivate()) + { + if (NewCameraMode) + { + CameraModeStack->PushCameraMode(NewCameraMode); + } + } +} + +void UGCMS_CameraSystemComponent::PushDefaultCameraMode() +{ + if (CameraModeStack->IsStackActivate()) + { + if (DefaultCameraMode) + { + CameraModeStack->PushCameraMode(DefaultCameraMode); + } + } +} + +void UGCMS_CameraSystemComponent::Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent) +{ + if (!CameraModeStack) + { + CameraModeStack = NewObject(this); + check(CameraModeStack); + } + + AssociatedCameraComponent = NewCameraComponent; + AssociatedSprintArmComponent = NewSpringArmComponent; +} + +void UGCMS_CameraSystemComponent::DrawDebug(UCanvas* Canvas) const +{ + check(Canvas); + + FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; + + DisplayDebugManager.SetFont(GEngine->GetSmallFont()); + DisplayDebugManager.SetDrawColor(FColor::Yellow); + DisplayDebugManager.DrawString(FString::Printf(TEXT("GCMS_CameraSystemComponent: %s"), *GetNameSafe(GetTargetActor()))); + + DisplayDebugManager.SetDrawColor(FColor::White); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" Location: %s"), *AssociatedCameraComponent->GetComponentLocation().ToCompactString())); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" Rotation: %s"), *AssociatedCameraComponent->GetComponentRotation().ToCompactString())); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmLength: %f"), AssociatedSprintArmComponent->TargetArmLength)); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmSocketOffset: %s"), *AssociatedSprintArmComponent->SocketOffset.ToCompactString())); + DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmTargetOffset: %s"), *AssociatedSprintArmComponent->TargetOffset.ToCompactString())); + + DisplayDebugManager.DrawString(FString::Printf(TEXT(" FOV: %f"), AssociatedCameraComponent->FieldOfView)); + + check(CameraModeStack); + CameraModeStack->DrawDebug(Canvas); +} + +void UGCMS_CameraSystemComponent::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const +{ + check(CameraModeStack); + CameraModeStack->GetBlendInfo(/*out*/ OutWeightOfTopLayer, /*out*/ OutTagOfTopLayer); +} diff --git a/Plugins/GGS/Source/GenericCameraSystem/Private/GenericCameraSystem.cpp b/Plugins/GGS/Source/GenericCameraSystem/Private/GenericCameraSystem.cpp new file mode 100644 index 0000000..3210a07 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Private/GenericCameraSystem.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericCameraSystem.h" + +#include "GameFramework/HUD.h" +#include "GCMS_CameraSystemComponent.h" + +#define LOCTEXT_NAMESPACE "FGenericCameraSystemModule" + +void FGenericCameraSystemModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +#if WITH_EDITOR +#if ENABLE_DRAW_DEBUG + if (!IsRunningDedicatedServer()) + { + AHUD::OnShowDebugInfo.AddStatic(&UGCMS_CameraSystemComponent::OnShowDebugInfo); + } +#endif // ENABLE_DRAW_DEBUG +#endif +} + +void FGenericCameraSystemModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericCameraSystemModule, GenericCameraSystem) diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraAssistInterface.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraAssistInterface.h new file mode 100644 index 0000000..cb73c9a --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraAssistInterface.h @@ -0,0 +1,41 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "GCMS_CameraAssistInterface.generated.h" + +/** */ +UINTERFACE(BlueprintType) +class UGCMS_CameraAssistInterface : public UInterface +{ + GENERATED_BODY() +}; + +class GENERICCAMERASYSTEM_API IGCMS_CameraAssistInterface +{ + GENERATED_BODY() + +public: + /** + * Get the list of actors that we're allowing the camera to penetrate. Useful in 3rd person cameras + * when you need the following camera to ignore things like the a collection of view targets, the pawn, + * a vehicle..etc. + */ + virtual void GetIgnoredActorsForCameraPentration(TArray& OutActorsAllowPenetration) const { } + + /** + * The target actor to prevent penetration on. Normally, this is almost always the view target, which if + * unimplemented will remain true. However, sometimes the view target, isn't the same as the root actor + * you need to keep in frame. + */ + virtual TOptional GetCameraPreventPenetrationTarget() const + { + return TOptional(); + } + + /** Called if the camera penetrates the focal target. Useful if you want to hide the target actor when being overlapped. */ + virtual void OnCameraPenetratingTarget() { } +}; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode.h new file mode 100644 index 0000000..7b55c00 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode.h @@ -0,0 +1,235 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/World.h" +#include "GameplayTagContainer.h" + +#include "GCMS_CameraMode.generated.h" + +class USpringArmComponent; +class UCameraComponent; +class AActor; +class UCanvas; + +/** + * EGCMS_CameraModeBlendFunction + * + * Blend function used for transitioning between camera modes. + */ +UENUM(BlueprintType) +enum class EGCMS_CameraModeBlendFunction : uint8 +{ + // Does a simple linear interpolation. + Linear, + + // Immediately accelerates, but smoothly decelerates into the target. Ease amount controlled by the exponent. + EaseIn, + + // Smoothly accelerates, but does not decelerate into the target. Ease amount controlled by the exponent. + EaseOut, + + // Smoothly accelerates and decelerates. Ease amount controlled by the exponent. + EaseInOut, + + COUNT UMETA(Hidden) +}; + + +/** + * GCMS_CameraModeView + * + * View data produced by the camera mode that is used to blend camera modes. + */ +USTRUCT(BlueprintType) +struct GENERICCAMERASYSTEM_API FGCMS_CameraModeView +{ +public: + GENERATED_BODY() + + FGCMS_CameraModeView(); + + void Blend(const FGCMS_CameraModeView& Other, float OtherWeight); + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + FVector Location{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + FRotator Rotation{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + FVector SprintArmSocketOffset{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + FVector SprintArmTargetOffset{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + float SprintArmLength{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + FRotator ControlRotation{ForceInit}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS") + float FieldOfView{ForceInit}; +}; + + +/** + * UGCMS_CameraMode + * + * Base class for all camera modes. + */ +UCLASS(Abstract, Blueprintable) +class GENERICCAMERASYSTEM_API UGCMS_CameraMode : public UObject +{ + GENERATED_BODY() + +public: + UGCMS_CameraMode(); + + virtual UWorld* GetWorld() const override; + + /** + * @return Returns the target actor that owning camera is looking at. 返回所属相机当前跟随的目标Actor。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode") + AActor* GetTargetActor() const; + + const FGCMS_CameraModeView& GetCameraModeView() const { return View; } + + // Called when this camera mode is activated on the camera mode stack. + virtual void OnActivation() + { + K2_OnActivation(); + }; + + // Called when this camera mode is deactivated on the camera mode stack. + virtual void OnDeactivation() + { + K2_OnDeactivation(); + }; + + void UpdateCameraMode(float DeltaTime); + + float GetBlendTime() const { return BlendTime; } + float GetBlendWeight() const { return BlendWeight; } + void SetBlendWeight(float Weight); + + FGameplayTag GetCameraTypeTag() const + { + return CameraTypeTag; + } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode") + UCameraComponent* GetAssociatedCamera() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode") + USpringArmComponent* GetAssociatedSprintArm() const; + + virtual void DrawDebug(UCanvas* Canvas) const; + +protected: + /** + * Called when this camera mode activated. + * 在此相机模式激活时调用。 + */ + UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnActivation")) + void K2_OnActivation(); + + /** + * Called when this camera mode deactivated. + * 在此相机模式禁用时调用。 + */ + UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnDeactivation")) + void K2_OnDeactivation(); + + /** + * Return the pivot location of this camera mode. + * @details Default will return Character's capsule location(Taking crouching state in count), or fallback to Pawn's ViewLocation or fallback to PawnLocation. + * 返回此相机模式的轴心位置。 + * @细节,默认实现是返回考虑到Character的蹲伏状态的胶囊体中心点,或者回退到Pawn的ViewLocation,或者回退到Pawn的Location。 + */ + UFUNCTION(BlueprintNativeEvent, Category="CameraMode") + FVector GetPivotLocation() const; + + /** + * Return the pivot rotation of this camera mode. + * @details Default will return TargetActor(Pawn)'s view rotation or fallback to actor rotation. + * 返回此相机模式的轴心旋转。 + * @细节 默认会返回TargetActor(Pawn)的ViewRotation,或者回退到Actor的旋转。 + */ + UFUNCTION(BlueprintNativeEvent, Category="CameraMode") + FRotator GetPivotRotation() const; + + virtual void UpdateView(float DeltaTime); + + virtual void UpdateBlending(float DeltaTime); + + /** + * This is where you update View(CameraModeView) + * @param DeltaTime + * @param PivotLocation Location returned from GetPivotLocation + * @param PivotRotation Rotation returned from GetPivotRotation + */ + UFUNCTION(BlueprintNativeEvent, Category="CameraMode") + void OnUpdateView(float DeltaTime, FVector PivotLocation, FRotator PivotRotation); + virtual void OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation); + +protected: + // A tag that can be queried by gameplay code that cares when a kind of camera mode is active + // without having to ask about a specific mode (e.g., when aiming downsights to get more accuracy) + UPROPERTY(EditDefaultsOnly, Category = "Blending") + FGameplayTag CameraTypeTag; + + // View output produced by the camera mode. + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="View") + FGCMS_CameraModeView View; + + // The horizontal field of view (in degrees). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "5.0", UIMax = "170", ClampMin = "5.0", ClampMax = "170.0")) + float FieldOfView; + + // Minimum view pitch (in degrees). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9")) + float ViewPitchMin; + + // Maximum view pitch (in degrees). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9")) + float ViewPitchMax; + + /** + * How long it takes to blend in this mode. + * 此相机模式的混入时间。 + */ + UPROPERTY(EditDefaultsOnly, Category = "Blending") + float BlendTime; + + /** + * Function used for blending. + * 用于混合的方式。 + */ + UPROPERTY(EditDefaultsOnly, Category = "Blending") + EGCMS_CameraModeBlendFunction BlendFunction; + + // Exponent used by blend functions to control the shape of the curve. + UPROPERTY(EditDefaultsOnly, Category = "Blending") + float BlendExponent; + + // Linear blend alpha used to determine the blend weight. + UPROPERTY(VisibleInstanceOnly, Category="Blending") + float BlendAlpha; + + // Blend weight calculated using the blend alpha and function. + UPROPERTY(VisibleInstanceOnly, Category="Blending") + float BlendWeight; + + UPROPERTY(EditDefaultsOnly, Category = "Duration") + float ActiveTime; + + /** + * The max active time of this camera mode, When active time reach this value, this camera mode will auto popup. + * 此相机模式的最大激活时间,当当前激活时间到达此时长,会返回到默认相机模式。 + */ + UPROPERTY(EditDefaultsOnly, Category = "Duraction") + float MaxActiveTime; + +protected: + /** If true, skips all interpolation and puts camera in ideal location. Automatically set to false next frame. */ + UPROPERTY(transient) + uint32 bResetInterpolation : 1; +}; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraModeStack.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraModeStack.h new file mode 100644 index 0000000..ba15147 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraModeStack.h @@ -0,0 +1,63 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GCMS_CameraMode.h" +#include "UObject/Object.h" +#include "GCMS_CameraModeStack.generated.h" + + +/** + * UGCMS_CameraModeStack + * + * Stack used for blending camera modes. + */ +UCLASS() +class GENERICCAMERASYSTEM_API UGCMS_CameraModeStack : public UObject +{ + GENERATED_BODY() + +public: + UGCMS_CameraModeStack(); + + void ActivateStack(); + void DeactivateStack(); + + bool IsStackActivate() const { return bIsActive; } + + void PushCameraMode(TSubclassOf CameraModeClass); + + void PopCameraMode(TSubclassOf CameraModeClass); + + bool EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView); + + void DrawDebug(UCanvas* Canvas) const; + + // Gets the tag associated with the top layer and the blend weight of it + void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const; + +protected: + /** + * Get or create new camera mode. + */ + UGCMS_CameraMode* GetCameraModeInstance(TSubclassOf CameraModeClass); + + void UpdateStack(float DeltaTime); + void BlendStack(FGCMS_CameraModeView& OutCameraModeView) const; + +protected: + bool bIsActive; + + /** + * Cached camera mode instance pool. + */ + UPROPERTY() + TArray> CameraModeInstances; + + /** + * The list of active camera mode. + */ + UPROPERTY() + TArray> CameraModeStack; +}; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_ThirdPerson.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_ThirdPerson.h new file mode 100644 index 0000000..ca2b81e --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_ThirdPerson.h @@ -0,0 +1,66 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// #pragma once +// +// #include "Curves/CurveFloat.h" +// #include "DrawDebugHelpers.h" +// #include "GCMS_CameraMode_WithPenetrationAvoidance.h" +// #include "GCMS_CameraMode_ThirdPerson.generated.h" +// +// class UCurveVector; +// +// /** +// * UGCMS_CameraMode_ThirdPerson +// * +// * A basic third person camera mode. +// */ +// UCLASS(Abstract, Blueprintable) +// class UGCMS_CameraMode_ThirdPerson : public UGCMS_CameraMode_WithPenetrationAvoidance +// { +// GENERATED_BODY() +// +// public: +// UGCMS_CameraMode_ThirdPerson(); +// +// protected: +// +// virtual void UpdateView_Implementation(float DeltaTime) override; +// +// UFUNCTION(BlueprintCallable, Category="Third Person") +// void UpdateForTarget(float DeltaTime); +// +// protected: +// // Curve that defines local-space offsets from the target using the view pitch to evaluate the curve. +// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "!bUseRuntimeFloatCurves")) +// TObjectPtr TargetOffsetCurve; +// +// // UE-103986: Live editing of RuntimeFloatCurves during PIE does not work (unlike curve assets). +// // Once that is resolved this will become the default and TargetOffsetCurve will be removed. +// UPROPERTY(EditDefaultsOnly, Category = "Third Person") +// bool bUseRuntimeFloatCurves; +// +// // time will be [-ViewPitchMin,ViewPitchMax] +// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves")) +// FRuntimeFloatCurve TargetOffsetX; +// +// // time will be [-ViewPitchMin,ViewPitchMax] +// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves")) +// FRuntimeFloatCurve TargetOffsetY; +// +// // time will be [-ViewPitchMin,ViewPitchMax] +// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves")) +// FRuntimeFloatCurve TargetOffsetZ; +// +// // Alters the speed that a crouch offset is blended in or out +// UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Third Person") +// float CrouchOffsetBlendMultiplier = 5.0f; +// +// protected: +// void SetTargetCrouchOffset(FVector NewTargetOffset); +// void UpdateCrouchOffset(float DeltaTime); +// +// FVector InitialCrouchOffset = FVector::ZeroVector; +// FVector TargetCrouchOffset = FVector::ZeroVector; +// float CrouchOffsetBlendPct = 1.0f; +// FVector CurrentCrouchOffset = FVector::ZeroVector; +// }; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_WithPenetrationAvoidance.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_WithPenetrationAvoidance.h new file mode 100644 index 0000000..ac30900 --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode_WithPenetrationAvoidance.h @@ -0,0 +1,72 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "DrawDebugHelpers.h" +#include "GCMS_CameraMode.h" +#include "GCMS_CameraPenetrationAvoidanceFeeler.h" +#include "GCMS_CameraMode_WithPenetrationAvoidance.generated.h" + + +/** + * + */ +UCLASS(Abstract, Blueprintable) +class GENERICCAMERASYSTEM_API UGCMS_CameraMode_WithPenetrationAvoidance : public UGCMS_CameraMode +{ + GENERATED_BODY() + +public: + UGCMS_CameraMode_WithPenetrationAvoidance(); + + // Penetration prevention + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance") + float PenetrationBlendInTime = 0.1f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance") + float PenetrationBlendOutTime = 0.15f; + + /** If true, does collision checks to keep the camera out of the world. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance") + bool bPreventPenetration = true; + + /** If true, try to detect nearby walls and move the camera in anticipation. Helps prevent popping. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance") + bool bDoPredictiveAvoidance = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance") + float CollisionPushOutDistance = 2.f; + + /** When the camera's distance is pushed into this percentage of its full distance due to penetration */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance") + float ReportPenetrationPercent = 0.f; + + /** + * These are the feeler rays that are used to find where to place the camera. + * Index: 0 : This is the normal feeler we use to prevent collisions. + * Index: 1+ : These feelers are used if you bDoPredictiveAvoidance=true, to scan for potential impacts if the player + * were to rotate towards that direction and primitively collide the camera so that it pulls in before + * impacting the occluder. + */ + UPROPERTY(EditDefaultsOnly, Category = "PenetrationAvoidance") + TArray PenetrationAvoidanceFeelers; + + UPROPERTY(Transient) + float AimLineToDesiredPosBlockedPct; + + UPROPERTY(Transient) + TArray> DebugActorsHitDuringCameraPenetration; + +#if ENABLE_DRAW_DEBUG + mutable float LastDrawDebugTime = -MAX_FLT; +#endif + +protected: + UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance") + void UpdatePreventPenetration(float DeltaTime); + UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance") + void PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, const FVector& SafeLoc, FVector& CameraLoc, float& DistBlockedPct); + + virtual void DrawDebug(UCanvas* Canvas) const override; +}; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraPenetrationAvoidanceFeeler.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraPenetrationAvoidanceFeeler.h new file mode 100644 index 0000000..4996b9c --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraPenetrationAvoidanceFeeler.h @@ -0,0 +1,67 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GCMS_CameraPenetrationAvoidanceFeeler.generated.h" + + +/** + * Struct defining a feeler ray used for camera penetration avoidance. + */ +USTRUCT() +struct GENERICCAMERASYSTEM_API FGCMS_CameraPenetrationAvoidanceFeeler +{ + GENERATED_BODY() + + /** FRotator describing deviance from main ray */ + UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler) + FRotator AdjustmentRot; + + /** how much this feeler affects the final position if it hits the world */ + UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler) + float WorldWeight; + + /** how much this feeler affects the final position if it hits a APawn (setting to 0 will not attempt to collide with pawns at all) */ + UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler) + float PawnWeight; + + /** extent to use for collision when tracing this feeler */ + UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler) + float Extent; + + /** minimum frame interval between traces with this feeler if nothing was hit last frame */ + UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler) + int32 TraceInterval; + + /** number of frames since this feeler was used */ + UPROPERTY(transient) + int32 FramesUntilNextTrace; + + + FGCMS_CameraPenetrationAvoidanceFeeler() + : AdjustmentRot(ForceInit) + , WorldWeight(0) + , PawnWeight(0) + , Extent(0) + , TraceInterval(0) + , FramesUntilNextTrace(0) + { + } + + FGCMS_CameraPenetrationAvoidanceFeeler(const FRotator& InAdjustmentRot, + const float& InWorldWeight, + const float& InPawnWeight, + const float& InExtent, + const int32& InTraceInterval = 0, + const int32& InFramesUntilNextTrace = 0) + : AdjustmentRot(InAdjustmentRot) + , WorldWeight(InWorldWeight) + , PawnWeight(InPawnWeight) + , Extent(InExtent) + , TraceInterval(InTraceInterval) + , FramesUntilNextTrace(InFramesUntilNextTrace) + { + } +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraSystemComponent.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraSystemComponent.h new file mode 100644 index 0000000..45840dd --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraSystemComponent.h @@ -0,0 +1,110 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + + +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" + +#include "GCMS_CameraSystemComponent.generated.h" + +class UCameraComponent; +class USpringArmComponent; +class UCanvas; +class AHUD; +class UGCMS_CameraMode; +class UGCMS_CameraModeStack; +class UObject; +struct FFrame; +struct FGameplayTag; +struct FMinimalViewInfo; +template +class TSubclassOf; + +DECLARE_DELEGATE_RetVal(TSubclassOf, FGMSCameraModeDelegate); + + +/** + * UGCMS_CameraSystemComponent + * + * The base camera component class used by GMS camera system. + */ +UCLASS(ClassGroup=GCMS, meta=(BlueprintSpawnableComponent)) +class GENERICCAMERASYSTEM_API UGCMS_CameraSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer); + + static void OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos); + + // Returns the camera component if one exists on the specified actor. + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCMS|Camera", meta=(DefaultToSelf="Actor")) + static UGCMS_CameraSystemComponent* GetCameraSystemComponent(const AActor* Actor) { return (Actor ? Actor->FindComponentByClass() : nullptr); } + + /** + * Returns the target actor that the camera is looking at. + */ + virtual AActor* GetTargetActor() const { return GetOwner(); } + + // Delegate used to query for the best camera mode. + FGMSCameraModeDelegate DetermineCameraModeDelegate; + + // Add an offset to the field of view. The offset is only for one frame, it gets cleared once it is applied. + void AddFieldOfViewOffset(float FovOffset) { FieldOfViewOffset += FovOffset; } + + // Push specified Camera Mode. + UFUNCTION(BlueprintCallable, Category="GCMS|Camera") + void PushCameraMode(TSubclassOf NewCameraMode); + + UFUNCTION(BlueprintCallable, Category="GCMS|Camera") + void PushDefaultCameraMode(); + + /** + * Initialize it with camera component and sprint arm component. + */ + UFUNCTION(BlueprintCallable, Category="GCMS|Camera") + void Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent); + + virtual void DrawDebug(UCanvas* Canvas) const; + + /** + * Gets the camera mode tag associated with the top layer and the blend weight of it + * 返回顶层相机模式的tag和当前权重。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera") + void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera") + UCameraComponent* GetAssociatedCamera() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera") + USpringArmComponent* GetAssociatedSprintArm() const; + + virtual void Activate(bool bReset) override; + virtual void Deactivate() override; + +protected: + virtual void OnRegister() override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + virtual void UpdateCameraModes(); + +protected: + UPROPERTY() + TObjectPtr AssociatedCameraComponent; + + UPROPERTY() + TObjectPtr AssociatedSprintArmComponent; + + // Stack used to blend the camera modes. + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="GCMS|Camera", meta=(ShowInnerProperties)) + TObjectPtr CameraModeStack; + + // Default camera mode will used. + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCMS|Camera") + TSubclassOf DefaultCameraMode; + + // Offset applied to the field of view. The offset is only for one frame, it gets cleared once it is applied. + float FieldOfViewOffset; +}; diff --git a/Plugins/GGS/Source/GenericCameraSystem/Public/GenericCameraSystem.h b/Plugins/GGS/Source/GenericCameraSystem/Public/GenericCameraSystem.h new file mode 100644 index 0000000..9797d5c --- /dev/null +++ b/Plugins/GGS/Source/GenericCameraSystem/Public/GenericCameraSystem.h @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FGenericCameraSystemModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GGS/Source/GenericEffectsSystem/GenericEffectsSystem.Build.cs b/Plugins/GGS/Source/GenericEffectsSystem/GenericEffectsSystem.Build.cs new file mode 100644 index 0000000..688bbbc --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/GenericEffectsSystem.Build.cs @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericEffectsSystem : ModuleRules +{ + public GenericEffectsSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "PhysicsCore", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "GameplayTags", + "Niagara", + "DeveloperSettings" + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_AnimNotify_ContextEffects.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_AnimNotify_ContextEffects.cpp new file mode 100644 index 0000000..a40bc81 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_AnimNotify_ContextEffects.cpp @@ -0,0 +1,303 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_AnimNotify_ContextEffects.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/World.h" +#include "Kismet/GameplayStatics.h" +#include "NiagaraFunctionLibrary.h" +#include "NiagaraSystem.h" +#include "Feedback/GES_ContextEffectsInterface.h" +#include "Feedback/GES_ContextEffectsLibrary.h" +#include "Feedback/GES_ContextEffectsPreviewSetting.h" +#include "Feedback/GES_ContextEffectsSubsystem.h" +#include "Kismet/KismetMathLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_AnimNotify_ContextEffects) + + +bool UGES_ContextEffectsSpawnParametersProvider::ProvideParameters_Implementation(USkeletalMeshComponent* InMeshComp, const UGES_AnimNotify_ContextEffects* InNotifyNotify, + UAnimSequenceBase* InAnimation, FVector& OutSpawnLocation, + FRotator& OutSpawnRotation) const +{ + return false; +} + +UGES_AnimNotify_ContextEffects::UGES_AnimNotify_ContextEffects(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +#if WITH_EDITORONLY_DATA + NotifyColor = FColor::Blue; +#endif +} + +void UGES_AnimNotify_ContextEffects::PostLoad() +{ + Super::PostLoad(); +} + +#if WITH_EDITOR +void UGES_AnimNotify_ContextEffects::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); +} +#endif + +FString UGES_AnimNotify_ContextEffects::GetNotifyName_Implementation() const +{ + // If the Effect Tag is valid, pass the string name to the notify name + if (Effect.IsValid()) + { + return Effect.ToString(); + } + + return Super::GetNotifyName_Implementation(); +} + +void UGES_AnimNotify_ContextEffects::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, + const FAnimNotifyEventReference& EventReference) +{ + Super::Notify(MeshComp, Animation, EventReference); + + if (!MeshComp) + { + return; + } + + FVector SpawnLocation = FVector::ZeroVector; + FRotator SpawnRotation = FRotator::ZeroRotator; + + bool bValidProvider = !bAttached && IsValid(SpawnParametersProvider) && SpawnParametersProvider->ProvideParameters(MeshComp, this, Animation, SpawnLocation, SpawnRotation); + + if (!bValidProvider && MeshComp->GetOwner()) + { + SpawnRotation = MeshComp->GetOwner()->GetActorTransform().TransformRotation(RotationOffset.Quaternion()).Rotator(); + SpawnLocation = MeshComp->GetOwner()->GetActorTransform().TransformPosition(LocationOffset); + } + // else + // { + // SpawnRotation = UKismetMathLibrary::ComposeRotators(SpawnRotation, RotationOffset); + // SpawnLocation = SpawnLocation + UKismetMathLibrary::Quat_RotateVector(SpawnRotation.Quaternion(), LocationOffset); + // } + + // Make sure both MeshComp and Owning Actor is valid + if (AActor* OwningActor = MeshComp->GetOwner()) + { + // Prepare Trace Data + bool bHitSuccess = false; + FHitResult HitResult; + FCollisionQueryParams QueryParams; + + if (TraceProperties.bIgnoreActor) + { + QueryParams.AddIgnoredActor(OwningActor); + } + + QueryParams.bReturnPhysicalMaterial = true; + + if (bPerformTrace) + { + // If trace is needed, set up Start Location to Attached + FVector TraceStart = bAttached ? MeshComp->GetSocketLocation(SocketName) + LocationOffset : SpawnLocation; + + // Make sure World is valid + if (UWorld* World = OwningActor->GetWorld()) + { + // Call Line Trace, Pass in relevant properties + bHitSuccess = World->LineTraceSingleByChannel(HitResult, TraceStart, (TraceStart + TraceProperties.EndTraceLocationOffset), + TraceProperties.TraceChannel, QueryParams, FCollisionResponseParams::DefaultResponseParam); + } + } + + // Prepare Contexts in advance + FGameplayTagContainer SourceContext; + + FGameplayTagContainer TargetContext; + + // Set up Array of Objects that implement the Context Effects Interface + TArray Implementers; + + // Determine if the Owning Actor is one of the Objects that implements the Context Effects Interface + if (OwningActor->Implements()) + { + // If so, add it to the Array + Implementers.Add(OwningActor); + } + + // Cycle through Owning Actor's Components and determine if any of them is a Component implementing the Context Effect Interface + for (const auto Component : OwningActor->GetComponents()) + { + if (Component) + { + // If the Component implements the Context Effects Interface, add it to the list + if (Component->Implements()) + { + Implementers.Add(Component); + } + } + } + + FGES_SpawnContextEffectsInput Input; + Input.EffectName = Effect; + Input.bAttached = bAttached; + Input.Bone = SocketName; + Input.ComponentToAttach = MeshComp; + Input.Location = bHitSuccess?HitResult.Location:SpawnLocation; + Input.Rotation = SpawnRotation; + Input.LocationOffset = LocationOffset; + Input.RotationOffset = RotationOffset; + Input.AnimationSequence = Animation; + Input.bHitSuccess = bHitSuccess; + Input.HitResult = HitResult; + Input.SourceContext = SourceContext; + Input.TargetContext = TargetContext; + Input.VFXScale = VFXProperties.Scale; + Input.AudioVolume = AudioProperties.VolumeMultiplier; + Input.AudioPitch = AudioProperties.PitchMultiplier; + + // Cycle through all objects implementing the Context Effect Interface + for (UObject* Implementer : Implementers) + { + // If the object is still valid, Execute the AnimMotionEffect Event on it, passing in relevant data + if (Implementer) + { + IGES_ContextEffectsInterface::Execute_PlayContextEffectsWithInput(Implementer, Input); + } + } + +#if WITH_EDITORONLY_DATA + PerformEditorPreview(OwningActor, SourceContext, TargetContext, MeshComp); +#endif + } +} + +#if WITH_EDITOR +void UGES_AnimNotify_ContextEffects::ValidateAssociatedAssets() +{ + Super::ValidateAssociatedAssets(); +} + +void UGES_AnimNotify_ContextEffects::SetParameters(FGameplayTag EffectIn, FVector LocationOffsetIn, FRotator RotationOffsetIn, + FGES_ContextEffectAnimNotifyVFXSettings VFXPropertiesIn, FGES_ContextEffectAnimNotifyAudioSettings AudioPropertiesIn, + bool bAttachedIn, FName SocketNameIn, bool bPerformTraceIn, FGES_ContextEffectAnimNotifyTraceSettings TracePropertiesIn) +{ + Effect = EffectIn; + LocationOffset = LocationOffsetIn; + RotationOffset = RotationOffsetIn; + VFXProperties.Scale = VFXPropertiesIn.Scale; + AudioProperties.PitchMultiplier = AudioPropertiesIn.PitchMultiplier; + AudioProperties.VolumeMultiplier = AudioPropertiesIn.VolumeMultiplier; + bAttached = bAttachedIn; + SocketName = SocketNameIn; + bPerformTrace = bPerformTraceIn; + TraceProperties.EndTraceLocationOffset = TracePropertiesIn.EndTraceLocationOffset; + TraceProperties.TraceChannel = TracePropertiesIn.TraceChannel; + TraceProperties.bIgnoreActor = TracePropertiesIn.bIgnoreActor; +} + +void UGES_AnimNotify_ContextEffects::PerformEditorPreview(AActor* OwningActor, FGameplayTagContainer& SourceContext, FGameplayTagContainer& TargetContext, USkeletalMeshComponent* MeshComp) +{ + if (!bAttached) + { + return; + } + const UGES_ContextEffectsSettings* ContextEffectsSettings = GetDefault(); + if (ContextEffectsSettings == nullptr) + { + return; + } + + // This is for Anim Editor previewing, it is a deconstruction of the calls made by the Interface and the Subsystem + if (!ContextEffectsSettings->bPreviewInEditor || ContextEffectsSettings->PreviewSetting.IsNull()) + return; + + UWorld* World = OwningActor->GetWorld(); + + // Get the world, make sure it's an Editor Preview World + if (!World || World->WorldType != EWorldType::EditorPreview) + return; + + // const auto& PreviewProperties = ContextEffectsSettings->PreviewProperties; + const UGES_ContextEffectsPreviewSetting* PreviewSetting = ContextEffectsSettings->PreviewSetting.LoadSynchronous(); + + if (PreviewSetting == nullptr) + { + return; + } + + // Add Preview contexts if necessary + SourceContext.AppendTags(PreviewSetting->PreviewSourceContext); + TargetContext.AppendTags(PreviewSetting->PreviewTargetContext); + + // Convert given Surface Type to Context and Add it to the Contexts for this Preview + if (PreviewSetting->bPreviewPhysicalSurfaceAsContext) + { + TEnumAsByte PhysicalSurfaceType = PreviewSetting->PreviewPhysicalSurface; + + if (const FGameplayTag* SurfaceContextPtr = ContextEffectsSettings->SurfaceTypeToContextMap.Find(PhysicalSurfaceType)) + { + FGameplayTag SurfaceContext = *SurfaceContextPtr; + + SourceContext.AddTag(SurfaceContext); + } + } + + for (int i = 0; i < PreviewSetting->PreviewContextEffectsLibraries.Num(); ++i) + { + // Libraries are soft referenced, so you will want to try to load them now + if (UObject* EffectsLibrariesObj = PreviewSetting->PreviewContextEffectsLibraries[i].TryLoad()) + { + // Check if it is in fact a UGES_ContextEffectLibrary type + if (UGES_ContextEffectsLibrary* EffectLibrary = Cast(EffectsLibrariesObj)) + { + // Prepare Sounds and Niagara System Arrays + TArray TotalSounds; + TArray TotalNiagaraSystems; + TArray TotalParticleSystems; + + + // Attempt to load the Effect Library content (will cache in Transient data on the Effect Library Asset) + EffectLibrary->LoadEffects(); + + // If the Effect Library is valid and marked as Loaded, Get Effects from it + if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded) + { + // Prepare local arrays + TArray Sounds; + TArray NiagaraSystems; + TArray ParticleSystems; + + + // Get the Effects + EffectLibrary->GetEffects(Effect, SourceContext, TargetContext, Sounds, NiagaraSystems, ParticleSystems); + + // Append to the accumulating arrays + TotalSounds.Append(Sounds); + TotalNiagaraSystems.Append(NiagaraSystems); + TotalParticleSystems.Append(ParticleSystems); + } + + // Cycle through Sounds and call Spawn Sound Attached, passing in relevant data + for (USoundBase* Sound : TotalSounds) + { + UGameplayStatics::SpawnSoundAttached(Sound, MeshComp, SocketName, LocationOffset, RotationOffset, EAttachLocation::KeepRelativeOffset, + false, AudioProperties.VolumeMultiplier, AudioProperties.PitchMultiplier, 0.0f, nullptr, nullptr, true); + } + + // Cycle through Niagara Systems and call Spawn System Attached, passing in relevant data + for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems) + { + UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, MeshComp, SocketName, LocationOffset, + RotationOffset, VFXProperties.Scale, EAttachLocation::KeepRelativeOffset, true, ENCPoolMethod::None, true, true); + } + + // Cycle through Particle Systems and call Spawn System Attached, passing in relevant data + for (UParticleSystem* ParticleSystem : TotalParticleSystems) + { + UGameplayStatics::SpawnEmitterAttached(ParticleSystem, MeshComp, SocketName, LocationOffset, + RotationOffset, VFXProperties.Scale, EAttachLocation::KeepRelativeOffset, true, EPSCPoolMethod::None, true); + } + } + } + } +} +#endif diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectComponent.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectComponent.cpp new file mode 100644 index 0000000..33018b7 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectComponent.cpp @@ -0,0 +1,266 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectComponent.h" + +#include "GameplayTagAssetInterface.h" +#include "Engine/World.h" +#include "Feedback/GES_ContextEffectsSubsystem.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "GES_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectComponent) + +class UAnimSequenceBase; +class USceneComponent; + + +// Sets default values for this component's properties +UGES_ContextEffectComponent::UGES_ContextEffectComponent() +{ + // Disable component tick, enable Auto Activate + PrimaryComponentTick.bCanEverTick = false; + bAutoActivate = true; + // ... +} + + +// Called when the game starts +void UGES_ContextEffectComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... + CurrentContexts.AppendTags(DefaultEffectContexts); + CurrentContextEffectsLibraries = DefaultContextEffectsLibraries; + + // On Begin Play, Load and Add Context Effects pairings + if (const UWorld* World = GetWorld()) + { + if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem()) + { + ContextEffectsSubsystem->LoadAndAddContextEffectsLibraries(GetOwner(), CurrentContextEffectsLibraries); + } + } + if (bAutoSetupTagsProvider) + { + SetupTagsProvider(); + } +} + +void UGES_ContextEffectComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // On End PLay, remove unnecessary context effects pairings + if (const UWorld* World = GetWorld()) + { + if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem()) + { + ContextEffectsSubsystem->UnloadAndRemoveContextEffectsLibraries(GetOwner()); + } + } + + Super::EndPlay(EndPlayReason); +} + +void UGES_ContextEffectComponent::SetupTagsProvider() +{ + if (GetOwner()->GetClass()->ImplementsInterface(UGameplayTagAssetInterface::StaticClass())) + { + SetGameplayTagsProvider(GetOwner()); + } + else + { + TArray Components = GetOwner()->GetComponentsByInterface(UGameplayTagAssetInterface::StaticClass()); + if (Components.IsValidIndex(0)) + { + SetGameplayTagsProvider(Components[0]); + } + } +} + + +void UGES_ContextEffectComponent::PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input) +{ + AggregateContexts(Input); + InjectPhysicalSurfaceToContexts(Input.HitResult, Input.SourceContext); + + // Prep Components + TArray AudioComponentsToAdd; + TArray NiagaraComponentsToAdd; + TArray ParticleComponentsToAdd; + + + // Cycle through Active Audio Components and cache + for (UAudioComponent* ActiveAudioComponent : ActiveAudioComponents) + { + if (ActiveAudioComponent) + { + AudioComponentsToAdd.Add(ActiveAudioComponent); + } + } + + // Cycle through Active Niagara Components and cache + for (UNiagaraComponent* ActiveNiagaraComponent : ActiveNiagaraComponents) + { + if (ActiveNiagaraComponent) + { + NiagaraComponentsToAdd.Add(ActiveNiagaraComponent); + } + } + + // Cycle through Active Particle Components and cache + for (UParticleSystemComponent* ActiveParticleComponent : ActiveParticleComponents) + { + if (ActiveParticleComponent) + { + ParticleComponentsToAdd.Add(ActiveParticleComponent); + } + } + + // Get World + if (const UWorld* World = GetWorld()) + { + // Get Subsystem + if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem()) + { + // Set up components + FGES_SpawnContextEffectsOutput Output; + + // Spawn effects + ContextEffectsSubsystem->SpawnContextEffectsExt(GetOwner(), Input, Output); + + // Append resultant effects + AudioComponentsToAdd.Append(Output.AudioComponents); + NiagaraComponentsToAdd.Append(Output.NiagaraComponents); + ParticleComponentsToAdd.Append(Output.ParticlesComponents); + } + } + + // Append Active Audio Components + ActiveAudioComponents.Empty(); + ActiveAudioComponents.Append(AudioComponentsToAdd); + + // Append Active + ActiveNiagaraComponents.Empty(); + ActiveNiagaraComponents.Append(NiagaraComponentsToAdd); + + ActiveParticleComponents.Empty(); + ActiveParticleComponents.Append(ParticleComponentsToAdd); +} + +void UGES_ContextEffectComponent::AggregateContexts(FGES_SpawnContextEffectsInput& Input) const +{ + if (Input.SourceContextType == EGES_EffectsContextType::Merge) + { + FGameplayTagContainer TotalContexts; + // Aggregate contexts + TotalContexts.AppendTags(Input.SourceContext); + TotalContexts.AppendTags(CurrentContexts); + if (IGameplayTagAssetInterface* TagAssetInterface = Cast(GameplayTagsProvider)) + { + FGameplayTagContainer RetTags; + TagAssetInterface->GetOwnedGameplayTags(RetTags); + TotalContexts.AppendTags(RetTags); + } + Input.SourceContext = TotalContexts; + } +} + + +void UGES_ContextEffectComponent::InjectPhysicalSurfaceToContexts(const FHitResult& InHitResult, FGameplayTagContainer& Contexts) +{ + // Check if converting Physical Surface Type to Context + if (!bConvertPhysicalSurfaceToContext) + { + return; + } + + // Get Phys Mat Type Pointer + TWeakObjectPtr PhysicalSurfaceTypePtr = InHitResult.PhysMaterial; + + // Check if pointer is okay + if (PhysicalSurfaceTypePtr.IsValid()) + { + // Get the Surface Type Pointer + TEnumAsByte PhysicalSurfaceType = PhysicalSurfaceTypePtr->SurfaceType; + + // If Settings are valid + if (const UGES_ContextEffectsSettings* ContextEffectsSettings = GetDefault()) + { + if (ContextEffectsSettings->SurfaceTypeToContextMap.IsEmpty()) + { + GES_CLOG(Warning, "No surface type to context map, Please check ContextEffectsSetting in ProjectSettings!"); + if (FallbackPhysicalSurface.IsValid()) + { + Contexts.AddTag(FallbackPhysicalSurface); + } + } + else + { + // Convert Surface Type to known + if (const FGameplayTag* SurfaceContextPtr = ContextEffectsSettings->SurfaceTypeToContextMap.Find(PhysicalSurfaceType)) + { + FGameplayTag SurfaceContext = *SurfaceContextPtr; + + Contexts.AddTag(SurfaceContext); + } + else + { + GES_CLOG(Warning, "No surface type(%d) to context map found, Please check ContextEffectsSetting in ProjectSettings!", PhysicalSurfaceType.GetValue()); + if (FallbackPhysicalSurface.IsValid()) + { + Contexts.AddTag(FallbackPhysicalSurface); + } + } + } + } + } + else + { + if (FallbackPhysicalSurface.IsValid()) + { + Contexts.AddTag(FallbackPhysicalSurface); + } + } +} + +void UGES_ContextEffectComponent::SetGameplayTagsProvider(UObject* Provider) +{ + if (!IsValid(Provider)) + { + return; + } + if (IGameplayTagAssetInterface* TagAssetInterface = Cast(Provider)) + { + GameplayTagsProvider = Provider; + } + else + { + GES_CLOG(Warning, "Passed in GameplayTagsProvider(%s) Doesn't implement GameplayTagAssetInterface, it can't provide gameplay tags.", *GetNameSafe(Provider->GetClass())); + } +} + +void UGES_ContextEffectComponent::UpdateEffectContexts(FGameplayTagContainer NewEffectContexts) +{ + // Reset and update + CurrentContexts.Reset(NewEffectContexts.Num()); + CurrentContexts.AppendTags(NewEffectContexts); +} + +void UGES_ContextEffectComponent::UpdateLibraries( + TSet> NewContextEffectsLibraries) +{ + // Clear out existing Effects + CurrentContextEffectsLibraries = NewContextEffectsLibraries; + + // Get World + if (const UWorld* World = GetWorld()) + { + // Get Subsystem + if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem()) + { + // Load and Add Libraries to Subsystem + ContextEffectsSubsystem->LoadAndAddContextEffectsLibraries(GetOwner(), CurrentContextEffectsLibraries); + } + } +} diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsEnumLibrary.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsEnumLibrary.cpp new file mode 100644 index 0000000..6a26cb7 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsEnumLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectsEnumLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsEnumLibrary) diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsLibrary.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsLibrary.cpp new file mode 100644 index 0000000..44c1400 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsLibrary.cpp @@ -0,0 +1,157 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectsLibrary.h" +#include "NiagaraSystem.h" +#include "Sound/SoundBase.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsLibrary) + + +void UGES_ContextEffectsLibrary::GetEffects(const FGameplayTag Effect, const FGameplayTagContainer& SourceContext, const FGameplayTagContainer& TargetContext, + TArray& Sounds, TArray& NiagaraSystems, TArray& ParticleSystems) +{ + // Make sure Effect is valid and Library is loaded + if (Effect.IsValid() && SourceContext.IsValid() && EffectsLoadState == EGES_ContextEffectsLibraryLoadState::Loaded) + { + // Loop through Context Effects + for (const auto& ActiveContextEffect : ActiveContextEffects) + { + bool bMatchesEffectTag = Effect.MatchesTagExact(ActiveContextEffect->EffectTag); + bool bMatchesSourceContext = ActiveContextEffect->SourceTagQuery.Matches(SourceContext) && (ActiveContextEffect->SourceTagQuery.IsEmpty() == SourceContext.IsEmpty()); + + // Target context is optional + bool bMatchesTargetContext = ActiveContextEffect->TargetTagQuery.IsEmpty() || ActiveContextEffect->TargetTagQuery.Matches(TargetContext); + + // Make sure the Effect is an exact Tag Match and ensure the Context has all tags in the Effect (and neither or both are empty) + if (bMatchesEffectTag && bMatchesSourceContext && bMatchesTargetContext) + { + // Get all Matching Sounds and Niagara Systems + Sounds.Append(ActiveContextEffect->Sounds); + NiagaraSystems.Append(ActiveContextEffect->NiagaraSystems); + ParticleSystems.Append(ActiveContextEffect->ParticleSystems); + } + } + } +} + +void UGES_ContextEffectsLibrary::LoadEffects() +{ + // Load Effects into Library if not currently loading + if (EffectsLoadState != EGES_ContextEffectsLibraryLoadState::Loading) + { + // Set load state to loading + EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Loading; + + // Clear out any old Active Effects + ActiveContextEffects.Empty(); + + // Call internal loading function + LoadEffectsInternal(); + } +} + +EGES_ContextEffectsLibraryLoadState UGES_ContextEffectsLibrary::GetContextEffectsLibraryLoadState() +{ + // Return current Load State + return EffectsLoadState; +} + +void UGES_ContextEffectsLibrary::LoadEffectsInternal() +{ + // TODO Add Async Loading for Libraries + + // Copy data for async load + TArray LocalContextEffects = ContextEffects; + + // Prepare Active Context Effects Array + TArray ActiveContextEffectsArray; + + // Loop through Context Effects + for (const FGES_ContextEffects& ContextEffect : LocalContextEffects) + { + // Make sure Tags are Valid + if (ContextEffect.EffectTag.IsValid() && !ContextEffect.SourceTagQuery.IsEmpty()) + { + // Create new Active Context Effect + UGES_ActiveContextEffects* NewActiveContextEffects = NewObject(this); + + // Pass relevant tag data + NewActiveContextEffects->EffectTag = ContextEffect.EffectTag; + NewActiveContextEffects->SourceTagQuery = ContextEffect.SourceTagQuery; + NewActiveContextEffects->TargetTagQuery = ContextEffect.TargetTagQuery; + + + // Try to load and add Effects to New Active Context Effects + for (const FSoftObjectPath& Effect : ContextEffect.Effects) + { + if (UObject* Object = Effect.TryLoad()) + { + if (Object->IsA(USoundBase::StaticClass())) + { + if (USoundBase* SoundBase = Cast(Object)) + { + NewActiveContextEffects->Sounds.Add(SoundBase); + } + } + else if (Object->IsA(UNiagaraSystem::StaticClass())) + { + if (UNiagaraSystem* NiagaraSystem = Cast(Object)) + { + NewActiveContextEffects->NiagaraSystems.Add(NiagaraSystem); + } + } + else if (Object->IsA(UParticleSystem::StaticClass())) + { + if (UParticleSystem* ParticleSystem = Cast(Object)) + { + NewActiveContextEffects->ParticleSystems.Add(ParticleSystem); + } + } + } + } + + // Add New Active Context to the Active Context Effects Array + ActiveContextEffectsArray.Add(NewActiveContextEffects); + } + } + + // TODO Call Load Complete after Async Load + // Mark loading complete + this->OnContextEffectLibraryLoadingComplete(ActiveContextEffectsArray); +} + +void UGES_ContextEffectsLibrary::OnContextEffectLibraryLoadingComplete( + TArray InActiveContextEffects) +{ + // Flag data as loaded + EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Loaded; + + // Append incoming Context Effects Array to current list of Active Context Effects + ActiveContextEffects.Append(InActiveContextEffects); +} + +#if WITH_EDITOR +void UGES_ContextEffectsLibrary::PreSave(FObjectPreSaveContext SaveContext) +{ + for (FGES_ContextEffects& ContextEffect : ContextEffects) + { + if (!ContextEffect.Context.IsEmpty()) + { + FGameplayTagQueryExpression Expression; + Expression.AllTagsMatch().AddTags(ContextEffect.Context); + ContextEffect.SourceTagQuery.Build(Expression, FString::Format(TEXT("Has all tags({0})"), {ContextEffect.Context.ToStringSimple()})); + ContextEffect.Context = FGameplayTagContainer(); + } + ContextEffect.EditorFriendlyName = ContextEffect.EffectTag.IsValid() + ? FString::Format(TEXT("Effect({0}) Source({1}) Target({2})"), + { + ContextEffect.EffectTag.GetTagName().ToString(), ContextEffect.SourceTagQuery.GetDescription(), + ContextEffect.TargetTagQuery.GetDescription() + }) + : TEXT("Invalid Effect"); + } + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsPreviewSetting.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsPreviewSetting.cpp new file mode 100644 index 0000000..1e3b846 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsPreviewSetting.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectsPreviewSetting.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsPreviewSetting) diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsStructLibrary.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsStructLibrary.cpp new file mode 100644 index 0000000..5ac605d --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsStructLibrary.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectsStructLibrary.h" +#include "Animation/AnimSequenceBase.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsStructLibrary) diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsSubsystem.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsSubsystem.cpp new file mode 100644 index 0000000..112192b --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/Feedback/GES_ContextEffectsSubsystem.cpp @@ -0,0 +1,289 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Feedback/GES_ContextEffectsSubsystem.h" + +#include "Feedback/GES_ContextEffectsLibrary.h" +#include "Kismet/GameplayStatics.h" +#include "NiagaraFunctionLibrary.h" +#include "NiagaraSystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsSubsystem) + +class AActor; +class UAudioComponent; +class UNiagaraSystem; +class USceneComponent; +class USoundBase; + +void UGES_ContextEffectsSubsystem::SpawnContextEffects(UObject* WorldContextObject, TSoftObjectPtr EffectsLibrary, FGES_SpawnContextEffectsInput Input, + FGES_SpawnContextEffectsOutput& Output) +{ + if (WorldContextObject == nullptr || WorldContextObject->GetWorld() == nullptr) + { + return; + } + + if (EffectsLibrary.IsNull()) + { + return; + } + + // Prepare Arrays for Sounds and Niagara Systems + TArray TotalSounds; + TArray TotalNiagaraSystems; + TArray TotalParticleSystems; + + // Cycle through Effect Libraries + if (UGES_ContextEffectsLibrary* EffectLibrary = EffectsLibrary.LoadSynchronous()) + { + if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Unloaded) + { + // Sync load effects + EffectLibrary->LoadEffects(); + } + + // Check if the Effect Library is valid and data Loaded + if (EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded) + { + // Set up local list of Sounds and Niagara Systems + TArray Sounds; + TArray NiagaraSystems; + TArray ParticleSystems; + + // Get Sounds and Niagara Systems + EffectLibrary->GetEffects(Input.EffectName, Input.SourceContext, Input.TargetContext, Sounds, NiagaraSystems, ParticleSystems); + + // Append to accumulating array + TotalSounds.Append(Sounds); + TotalNiagaraSystems.Append(NiagaraSystems); + TotalParticleSystems.Append(ParticleSystems); + } + } + + // Cycle through found Sounds + for (USoundBase* Sound : TotalSounds) + { + if (Input.bAttached) + { + // Spawn Sounds Attached, add Audio Component to List of ACs + UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAttached(Sound, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, Input.RotationOffset, + EAttachLocation::KeepRelativeOffset, + false, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, nullptr, true); + Output.AudioComponents.Add(AudioComponent); + } + else + { + UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAtLocation(WorldContextObject, Sound, Input.Location, Input.Rotation, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, + nullptr, true); + Output.AudioComponents.Add(AudioComponent); + } + } + + // Cycle through found Niagara Systems + for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems) + { + if (Input.bAttached) + { + // Spawn Niagara Systems Attached, add Niagara Component to List of NCs + UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, + Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true, + ENCPoolMethod::None, + true, + true); + Output.NiagaraComponents.Add(NiagaraComponent); + } + else + { + UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation(WorldContextObject, NiagaraSystem, Input.Location, + Input.Rotation, Input.VFXScale, true, true, + ENCPoolMethod::None, true); + + Output.NiagaraComponents.Add(NiagaraComponent); + } + } + + // Cycle through found Particle Systems + for (UParticleSystem* ParticleSystem : TotalParticleSystems) + { + if (Input.bAttached) + { + // Spawn Particle Systems Attached, add Niagara Component to List of NCs + UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAttached(ParticleSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, + Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true, + EPSCPoolMethod::None, + true); + Output.ParticlesComponents.Add(ParticleComponent); + } + else + { + UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAtLocation(WorldContextObject, ParticleSystem, Input.Location, Input.Rotation, Input.VFXScale, true, + EPSCPoolMethod::None, true); + Output.ParticlesComponents.Add(ParticleComponent); + } + } +} + +void UGES_ContextEffectsSubsystem::SpawnContextEffectsExt(const AActor* SpawningActor, const FGES_SpawnContextEffectsInput& Input, FGES_SpawnContextEffectsOutput& Output) +{ + // First determine if this Actor has a matching Set of Libraries + if (TObjectPtr* EffectsLibrariesSetPtr = ActiveActorEffectsMap.Find(SpawningActor)) + { + // Validate the pointers from the Map Find + if (UGES_ContextEffectsSet* EffectsLibraries = *EffectsLibrariesSetPtr) + { + // Prepare Arrays for Sounds and Niagara Systems + TArray TotalSounds; + TArray TotalNiagaraSystems; + TArray TotalParticleSystems; + + // Cycle through Effect Libraries + for (UGES_ContextEffectsLibrary* EffectLibrary : EffectsLibraries->ContextEffectsLibraries) + { + // Check if the Effect Library is valid and data Loaded + if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded) + { + // Set up local list of Sounds and Niagara Systems + TArray Sounds; + TArray NiagaraSystems; + TArray ParticleSystems; + + // Get Sounds and Niagara Systems + EffectLibrary->GetEffects(Input.EffectName, Input.SourceContext, Input.TargetContext, Sounds, NiagaraSystems, ParticleSystems); + + // Append to accumulating array + TotalSounds.Append(Sounds); + TotalNiagaraSystems.Append(NiagaraSystems); + TotalParticleSystems.Append(ParticleSystems); + } + else if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Unloaded) + { + // Else load effects + EffectLibrary->LoadEffects(); + } + } + + // Cycle through found Sounds + for (USoundBase* Sound : TotalSounds) + { + if (Input.bAttached) + { + // Spawn Sounds Attached, add Audio Component to List of ACs + UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAttached(Sound, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, Input.RotationOffset, + EAttachLocation::KeepRelativeOffset, + false, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, nullptr, true); + Output.AudioComponents.Add(AudioComponent); + } + else + { + UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAtLocation(SpawningActor, Sound, Input.Location, Input.Rotation, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, + nullptr, true); + Output.AudioComponents.Add(AudioComponent); + } + } + + // Cycle through found Niagara Systems + for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems) + { + if (Input.bAttached) + { + // Spawn Niagara Systems Attached, add Niagara Component to List of NCs + UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, + Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true, + ENCPoolMethod::None, + true, + true); + Output.NiagaraComponents.Add(NiagaraComponent); + } + else + { + UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation(SpawningActor, NiagaraSystem, Input.Location, + Input.Rotation, Input.VFXScale, true, true, + ENCPoolMethod::None, true); + + Output.NiagaraComponents.Add(NiagaraComponent); + } + } + + // Cycle through found Particle Systems + for (UParticleSystem* ParticleSystem : TotalParticleSystems) + { + if (Input.bAttached) + { + // Spawn Particle Systems Attached, add Niagara Component to List of NCs + UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAttached(ParticleSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, + Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true, + EPSCPoolMethod::None, + true); + Output.ParticlesComponents.Add(ParticleComponent); + } + else + { + UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAtLocation(SpawningActor, ParticleSystem, Input.Location, Input.Rotation, Input.VFXScale, true, + EPSCPoolMethod::None, true); + Output.ParticlesComponents.Add(ParticleComponent); + } + } + } + } +} + +bool UGES_ContextEffectsSubsystem::GetContextFromSurfaceType( + TEnumAsByte PhysicalSurface, FGameplayTag& Context) +{ + // Get Project Settings + if (const UGES_ContextEffectsSettings* ProjectSettings = GetDefault()) + { + // Find which Gameplay Tag the Surface Type is mapped to + if (const FGameplayTag* GameplayTagPtr = ProjectSettings->SurfaceTypeToContextMap.Find(PhysicalSurface)) + { + Context = *GameplayTagPtr; + } + } + + // Return true if Context is Valid + return Context.IsValid(); +} + +void UGES_ContextEffectsSubsystem::LoadAndAddContextEffectsLibraries(AActor* OwningActor, + TSet> ContextEffectsLibraries) +{ + // Early out if Owning Actor is invalid or if the associated Libraries is 0 (or less) + if (OwningActor == nullptr || ContextEffectsLibraries.Num() <= 0) + { + return; + } + + // Create new Context Effect Set + UGES_ContextEffectsSet* EffectsLibrariesSet = NewObject(this); + + // Cycle through Libraries getting Soft Obj Refs + for (const TSoftObjectPtr& ContextEffectSoftObj : ContextEffectsLibraries) + { + // Load Library Assets from Soft Obj refs + // TODO Support Async Loading of Asset Data + if (UGES_ContextEffectsLibrary* EffectsLibrary = ContextEffectSoftObj.LoadSynchronous()) + { + // Call load on valid Libraries + EffectsLibrary->LoadEffects(); + + // Add new library to Set + EffectsLibrariesSet->ContextEffectsLibraries.Add(EffectsLibrary); + } + } + + // Update Active Actor Effects Map + ActiveActorEffectsMap.Emplace(OwningActor, EffectsLibrariesSet); +} + +void UGES_ContextEffectsSubsystem::UnloadAndRemoveContextEffectsLibraries(AActor* OwningActor) +{ + // Early out if Owning Actor is invalid + if (OwningActor == nullptr) + { + return; + } + + // Remove ref from Active Actor/Effects Set Map + ActiveActorEffectsMap.Remove(OwningActor); +} diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_LogChannels.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_LogChannels.cpp new file mode 100644 index 0000000..9e561bd --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_LogChannels.cpp @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GES_LogChannels.h" +#include "UObject/Object.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" + +DEFINE_LOG_CATEGORY(LogGES) + + +FString GetGESLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + if (AActor* ActorOwner = Cast(Component->GetOuter())) + { + Role = ActorOwner->GetLocalRole(); + Name = ActorOwner->GetName(); + } + else + { + const AActor* Owner = Component->GetOwner(); + Role = IsValid(Owner) ? Owner->GetLocalRole() : ROLE_None; + Name = IsValid(Owner) ? Owner->GetName() : TEXT("None"); + } + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_Tags.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_Tags.cpp new file mode 100644 index 0000000..999e90f --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/GES_Tags.cpp @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GES_Tags.h" + +namespace GMS_MovementModeTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Root, FName{TEXTVIEW("GES")},"Generic Effects System") +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Private/GenericEffectsSystem.cpp b/Plugins/GGS/Source/GenericEffectsSystem/Private/GenericEffectsSystem.cpp new file mode 100644 index 0000000..156b833 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Private/GenericEffectsSystem.cpp @@ -0,0 +1,22 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericEffectsSystem.h" + +#define LOCTEXT_NAMESPACE "FGenericEffectsSystemModule" + +void FGenericEffectsSystemModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +} + +void FGenericEffectsSystemModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericEffectsSystemModule, GenericEffectsSystem) \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_AnimNotify_ContextEffects.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_AnimNotify_ContextEffects.h new file mode 100644 index 0000000..209a763 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_AnimNotify_ContextEffects.h @@ -0,0 +1,195 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Animation/AnimNotifies/AnimNotify.h" +#include "Chaos/ChaosEngineInterface.h" +#include "GameplayTagContainer.h" +#include "GES_ContextEffectsStructLibrary.h" +#include "Engine/EngineTypes.h" +#include "GES_AnimNotify_ContextEffects.generated.h" + +class UGES_AnimNotify_ContextEffects; + +/** + * Base class for customizing spawn behavior of context effects. + * 自定义情景效果生成行为的基类。 + */ +UCLASS(Abstract, Blueprintable, EditInlineNew, DefaultToInstanced, CollapseCategories, Const) +class UGES_ContextEffectsSpawnParametersProvider : public UObject +{ + GENERATED_BODY() + +public: + /** + * Provides spawn parameters for context effects. + * 为情景效果提供生成参数。 + * @param InMeshComp The skeletal mesh component. 骨骼网格组件。 + * @param InNotifyNotify The context effects notify. 情景效果通知。 + * @param InAnimation The animation sequence. 动画序列。 + * @param OutSpawnLocation The spawn location (output). 生成位置(输出)。 + * @param OutSpawnRotation The spawn rotation (output). 生成旋转(输出)。 + * @return True if parameters were provided, false otherwise. 如果提供了参数则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GES|AnimNotify") + bool ProvideParameters(USkeletalMeshComponent* InMeshComp, const UGES_AnimNotify_ContextEffects* InNotifyNotify, UAnimSequenceBase* InAnimation, + FVector& OutSpawnLocation, FRotator& OutSpawnRotation) const; +}; + +/** + * Animation notify for playing context effects. + * 用于播放情景效果的动画通知。 + */ +UCLASS(const, hidecategories = Object, CollapseCategories, Config = Game, meta = (DisplayName = "Play Context Effects")) +class GENERICEFFECTSSYSTEM_API UGES_AnimNotify_ContextEffects : public UAnimNotify +{ + GENERATED_BODY() + +public: + /** + * Constructor for the context effects animation notify. + * 情景效果动画通知构造函数。 + */ + UGES_AnimNotify_ContextEffects(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Called after the object is loaded. + * 对象加载后调用。 + */ + virtual void PostLoad() override; + +#if WITH_EDITOR + /** + * Handles property changes in the editor. + * 处理编辑器中的属性更改。 + * @param PropertyChangedEvent The property change event. 属性更改事件。 + */ + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + /** + * Retrieves the display name for the notify. + * 获取通知的显示名称。 + * @return The notify name. 通知名称。 + */ + virtual FString GetNotifyName_Implementation() const override; + + /** + * Called when the notify is triggered during animation. + * 动画期间通知触发时调用。 + * @param MeshComp The skeletal mesh component. 骨骼网格组件。 + * @param Animation The animation sequence. 动画序列。 + * @param EventReference The animation notify event reference. 动画通知事件引用。 + */ + virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override; + +#if WITH_EDITOR + /** + * Validates associated assets in the editor. + * 在编辑器中验证相关资产。 + */ + virtual void ValidateAssociatedAssets() override; +#endif + +#if WITH_EDITOR + /** + * Sets parameters for the context effects notify in the editor. + * 在编辑器中设置情景效果通知的参数。 + * @param EffectIn The effect tag. 效果标签。 + * @param LocationOffsetIn The location offset. 位置偏移。 + * @param RotationOffsetIn The rotation offset. 旋转偏移。 + * @param VFXPropertiesIn The VFX settings. VFX设置。 + * @param AudioPropertiesIn The audio settings. 音频设置。 + * @param bAttachedIn Whether to attach to mesh. 是否附加到网格。 + * @param SocketNameIn The socket name for attachment. 附加的插槽名称。 + * @param bPerformTraceIn Whether to perform a trace. 是否执行追踪。 + * @param TracePropertiesIn The trace settings. 追踪设置。 + */ + UFUNCTION(BlueprintCallable, Category="GES|AnimNotify") + void SetParameters(FGameplayTag EffectIn, FVector LocationOffsetIn, FRotator RotationOffsetIn, + FGES_ContextEffectAnimNotifyVFXSettings VFXPropertiesIn, FGES_ContextEffectAnimNotifyAudioSettings AudioPropertiesIn, + bool bAttachedIn, FName SocketNameIn, bool bPerformTraceIn, FGES_ContextEffectAnimNotifyTraceSettings TracePropertiesIn); +#endif + + /** + * The effect to play. + * 要播放的效果。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (DisplayName = "Effect", ExposeOnSpawn = true)) + FGameplayTag Effect; + + /** + * Location offset for effect spawning (socket if attached, mesh if not). + * 效果生成的位置偏移(附加时为插槽,否则为网格)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true)) + FVector LocationOffset = FVector::ZeroVector; + + /** + * Rotation offset for effect spawning. + * 效果生成的旋转偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true)) + FRotator RotationOffset = FRotator::ZeroRotator; + + /** + * Visual effects settings for the notify. + * 通知的视觉效果设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true)) + FGES_ContextEffectAnimNotifyVFXSettings VFXProperties; + + /** + * Audio settings for the notify. + * 通知的音频设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true)) + FGES_ContextEffectAnimNotifyAudioSettings AudioProperties; + + /** + * Whether to attach the effect to the mesh component. + * 是否将效果附加到网格组件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AttachmentProperties", meta = (DisplayName="Attach To Mesh", ExposeOnSpawn = true)) + uint32 bAttached : 1; + + /** + * Optional provider for custom spawn location/rotation if not attached. + * 如果未附加,可选的自定义生成位置/旋转提供者。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="AttachmentProperties", meta=(EditCondition="!bAttached")) + TObjectPtr SpawnParametersProvider; + + /** + * Socket name to attach the effect to. + * 附加效果的插槽名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AttachmentProperties", meta=(EditCondition="bAttached")) + FName SocketName{NAME_None}; + + /** + * Whether to perform a trace for surface type conversion. + * 是否执行追踪以进行表面类型转换。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true)) + uint32 bPerformTrace : 1; + + /** + * Trace settings for the notify. + * 通知的追踪设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true, EditCondition = "bPerformTrace")) + FGES_ContextEffectAnimNotifyTraceSettings TraceProperties; + +#if WITH_EDITORONLY_DATA + /** + * Performs a preview of the context effects in the editor. + * 在编辑器中执行情景效果预览。 + * @param InOwningActor The owning actor. 拥有演员。 + * @param InSourceContext The source context tags. 源情景标签。 + * @param InTargetContext The target context tags. 目标情景标签。 + * @param InMeshComp The skeletal mesh component. 骨骼网格组件。 + */ + void PerformEditorPreview(AActor* InOwningActor, FGameplayTagContainer& InSourceContext, FGameplayTagContainer& InTargetContext, USkeletalMeshComponent* InMeshComp); +#endif +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectComponent.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectComponent.h new file mode 100644 index 0000000..3fd22a6 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectComponent.h @@ -0,0 +1,184 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Components/ActorComponent.h" +#include "GES_ContextEffectsInterface.h" +#include "GES_ContextEffectComponent.generated.h" + +namespace EEndPlayReason +{ + enum Type : int; +} + +class UAnimSequenceBase; +class UAudioComponent; +class UGES_ContextEffectsLibrary; +class UNiagaraComponent; +class UObject; +class USceneComponent; +struct FFrame; +struct FHitResult; + +/** + * Component that implements context effects interface for handling effects playback. + * 实现情景效果接口的组件,用于处理效果播放。 + */ +UCLASS(ClassGroup = (GES), hidecategories = (Variable, Tags, ComponentTick, ComponentReplication, Activation, Cooking, AssetUserData, Collision), CollapseCategories, + meta = (BlueprintSpawnableComponent)) +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectComponent : public UActorComponent, public IGES_ContextEffectsInterface +{ + GENERATED_BODY() + +public: + /** + * Constructor for the context effect component. + * 情景效果组件构造函数。 + */ + UGES_ContextEffectComponent(); + +protected: + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending play. 结束播放的原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Sets up the gameplay tags provider. + * 设置游戏标签提供者。 + */ + virtual void SetupTagsProvider(); + +public: + /** + * Plays context effects based on input parameters. + * 根据输入参数播放情景效果。 + * @param Input The context effects input data. 情景效果输入数据。 + */ + virtual void PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input) override; + + /** + * Aggregates context tags for effect playback. + * 为效果播放聚合情景标签。 + * @param Input The context effects input data. 情景效果输入数据。 + */ + void AggregateContexts(FGES_SpawnContextEffectsInput& Input) const; + + /** + * Injects physical surface information into context tags. + * 将物理表面信息注入情景标签。 + * @param InHitResult The hit result. 命中结果。 + * @param Contexts The context tags (output). 情景标签(输出)。 + */ + void InjectPhysicalSurfaceToContexts(const FHitResult& InHitResult, FGameplayTagContainer& Contexts); + + /** + * Sets the gameplay tags provider. + * 设置游戏标签提供者。 + * @param Provider The object providing gameplay tags. 提供游戏标签的对象。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffect") + void SetGameplayTagsProvider(UObject* Provider); + + /** + * Automatically converts physical surface to context tags. + * 自动将物理表面转换为情景标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings") + bool bConvertPhysicalSurfaceToContext = true; + + /** + * Fallback surface type when no mapping or valid physical material exists. + * 当无映射或有效物理材料时使用的备用表面类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings", meta=(Categories="GES.SurfaceType")) + FGameplayTag FallbackPhysicalSurface; + + /** + * Default context tags for effect playback. + * 用于效果播放的默认情景标签。 + */ + UPROPERTY(EditAnywhere, Category="Settings") + FGameplayTagContainer DefaultEffectContexts; + + /** + * Default context effects libraries for this actor. + * 此演员的默认情景效果库。 + */ + UPROPERTY(EditAnywhere, Category="Settings") + TSet> DefaultContextEffectsLibraries; + + /** + * Updates the current effect contexts, overriding default contexts. + * 更新当前效果情景,覆盖默认情景。 + * @param NewEffectContexts The new context tags. 新情景标签。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffect") + void UpdateEffectContexts(FGameplayTagContainer NewEffectContexts); + + /** + * Updates the context effects libraries. + * 更新情景效果库。 + * @param NewContextEffectsLibraries The new effects libraries. 新效果库。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffect") + void UpdateLibraries(TSet> NewContextEffectsLibraries); + +private: + /** + * Current context tags for effect playback. + * 用于效果播放的当前情景标签。 + */ + UPROPERTY(VisibleInstanceOnly, Transient, Category="State") + FGameplayTagContainer CurrentContexts; + + /** + * Automatically sets up the tags provider if enabled. + * 如果启用,自动设置标签提供者。 + */ + UPROPERTY(EditDefaultsOnly, Category="Settings") + bool bAutoSetupTagsProvider{true}; + + /** + * Optional object implementing GameplayTagAssetInterface for tag provision. + * 实现GameplayTagAssetInterface的可选对象,用于提供标签。 + */ + UPROPERTY(VisibleAnywhere, Category="State") + TObjectPtr GameplayTagsProvider{nullptr}; + + /** + * Current context effects libraries in use. + * 当前使用的情景效果库。 + */ + UPROPERTY(Transient) + TSet> CurrentContextEffectsLibraries; + + /** + * Active audio components for effect playback. + * 用于效果播放的活跃音频组件。 + */ + UPROPERTY(Transient) + TArray> ActiveAudioComponents; + + /** + * Active Niagara components for effect playback. + * 用于效果播放的活跃Niagara组件。 + */ + UPROPERTY(Transient) + TArray> ActiveNiagaraComponents; + + /** + * Active particle system components for effect playback. + * 用于效果播放的活跃粒子系统组件。 + */ + UPROPERTY(Transient) + TArray> ActiveParticleComponents; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsEnumLibrary.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsEnumLibrary.h new file mode 100644 index 0000000..8af667e --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsEnumLibrary.h @@ -0,0 +1,53 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GES_ContextEffectsEnumLibrary.generated.h" + +/** + * Enum defining the load state of a context effects library. + * 定义情景效果库加载状态的枚举。 + */ +UENUM() +enum class EGES_ContextEffectsLibraryLoadState : uint8 +{ + /** + * Library is not loaded. + * 库未加载。 + */ + Unloaded = 0, + + /** + * Library is currently loading. + * 库正在加载。 + */ + Loading = 1, + + /** + * Library is fully loaded. + * 库已完全加载。 + */ + Loaded = 2 +}; + +/** + * Enum defining how source context tags are applied. + * 定义如何应用源情景标签的枚举。 + */ +UENUM() +enum class EGES_EffectsContextType : uint8 +{ + /** + * Merge source context with existing contexts. + * 将源情景与现有情景合并。 + */ + Merge, + + /** + * Override existing contexts with source context. + * 使用源情景覆盖现有情景。 + */ + Override +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsInterface.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsInterface.h new file mode 100644 index 0000000..fd515b5 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsInterface.h @@ -0,0 +1,42 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/HitResult.h" +#include "GES_ContextEffectsStructLibrary.h" +#include "UObject/Interface.h" +#include "GES_ContextEffectsInterface.generated.h" + +class UAnimSequenceBase; +class UObject; +class USceneComponent; +struct FFrame; + +/** + * Interface for objects that can respond to context effects. + * 可响应情景效果的对象接口。 + */ +UINTERFACE(Blueprintable) +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Implementation class for context effects interface. + * 情景效果接口的实现类。 + */ +class GENERICEFFECTSSYSTEM_API IGES_ContextEffectsInterface : public IInterface +{ + GENERATED_BODY() + +public: + /** + * Plays context effects based on input parameters. + * 根据输入参数播放情景效果。 + * @param Input The context effects input data. 情景效果输入数据。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GES|ContextEffect") + void PlayContextEffectsWithInput(FGES_SpawnContextEffectsInput Input); + virtual void PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input) = 0; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsLibrary.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsLibrary.h new file mode 100644 index 0000000..8ea0c22 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsLibrary.h @@ -0,0 +1,152 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "GES_ContextEffectsEnumLibrary.h" +#include "UObject/SoftObjectPath.h" +#include "Engine/DataAsset.h" +#include "UObject/WeakObjectPtr.h" +#include "GES_ContextEffectsStructLibrary.h" +#include "GES_ContextEffectsLibrary.generated.h" + +class UNiagaraSystem; +class USoundBase; +struct FFrame; + +/** + * Instance of context effects for runtime use. + * 用于运行时的情景效果实例。 + */ +UCLASS(DisplayName="GES Context Effects Instance") +class GENERICEFFECTSSYSTEM_API UGES_ActiveContextEffects : public UObject +{ + GENERATED_BODY() + +public: + /** + * Tag identifying the effect. + * 标识效果的标签。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + FGameplayTag EffectTag; + + /** + * Query for source tags. + * 源标签查询。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + FGameplayTagQuery SourceTagQuery; + + /** + * Query for target tags. + * 目标标签查询。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + FGameplayTagQuery TargetTagQuery; + + /** + * Array of sound assets for the effect. + * 效果的音效资产数组。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + TArray> Sounds; + + /** + * Array of Niagara systems for the effect. + * 效果的Niagara系统数组。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + TArray> NiagaraSystems; + + /** + * Array of particle systems for the effect. + * 效果的粒子系统数组。 + */ + UPROPERTY(VisibleAnywhere, Category="GES") + TArray> ParticleSystems; +}; + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGES_ContextEffectLibraryLoadingComplete, TArray, ActiveContextEffects); + +/** + * Data asset containing context effects definitions. + * 包含情景效果定义的数据资产。 + */ +UCLASS(BlueprintType) +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsLibrary : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Array of context effects definitions. + * 情景效果定义数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GES", meta = (TitleProperty="EditorFriendlyName")) + TArray ContextEffects; + + /** + * Retrieves effects for a given tag and context. + * 获取指定标签和情景的效果。 + * @param Effect The effect tag. 效果标签。 + * @param SourceContext The source context tags. 源情景标签。 + * @param TargetContext The target context tags. 目标情景标签。 + * @param Sounds The sound assets (output). 音效资产(输出)。 + * @param NiagaraSystems The Niagara systems (output). Niagara系统(输出)。 + * @param ParticleSystems The particle systems (output). 粒子系统(输出)。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffect") + void GetEffects(const FGameplayTag Effect, const FGameplayTagContainer& SourceContext, const FGameplayTagContainer& TargetContext, TArray& Sounds, + TArray& NiagaraSystems, TArray& ParticleSystems); + + /** + * Loads the effects in the library. + * 加载库中的效果。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffect") + void LoadEffects(); + + /** + * Retrieves the load state of the effects library. + * 获取效果库的加载状态。 + * @return The load state. 加载状态。 + */ + EGES_ContextEffectsLibraryLoadState GetContextEffectsLibraryLoadState(); + +private: + /** + * Internal method for loading effects. + * 加载效果的内部方法。 + */ + void LoadEffectsInternal(); + + /** + * Called when effect library loading is complete. + * 效果库加载完成时调用。 + * @param InActiveContextEffects The loaded context effects. 已加载的情景效果。 + */ + void OnContextEffectLibraryLoadingComplete(TArray InActiveContextEffects); + + /** + * Array of active context effects. + * 活跃情景效果数组。 + */ + UPROPERTY(Transient) + TArray> ActiveContextEffects; + + /** + * Current load state of the effects library. + * 效果库的当前加载状态。 + */ + UPROPERTY(Transient) + EGES_ContextEffectsLibraryLoadState EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Unloaded; + +#if WITH_EDITOR + /** + * Pre-save processing for editor. + * 编辑器预保存处理。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsPreviewSetting.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsPreviewSetting.h new file mode 100644 index 0000000..70368d9 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsPreviewSetting.h @@ -0,0 +1,56 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/DataAsset.h" +#include "Chaos/ChaosEngineInterface.h" +#include "UObject/Object.h" +#include "GES_ContextEffectsPreviewSetting.generated.h" + +/** + * Data asset for context effects preview settings. + * 情景效果预览设置的数据资产。 + */ +UCLASS() +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsPreviewSetting : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Whether to use physical surface as context for preview. + * 是否使用物理表面作为预览情景。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview) + bool bPreviewPhysicalSurfaceAsContext = true; + + /** + * Physical surface type for preview. + * 预览的物理表面类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview, meta = (EditCondition = "bPreviewPhysicalSurfaceAsContext")) + TEnumAsByte PreviewPhysicalSurface = EPhysicalSurface::SurfaceType_Default; + + /** + * Context effects libraries for preview. + * 预览的情景效果库。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview, meta = (AllowedClasses = "/Script/GenericEffectsSystem.GES_ContextEffectsLibrary")) + TArray PreviewContextEffectsLibraries; + + /** + * Source context tags for preview. + * 预览的源情景标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview) + FGameplayTagContainer PreviewSourceContext; + + /** + * Target context tags for preview. + * 预览的目标情景标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview) + FGameplayTagContainer PreviewTargetContext; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsStructLibrary.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsStructLibrary.h new file mode 100644 index 0000000..358f5c4 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsStructLibrary.h @@ -0,0 +1,303 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/EngineTypes.h" +#include "Engine/HitResult.h" +#include "GES_ContextEffectsEnumLibrary.h" +#include "UObject/Object.h" +#include "GES_ContextEffectsStructLibrary.generated.h" + +class UNiagaraComponent; +class UAudioComponent; +class UAnimSequenceBase; +class UParticleSystemComponent; + +/** + * Definition of a context effect. + * 情景效果的定义。 + */ +USTRUCT(BlueprintType, DisplayName="GES Context Effects Definition") +struct GENERICEFFECTSSYSTEM_API FGES_ContextEffects +{ + GENERATED_BODY() + + /** + * Tag identifying the effect. + * 标识效果的标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES") + FGameplayTag EffectTag; + + /** + * Query for source tags. + * 源标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES") + FGameplayTagQuery SourceTagQuery; + + /** + * Query for target tags. + * 目标标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES") + FGameplayTagQuery TargetTagQuery; + + /** + * Array of effect assets (sounds, Niagara systems, particle systems). + * 效果资产数组(音效、Niagara系统、粒子系统)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES", meta = (AllowedClasses = "/Script/Engine.SoundBase, /Script/Niagara.NiagaraSystem, /Script/Engine.ParticleSystem")) + TArray Effects; + + /** + * Deprecated context tags. + * 已废弃的情景标签。 + */ + UPROPERTY(VisibleAnywhere, Category="GES", meta=(DisplayName="Context(Deprecated)")) + FGameplayTagContainer Context; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the effect. + * 效果的编辑器友好名称。 + */ + UPROPERTY(EditAnywhere, Category="GES", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Visual effects settings for animation notify. + * 动画通知的视觉效果设置。 + */ +USTRUCT(BlueprintType) +struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyVFXSettings +{ + GENERATED_BODY() + + /** + * Scale for spawning visual effects. + * 生成视觉效果的缩放。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=FX) + FVector Scale = FVector(1.0f, 1.0f, 1.0f); +}; + +/** + * Audio settings for animation notify. + * 动画通知的音频设置。 + */ +USTRUCT(BlueprintType) +struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyAudioSettings +{ + GENERATED_BODY() + + /** + * Volume multiplier for audio effects. + * 音频效果的音量倍增器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Sound) + float VolumeMultiplier = 1.0f; + + /** + * Pitch multiplier for audio effects. + * 音频效果的音高倍增器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Sound) + float PitchMultiplier = 1.0f; +}; + +/** + * Trace settings for animation notify. + * 动画通知的追踪设置。 + */ +USTRUCT(BlueprintType) +struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyTraceSettings +{ + GENERATED_BODY() + + /** + * Trace channel for surface detection. + * 用于表面检测的追踪通道。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace) + TEnumAsByte TraceChannel = ECollisionChannel::ECC_Visibility; + + /** + * Vector offset for the end of the trace. + * 追踪结束位置的向量偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace) + FVector EndTraceLocationOffset = FVector::ZeroVector; + + /** + * Whether to ignore the actor during tracing. + * 追踪时是否忽略演员。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace) + bool bIgnoreActor = true; +}; + +/** + * Input structure for spawning context effects. + * 生成情景效果的输入结构。 + */ +USTRUCT(BlueprintType) +struct GENERICEFFECTSSYSTEM_API FGES_SpawnContextEffectsInput +{ + GENERATED_BODY() + +public: + /** + * Name of the effect to spawn. + * 要生成的效果名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + FGameplayTag EffectName; + + /** + * Whether the effect is attached to a component. + * 效果是否附加到组件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + bool bAttached{true}; + + /** + * Location for spawning if not attached. + * 如果未附加,生成位置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES", meta=(EditCondition="!bAttached", EditConditionHides)) + FVector Location{FVector::ZeroVector}; + + /** + * Rotation for spawning if not attached. + * 如果未附加,生成旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES", meta=(EditCondition="!bAttached", EditConditionHides)) + FRotator Rotation{ForceInit}; + + /** + * Determines how source context is applied (merge or override). + * 确定源情景的应用方式(合并或覆盖)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context") + EGES_EffectsContextType SourceContextType{EGES_EffectsContextType::Merge}; + + /** + * Optional source context tags. + * 可选的源情景标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context") + FGameplayTagContainer SourceContext; + + /** + * Optional target context tags. + * 可选的目标情景标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context") + FGameplayTagContainer TargetContext; + + /** + * Bone name for attachment if attached. + * 如果附加,附加的骨骼名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides)) + FName Bone{NAME_None}; + + /** + * Component to attach the effect to. + * 附加效果的组件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides)) + TObjectPtr ComponentToAttach{nullptr}; + + /** + * Location offset for attached effects. + * 附加效果的位置偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides)) + FVector LocationOffset{FVector::ZeroVector}; + + /** + * Rotation offset for attached effects. + * 附加效果的旋转偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides)) + FRotator RotationOffset{ForceInit}; + + /** + * Optional animation sequence triggering the effect. + * 触发效果的可选动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + TObjectPtr AnimationSequence{nullptr}; + + /** + * Scale for visual effects. + * 视觉效果的缩放。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Vfx") + FVector VFXScale = FVector(1); + + /** + * Volume multiplier for audio effects. + * 音频效果的音量倍增器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Sfx") + float AudioVolume = 1; + + /** + * Pitch multiplier for audio effects. + * 音频效果的音高倍增器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Sfx") + float AudioPitch = 1; + + /** + * Whether the effect was triggered by a successful hit. + * 效果是否由成功命中触发。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Hit") + bool bHitSuccess{false}; + + /** + * Optional hit result for the effect. + * 效果的可选命中结果。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Hit") + FHitResult HitResult; +}; + +/** + * Output structure for spawned context effects. + * 生成情景效果的输出结构。 + */ +USTRUCT(BlueprintType) +struct GENERICEFFECTSSYSTEM_API FGES_SpawnContextEffectsOutput +{ + GENERATED_BODY() + + /** + * Array of spawned audio components. + * 生成的音频组件数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + TArray> AudioComponents; + + /** + * Array of spawned Niagara components. + * 生成的Niagara组件数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + TArray> NiagaraComponents; + + /** + * Array of spawned particle system components. + * 生成的粒子系统组件数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES") + TArray> ParticlesComponents; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsSubsystem.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsSubsystem.h new file mode 100644 index 0000000..8b28671 --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/Feedback/GES_ContextEffectsSubsystem.h @@ -0,0 +1,140 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/DeveloperSettings.h" +#include "GameplayTagContainer.h" +#include "GES_ContextEffectsStructLibrary.h" +#include "Subsystems/WorldSubsystem.h" +#include "GES_ContextEffectsSubsystem.generated.h" + +class UGES_ContextEffectsPreviewSetting; +enum EPhysicalSurface : int; + +class AActor; +class UAudioComponent; +class UGES_ContextEffectsLibrary; +class UNiagaraComponent; +class USceneComponent; +struct FFrame; +struct FGameplayTag; +struct FGameplayTagContainer; + +/** + * Developer settings for context effects system. + * 情景效果系统的开发者设置。 + */ +UCLASS(config = Game, defaultconfig) +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Mapping of physical surface types to context tags. + * 物理表面类型到情景标签的映射。 + */ + UPROPERTY(config, EditAnywhere, Category="GES") + TMap, FGameplayTag> SurfaceTypeToContextMap; + +#if WITH_EDITORONLY_DATA + /** + * Enables preview in the editor. + * 在编辑器中启用预览。 + */ + UPROPERTY(Config, EditAnywhere, Category="PreviewProperties") + uint32 bPreviewInEditor : 1; + + /** + * Preview settings for context effects in the editor. + * 编辑器中情景效果的预览设置。 + */ + UPROPERTY(config, EditAnywhere, Category="PreviewProperties", meta = (EditCondition = "bPreviewInEditor")) + TSoftObjectPtr PreviewSetting; +#endif +}; + +/** + * Set of context effects libraries for an actor. + * 演员的情景效果库集合。 + */ +UCLASS() +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSet : public UObject +{ + GENERATED_BODY() + +public: + /** + * Set of context effects libraries. + * 情景效果库集合。 + */ + UPROPERTY(Transient) + TSet> ContextEffectsLibraries; +}; + +/** + * World subsystem for managing context effects. + * 管理情景效果的世界子系统。 + */ +UCLASS() +class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + /** + * Spawns context effects using a single effects library. + * 使用单个效果库生成情景效果。 + * @param WorldContextObject The world context object. 世界上下文对象。 + * @param EffectsLibrary The effects library to use. 要使用的效果库。 + * @param Input The context effects input data. 情景效果输入数据。 + * @param Output The context effects output data (output). 情景效果输出数据(输出)。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffects", meta=(WorldContext = "WorldContextObject")) + void SpawnContextEffects(UObject* WorldContextObject, TSoftObjectPtr EffectsLibrary, FGES_SpawnContextEffectsInput Input, FGES_SpawnContextEffectsOutput& Output); + + /** + * Spawns context effects for an actor with extended input. + * 为演员生成情景效果,使用扩展输入。 + * @param SpawningActor The actor spawning the effects. 生成效果的演员。 + * @param Input The context effects input data. 情景效果输入数据。 + * @param Output The context effects output data (output). 情景效果输出数据(输出)。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffects") + void SpawnContextEffectsExt(const AActor* SpawningActor, const FGES_SpawnContextEffectsInput& Input, FGES_SpawnContextEffectsOutput& Output); + + /** + * Retrieves the context tag for a given physical surface. + * 获取指定物理表面的情景标签。 + * @param PhysicalSurface The physical surface type. 物理表面类型。 + * @param Context The context tag (output). 情景标签(输出)。 + * @return True if a context tag was found, false otherwise. 如果找到情景标签则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffects") + bool GetContextFromSurfaceType(TEnumAsByte PhysicalSurface, FGameplayTag& Context); + + /** + * Loads and adds context effects libraries for an actor. + * 为演员加载并添加情景效果库。 + * @param OwningActor The actor owning the libraries. 拥有库的演员。 + * @param ContextEffectsLibraries The libraries to load. 要加载的库。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffects") + void LoadAndAddContextEffectsLibraries(AActor* OwningActor, TSet> ContextEffectsLibraries); + + /** + * Unloads and removes context effects libraries for an actor. + * 为演员卸载并移除情景效果库。 + * @param OwningActor The actor owning the libraries. 拥有库的演员。 + */ + UFUNCTION(BlueprintCallable, Category="GES|ContextEffects") + void UnloadAndRemoveContextEffectsLibraries(AActor* OwningActor); + +private: + /** + * Map of actors to their active context effects sets. + * 演员到其活跃情景效果集合的映射。 + */ + UPROPERTY(Transient) + TMap, TObjectPtr> ActiveActorEffectsMap; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_LogChannels.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_LogChannels.h new file mode 100644 index 0000000..e189c0f --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_LogChannels.h @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "CoreMinimal.h" +#include "UObject/Object.h" + +/** + * Gets the context string for logging purposes. + * 获取用于日志记录的上下文字符串。 + * @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。 + * @return The context string. 上下文字符串。 + */ +FString GetGESLogContextString(const UObject* ContextObject = nullptr); + +/** + * Log category for generic effects system messages. + * 通用效果系统消息的日志类别。 + */ +GENERICEFFECTSSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGES, Log, All); + +/** + * Macro for logging effects system messages. + * 用于记录效果系统消息的宏。 + * @details Logs messages with function name and formatted message. 记录包含函数名和格式化消息的日志。 + */ +#define GES_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGES, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging for effects system. + * 用于效果系统的基于上下文的日志记录宏。 + * @details Logs messages with function name, context, and formatted message. 记录包含函数名、上下文和格式化消息的日志。 + */ +#define GES_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGES, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGESLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging with an explicit owner. + * 使用显式拥有者进行基于上下文的日志记录宏。 + * @details Logs messages with function name, owner context, and formatted message. 记录包含函数名、拥有者上下文和格式化消息的日志。 + */ +#define GES_OWNED_CLOG(LogOwner, Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGES, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGESLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_Tags.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_Tags.h new file mode 100644 index 0000000..b29b46d --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/GES_Tags.h @@ -0,0 +1,10 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "NativeGameplayTags.h" + +namespace GES_Tags +{ + GENERICEFFECTSSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Root) +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericEffectsSystem/Public/GenericEffectsSystem.h b/Plugins/GGS/Source/GenericEffectsSystem/Public/GenericEffectsSystem.h new file mode 100644 index 0000000..3b6c71e --- /dev/null +++ b/Plugins/GGS/Source/GenericEffectsSystem/Public/GenericEffectsSystem.h @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FGenericEffectsSystemModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/GenericGameSystem.Build.cs b/Plugins/GGS/Source/GenericGameSystem/GenericGameSystem.Build.cs new file mode 100644 index 0000000..45ba448 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/GenericGameSystem.Build.cs @@ -0,0 +1,39 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericGameSystem : ModuleRules +{ + public GenericGameSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new[] + { + "Core", "SmartObjectsModule" + } + ); + + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", + "ModularGameplay", + "NetCore", + "PhysicsCore", + "Engine", + "Slate", + "SlateCore", + "GameplayTags", + "UMG", + "TargetingSystem", + "GameplayTasks", + "GameplayAbilities", + "GameplayBehaviorsModule", + "SmartObjectsModule", + "GameplayBehaviorSmartObjectsModule" + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/GGS_LogChannels.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/GGS_LogChannels.cpp new file mode 100644 index 0000000..88972c0 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/GGS_LogChannels.cpp @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GGS_LogChannels.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" +#include "GameplayBehavior.h" +#include "GameplayTask.h" +#include "Abilities/GameplayAbility.h" +#include "GameplayTask.h" + +DEFINE_LOG_CATEGORY(LogGGS) + +FString GetGGSLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + Role = Component->GetOwnerRole(); + Name = Component->GetOwner()->GetName(); + } + else if (const UGameplayBehavior* Behavior = Cast(ContextObject)) + { + Role = Behavior->GetAvatar()->GetLocalRole(); + Name = Behavior->GetAvatar()->GetName(); + } + else if (const UGameplayTask* Task = Cast(ContextObject)) + { + Role = Task->GetAvatarActor()->GetLocalRole(); + Name = Task->GetAvatarActor()->GetName(); + } + else if (const UGameplayAbility* Ability = Cast(ContextObject)) + { + Role = Ability->GetAvatarActorFromActorInfo()->GetLocalRole(); + Name = Ability->GetAvatarActorFromActorInfo()->GetName(); + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/GenericGameSystem.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/GenericGameSystem.cpp new file mode 100644 index 0000000..574e600 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/GenericGameSystem.cpp @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +#include "GenericGameSystem.h" + +#define LOCTEXT_NAMESPACE "FGenericGameSystemModule" + +void FGenericGameSystemModule::StartupModule() +{ + +} + +void FGenericGameSystemModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericGameSystemModule, GenericGameSystem) \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Abilities/GGS_GameplayAbility_Interaction.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Abilities/GGS_GameplayAbility_Interaction.cpp new file mode 100644 index 0000000..749cfed --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Abilities/GGS_GameplayAbility_Interaction.cpp @@ -0,0 +1,111 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/Abilities/GGS_GameplayAbility_Interaction.h" +#include "Engine/World.h" +#include "GGS_LogChannels.h" +#include "SmartObjectBlueprintFunctionLibrary.h" +#include "Interaction/GGS_InteractionDefinition.h" +#include "Interaction/GGS_InteractionSystemComponent.h" +#include "Misc/DataValidation.h" + +UGGS_GameplayAbility_Interaction::UGGS_GameplayAbility_Interaction() +{ + InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor; + ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateYes; +} + +void UGGS_GameplayAbility_Interaction::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) +{ + InteractionSystem = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(ActorInfo->AvatarActor.Get()); + if (InteractionSystem == nullptr) + { + EndAbility(Handle, ActorInfo, ActivationInfo, true, true); + return; + } + InteractionSystem->OnInteractableActorChangedEvent.AddDynamic(this, &ThisClass::OnInteractActorChanged); + + Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); +} + +void UGGS_GameplayAbility_Interaction::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + bool bReplicateEndAbility, bool bWasCancelled) +{ + if (UGGS_InteractionSystemComponent* UserComponent = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(ActorInfo->AvatarActor.Get())) + { + UserComponent->OnInteractableActorChangedEvent.RemoveDynamic(this, &ThisClass::OnInteractActorChanged); + } + Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); +} + +bool UGGS_GameplayAbility_Interaction::TryClaimInteraction(int32 Index, FSmartObjectClaimHandle& ClaimedHandle) +{ + USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(GetWorld()); + + check(Subsystem!=nullptr) + const TArray& InteractionInstances = InteractionSystem->GetInteractionOptions(); + if (!InteractionInstances.IsValidIndex(Index)) + { + GGS_CLOG(Error, "Interaction at index(%d) not exist!!", Index) + return false; + } + + if (InteractionInstances[Index].Definition == nullptr) + { + GGS_CLOG(Error, "Interaction at index(%d) has invalid definition!", Index) + return false; + } + + if (InteractionInstances[Index].SlotState != ESmartObjectSlotState::Free) + { + GGS_CLOG(Error, "Interaction(%s) was Claimed/Occupied!", *InteractionInstances[Index].Definition->Text.ToString()) + return false; + } + + const FGGS_InteractionOption& CurrentOption = InteractionInstances[Index]; + + FSmartObjectClaimHandle NewlyClaimedHandle = USmartObjectBlueprintFunctionLibrary::MarkSmartObjectSlotAsClaimed(GetWorld(), CurrentOption.RequestResult.SlotHandle, GetAvatarActorFromActorInfo()); + + // A valid claimed handle can point to an object that is no longer part of the simulation + if (!Subsystem->IsClaimedSmartObjectValid(NewlyClaimedHandle)) + { + GGS_CLOG(Error, "Interaction(%s) refers to an object that is no longer available.!", *InteractionInstances[Index].Definition->Text.ToString()) + return false; + } + + ClaimedHandle = NewlyClaimedHandle; + return true; +} + + +void UGGS_GameplayAbility_Interaction::OnInteractActorChanged_Implementation(AActor* OldActor, AActor* NewActor) +{ +} + +#if WITH_EDITORONLY_DATA +EDataValidationResult UGGS_GameplayAbility_Interaction::IsDataValid(class FDataValidationContext& Context) const +{ + if (ReplicationPolicy != EGameplayAbilityReplicationPolicy::ReplicateYes) + { + Context.AddError(FText::FromString(TEXT("Core Interaction ability must be Replicated to allow client->server communications via RPC."))); + return EDataValidationResult::Invalid; + } + if (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::LocalOnly || NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::ServerOnly) + { + Context.AddError(FText::FromString(TEXT("Core Interaction ability must not be Local/Server only."))); + return EDataValidationResult::Invalid; + } + if (!AbilityTriggers.IsEmpty()) + { + Context.AddError(FText::FromString(TEXT("Core Interaction ability doesn't allow event triggering!"))); + return EDataValidationResult::Invalid; + } + if (InstancingPolicy != EGameplayAbilityInstancingPolicy::InstancedPerActor) + { + Context.AddError(FText::FromString(TEXT("Core Interaction ability's instancing policy must be InstancedPerActor"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.cpp new file mode 100644 index 0000000..35be26d --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.cpp @@ -0,0 +1,21 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h" +#include "Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h" + +UGGS_GameplayBehaviorConfig_InteractionWithAbility::UGGS_GameplayBehaviorConfig_InteractionWithAbility() +{ + BehaviorClass = UGGS_GameplayBehavior_InteractionWithAbility::StaticClass(); +} + +#if WITH_EDITORONLY_DATA +EDataValidationResult UGGS_GameplayBehaviorConfig_InteractionWithAbility::IsDataValid(class FDataValidationContext& Context) const +{ + if (BehaviorClass == nullptr || !BehaviorClass->GetClass()->IsChildOf(UGGS_GameplayBehavior_InteractionWithAbility::StaticClass())) + { + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.cpp new file mode 100644 index 0000000..74ed9fc --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.cpp @@ -0,0 +1,162 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h" + +#include "AbilitySystemComponent.h" +#include "AbilitySystemGlobals.h" +#include "GGS_LogChannels.h" +#include "Abilities/GameplayAbility.h" +#include "Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h" +#include "Interaction/GGS_InteractionSystemComponent.h" + +bool UGGS_GameplayBehavior_InteractionWithAbility::Trigger(AActor& InAvatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner) +{ + bTransientIsTriggering = true; + bTransientIsActive = false; + TransientAvatar = &InAvatar; + TransientSmartObjectOwner = SmartObjectOwner; + + UGGS_InteractionSystemComponent* InteractionSystem = InAvatar.FindComponentByClass(); + + if (!InteractionSystem) + { + GGS_CLOG(Error, "Missing InteractionSystem Component!") + return false; + } + + UAbilitySystemComponent* Asc = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(&InAvatar); + if (!Asc) + { + GGS_CLOG(Error, "Missing Ability System Component!") + return false; + } + + TSubclassOf AbilityClass{nullptr}; + int32 AbilityLevel = 0; + if (!CheckValidAbilitySetting(Config, AbilityClass, AbilityLevel)) + { + return false; + } + + if (FGameplayAbilitySpec* Handle = Asc->FindAbilitySpecFromClass(AbilityClass)) + { + GGS_CLOG(Error, "Try granting repeated interaction ability of class:%s, which is not supported!", *AbilityClass->GetName()) + return false; + } + + GrantedAbilityClass = AbilityClass; + + AbilityEndedDelegateHandle = Asc->OnAbilityEnded.AddUObject(this, &ThisClass::OnAbilityEndedCallback); + + //Ability trigger by event when activation polciy=ServerInitied won't work. + AbilitySpecHandle = Asc->K2_GiveAbilityAndActivateOnce(AbilityClass, AbilityLevel); + + if (!AbilitySpecHandle.IsValid()) + { + GGS_CLOG(Error, "Can't active ability of class:%s! Check ability settings!", *AbilityClass->GetName()) + return false; + } + + // Special case: behavior already interrupted + if (bBehaviorWasInterrupted && AbilitySpecHandle.IsValid() && !bAbilityEnded) + { + Asc->ClearAbility(AbilitySpecHandle); + return false; + } + + if (AbilitySpecHandle.IsValid() && bAbilityEnded) + { + GGS_CLOG(Verbose, "Instantly executed interaction ability:%s,handle%s", *AbilityClass->GetName(), *AbilitySpecHandle.ToString()) + EndBehavior(InAvatar, false); + return false; + } + + GGS_CLOG(Verbose, "Granted and activate interaction ability:%s,handle%s", *AbilityClass->GetName(), *AbilitySpecHandle.ToString()) + + //TODO what happens when avatar or target get destoryied? + // SOOwner销毁的时候, 需要Abort当前行为, 目的是清除赋予的Ability + // SmartObjectOwner->OnDestroyed.AddDynamic(this, &ThisClass::OnSmartObjectOwnerDestroyed); + GGS_CLOG(Verbose, "Interaction begins with ability:%s", *AbilityClass->GetName()) + + bTransientIsTriggering = false; + bTransientIsActive = true; + return bTransientIsActive; +} + +void UGGS_GameplayBehavior_InteractionWithAbility::EndBehavior(AActor& Avatar, const bool bInterrupted) +{ + GGS_CLOG(Verbose, "Interaction behavior ended %s", bInterrupted?TEXT("due to interruption!"):TEXT("normally!")) + + // clear ability stuff. + if (UAbilitySystemComponent* Asc = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(&Avatar)) + { + if (AbilityEndedDelegateHandle.IsValid()) + { + Asc->OnAbilityEnded.Remove(AbilityEndedDelegateHandle); + AbilityEndedDelegateHandle.Reset(); + } + + // Special case: behavior interrupting active ability, so cancel ability. + if (bInterrupted && bTransientIsActive && !bAbilityEnded && AbilitySpecHandle.IsValid()) + { + if (const FGameplayAbilitySpec* Spec = Asc->FindAbilitySpecFromHandle(AbilitySpecHandle)) + { + GGS_CLOG(Verbose, "Cancel ability(%s) because behavior was interrupted.", *Spec->Ability.GetClass()->GetName()) + Asc->CancelAbilityHandle(AbilitySpecHandle); + } + } + + if (bInterrupted && !bTransientIsActive && AbilitySpecHandle.IsValid()) + { + Asc->ClearAbility(AbilitySpecHandle); + } + } + + Super::EndBehavior(Avatar, bInterrupted); + + bBehaviorWasInterrupted = bInterrupted; +} + +bool UGGS_GameplayBehavior_InteractionWithAbility::CheckValidAbilitySetting(const UGameplayBehaviorConfig* Config, TSubclassOf& OutAbilityClass, int32& OutAbilityLevel) +{ + // Ability class validation. + const UGGS_GameplayBehaviorConfig_InteractionWithAbility* InteractionConfig = Cast(Config); + if (!InteractionConfig) + { + GGS_CLOG(Error, "Invalid GameplayBehaviorConfig! expect Config be type of %s.", *UGGS_GameplayBehaviorConfig_InteractionWithAbility::StaticClass()->GetName()) + return false; + } + + const TSubclassOf AbilityClass = InteractionConfig->AbilityToGrant.LoadSynchronous(); + if (!AbilityClass) + { + GGS_CLOG(Error, "Invalid AbilityToGrant Class!") + return false; + } + OutAbilityClass = AbilityClass; + OutAbilityLevel = InteractionConfig->AbilityLevel; + return true; +} + +void UGGS_GameplayBehavior_InteractionWithAbility::OnAbilityEndedCallback(const FAbilityEndedData& EndedData) +{ + if (bAbilityEnded) + { + return; + } + // check for ability granted by this behavior. + if (EndedData.AbilitySpecHandle == AbilitySpecHandle || EndedData.AbilityThatEnded->GetClass() == GrantedAbilityClass) + { + bAbilityEnded = true; + bAbilityWasCancelled = EndedData.bWasCancelled; + + // Special case: behavior already active and abilities ended, ending behavior normally. + if (!bTransientIsTriggering && bTransientIsActive) + { + GGS_CLOG(Verbose, "Interaction ability(%s) %s.", *EndedData.AbilityThatEnded.GetClass()->GetName(), + EndedData.bWasCancelled?TEXT("was canceled"):TEXT("ended normally")) + EndBehavior(*GetAvatar(), false); + } + } +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractableInterface.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractableInterface.cpp new file mode 100644 index 0000000..98a02a1 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractableInterface.cpp @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/GGS_InteractableInterface.h" + + +// Add default functionality here for any IGGS_InteractableInterface functions that are not pure virtual. + +FText IGGS_InteractableInterface::GetInteractionDisplayNameText_Implementation() const +{ + if (UObject* Object = _getUObject()) + { + return FText::FromString(GetNameSafe(Object)); + } + return FText::GetEmpty(); +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionDefinition.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionDefinition.cpp new file mode 100644 index 0000000..2aa087f --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionDefinition.cpp @@ -0,0 +1,5 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/GGS_InteractionDefinition.h" + diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionStructLibrary.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionStructLibrary.cpp new file mode 100644 index 0000000..48dbefa --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionStructLibrary.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/GGS_InteractionStructLibrary.h" +#include "Interaction/GGS_InteractionDefinition.h" + +FString FGGS_InteractionOption::ToString() const +{ + return FString::Format(TEXT("{0} {1} {2}"), { + Definition ? Definition->Text.ToString() : TEXT("Null Definition"), SlotState == ESmartObjectSlotState::Free ? TEXT("Valid") : TEXT("Invalid"), SlotIndex + }); +} + +bool operator==(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS) +{ + return Lhs.Definition == RHS.Definition + && Lhs.RequestResult == RHS.RequestResult + && Lhs.SlotIndex == RHS.SlotIndex + && Lhs.SlotState == RHS.SlotState; +} + +bool operator!=(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS) +{ + return !(Lhs == RHS); +} + +bool operator<(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS) +{ + return Lhs.SlotIndex < RHS.SlotIndex; +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionSystemComponent.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionSystemComponent.cpp new file mode 100644 index 0000000..e9fe07e --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_InteractionSystemComponent.cpp @@ -0,0 +1,399 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/GGS_InteractionSystemComponent.h" +#include "Engine/World.h" +#include "GameplayBehaviorSmartObjectBehaviorDefinition.h" +#include "GGS_LogChannels.h" +#include "SmartObjectComponent.h" +#include "SmartObjectSubsystem.h" +#include "Interaction/GGS_InteractableInterface.h" +#include "Interaction/GGS_SmartObjectFunctionLibrary.h" +#include "Interaction/GGS_InteractionStructLibrary.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" + +// Sets default values for this component's properties +UGGS_InteractionSystemComponent::UGGS_InteractionSystemComponent() +{ + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); +} + +void UGGS_InteractionSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Params; + Params.bIsPushBased = true; + Params.Condition = COND_OwnerOnly; + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractableActor, Params); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, NumsOfInteractableActors, Params); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractingOption, Params); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractionOptions, Params); +} + +UGGS_InteractionSystemComponent* UGGS_InteractionSystemComponent::GetInteractionSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +void UGGS_InteractionSystemComponent::CycleInteractableActors_Implementation(bool bNext) +{ + if (bInteracting || InteractableActors.Num() <= 1) + { + return; + } + + int32 Index = InteractableActor != nullptr ? InteractableActors.IndexOfByKey(InteractableActor) : 0; + if (!InteractableActors.IsValidIndex(Index)) //一个都没 + { + return; + } + if (bNext) + { + Index = FMath::Clamp(Index + 1, 0, InteractableActors.Num()); + } + else + { + Index = FMath::Clamp(Index - 1, 0, InteractableActors.Num()); + } + if (InteractableActors.IsValidIndex(Index) && InteractableActors[Index] != nullptr && InteractableActors[Index] != + InteractableActor) + { + SetInteractableActor(InteractableActors[Index]); + } +} + +void UGGS_InteractionSystemComponent::SearchInteractableActors() +{ + OnSearchInteractableActorsEvent.Broadcast(); +} + +void UGGS_InteractionSystemComponent::SetInteractableActors(TArray NewActors) +{ + if (!GetOwner()->HasAuthority()) + { + return; + } + + InteractableActors = NewActors; + SetInteractableActorsNum(InteractableActors.Num()); + OnInteractableActorsChanged(); +} + +void UGGS_InteractionSystemComponent::SetInteractableActorsNum(int32 NewNum) +{ + if (NewNum != NumsOfInteractableActors) + { + const int32 PrevNum = NumsOfInteractableActors; + NumsOfInteractableActors = NewNum; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, NumsOfInteractableActors, this) + OnInteractableActorsNumChanged(); + } +} + +void UGGS_InteractionSystemComponent::SetInteractableActor(AActor* InActor) +{ + if (InActor != InteractableActor) + { + AActor* OldActor = InteractableActor; + InteractableActor = InActor; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractableActor, this) + OnInteractableActorChanged(OldActor); + } +} + +FSmartObjectRequestFilter UGGS_InteractionSystemComponent::GetSmartObjectRequestFilter_Implementation() +{ + return DefaultRequestFilter; +} + +void UGGS_InteractionSystemComponent::StartInteraction(int32 NewIndex) +{ + if (bInteracting) + { + GGS_CLOG(Warning, "Can't start interaction(%d) while already interacting(%d)", NewIndex, InteractingOption) + return; + } + + if (!InteractionOptions.IsValidIndex(NewIndex)) + { + GGS_CLOG(Warning, "Try start invalid interaction(%d)", NewIndex) + return; + } + + int32 Prev = InteractingOption; + InteractingOption = NewIndex; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this); + OnInteractingOptionChanged(Prev); +} + +void UGGS_InteractionSystemComponent::EndInteraction() +{ + if (!bInteracting) + { + //GGS_CLOG(Warning, TEXT("no need to end interaction when there's no any active interaction.")) + return; + } + + int32 Prev = InteractingOption; + InteractingOption = INDEX_NONE; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this); + OnInteractingOptionChanged(Prev); +} + +void UGGS_InteractionSystemComponent::InstantInteraction(int32 NewIndex) +{ + if (bInteracting) + { + GGS_CLOG(Warning, "Can't trigger instant interaction(%d) while already interacting(%d)", NewIndex, InteractingOption) + return; + } + if (!InteractionOptions.IsValidIndex(NewIndex)) + { + GGS_CLOG(Warning, "Try trigger invalid interaction(%d)", NewIndex) + return; + } + + int32 Prev = InteractingOption; + InteractingOption = NewIndex; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this); + OnInteractingOptionChanged(Prev); + + int32 Prev2 = InteractingOption; + InteractingOption = INDEX_NONE; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this); + OnInteractingOptionChanged(Prev2); +} + +bool UGGS_InteractionSystemComponent::IsInteracting() const +{ + return bInteracting; +} + +int32 UGGS_InteractionSystemComponent::GetInteractingOption() const +{ + return InteractingOption; +} + +void UGGS_InteractionSystemComponent::OnInteractableActorChanged(AActor* OldActor) +{ + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + RefreshOptionsForActor(); + } + + if (IsValid(OldActor) && OldActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass())) + { + IGGS_InteractableInterface::Execute_OnInteractionDeselected(OldActor, GetOwner()); + } + + if (IsValid(InteractableActor) && InteractableActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass())) + { + IGGS_InteractableInterface::Execute_OnInteractionSelected(InteractableActor, GetOwner()); + } + + OnInteractableActorChangedEvent.Broadcast(OldActor, InteractableActor); +} + +void UGGS_InteractionSystemComponent::OnInteractableActorsNumChanged() +{ + OnInteractableActorNumChangedEvent.Broadcast(NumsOfInteractableActors); +} + +void UGGS_InteractionSystemComponent::OnInteractableActorsChanged_Implementation() +{ + if (!bInteracting) + { + // update potential actor. + if (!IsValid(InteractableActor) || !InteractableActors.Contains(InteractableActor)) + { + if (InteractableActors.IsValidIndex(0) && IsValid(InteractableActors[0])) + { + SetInteractableActor(InteractableActors[0]); + } + else + { + SetInteractableActor(nullptr); + } + } + + if (bNewActorHasPriority) + { + if (IsValid(InteractableActor) && InteractableActors.IsValidIndex(0) && InteractableActors[0] != InteractableActor) + { + SetInteractableActor(InteractableActors[0]); + } + } + } +} + +void UGGS_InteractionSystemComponent::OnSmartObjectEventCallback(const FSmartObjectEventData& EventData) +{ + check(InteractableActor != nullptr); + + for (int32 i = 0; i < InteractionOptions.Num(); i++) + { + const FGGS_InteractionOption& Option = InteractionOptions[i]; + if (EventData.SmartObjectHandle == Option.RequestResult.SmartObjectHandle && EventData.SlotHandle == Option.RequestResult.SlotHandle) + { + if (EventData.Reason == ESmartObjectChangeReason::OnOccupied || EventData.Reason == ESmartObjectChangeReason::OnReleased || EventData.Reason == ESmartObjectChangeReason::OnClaimed) + { + RefreshOptionsForActor(); + } + } + } +} + +void UGGS_InteractionSystemComponent::OnInteractionOptionsChanged() +{ + for (FGGS_InteractionOption& InteractionOption : InteractionOptions) + { + GGS_CLOG(Verbose, "Available Options:%s", *InteractionOption.ToString()) + } + OnInteractionOptionsChangedEvent.Broadcast(); +} + +void UGGS_InteractionSystemComponent::OnInteractingOptionChanged_Implementation(int32 PrevOptionIndex) +{ + bool bPrevInteracting = bInteracting; + bInteracting = InteractingOption != INDEX_NONE; + + if (IsValid(InteractableActor) && InteractableActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass())) + { + if (!bPrevInteracting && bInteracting) + { + IGGS_InteractableInterface::Execute_OnInteractionStarted(InteractableActor, GetOwner(), InteractingOption); + } + if (bPrevInteracting && !bInteracting) + { + IGGS_InteractableInterface::Execute_OnInteractionEnded(InteractableActor, GetOwner(), PrevOptionIndex); + } + } + + OnInteractingStateChangedEvent.Broadcast(bInteracting); +} + +void UGGS_InteractionSystemComponent::RefreshOptionsForActor() +{ + USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(GetWorld()); + + if (!Subsystem) + { + return; + } + + // getting new options for current interact actor. + TArray NewOptions; + { + TArray Results; + if (IsValid(InteractableActor) && UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(GetSmartObjectRequestFilter(), InteractableActor, Results, GetOwner())) + { + for (int32 i = 0; i < Results.Num(); i++) + { + FGGS_InteractionOption Option; + UGGS_InteractionDefinition* FoundDefinition; + if (UGGS_SmartObjectFunctionLibrary::FindInteractionDefinitionFromSmartObjectSlot(this, Results[i].SlotHandle, FoundDefinition)) + { + Option.Definition = FoundDefinition; + Option.SlotState = Subsystem->GetSlotState(Results[i].SlotHandle); + Option.RequestResult = Results[i]; + Option.SlotIndex = i; + Option.BehaviorDefinition = Subsystem->GetBehaviorDefinitionByRequestResult(Results[i], USmartObjectBehaviorDefinition::StaticClass()); + NewOptions.Add(Option); + } + } + } + } + + // check any options changed. + bool bOptionsChanged = false; + { + if (NewOptions.Num() == InteractionOptions.Num()) + { + NewOptions.Sort(); + + for (int OptionIndex = 0; OptionIndex < NewOptions.Num(); OptionIndex++) + { + const FGGS_InteractionOption& NewOption = NewOptions[OptionIndex]; + const FGGS_InteractionOption& CurrentOption = InteractionOptions[OptionIndex]; + + if (NewOption != CurrentOption) + { + bOptionsChanged = true; + break; + } + } + } + else + { + bOptionsChanged = true; + } + } + + if (bOptionsChanged) + { + // unregister event callbacks for existing options. + for (int32 i = 0; i < InteractionOptions.Num(); i++) + { + auto& Handle = InteractionOptions[i].RequestResult.SlotHandle; + if (SlotCallbacks.Contains(Handle)) + { + if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Handle)) + { + OnEventDelegate->Remove(SlotCallbacks[Handle]); + SlotCallbacks.Remove(Handle); + } + } + } + + for (FGGS_InteractionOption& Option : InteractionOptions) + { + if (SlotCallbacks.Contains(Option.RequestResult.SlotHandle)) + { + if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle)) + { + OnEventDelegate->Remove(SlotCallbacks[Option.RequestResult.SlotHandle]); + SlotCallbacks.Remove(Option.RequestResult.SlotHandle); + } + } + // if (Option.DelegateHandle.IsValid()) + // { + // if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle)) + // { + // OnEventDelegate->Remove(Option.DelegateHandle); + // Option.DelegateHandle.Reset(); + // } + // } + } + + InteractionOptions = NewOptions; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractionOptions, this) + + GGS_CLOG(Verbose, "Interaction options changed, nums of options:%d", InteractionOptions.Num()) + + // register slot event callbacks. + // for (int32 i = 0; i < InteractionOptions.Num(); i++) + // { + // FGGS_InteractionOption& Option = InteractionOptions[i]; + // if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle)) + // { + // Option.DelegateHandle = OnEventDelegate->AddUObject(this, &ThisClass::OnSmartObjectEventCallback); + // } + // } + + for (int32 i = 0; i < InteractionOptions.Num(); i++) + { + auto& Handle = InteractionOptions[i].RequestResult.SlotHandle; + if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Handle)) + { + FDelegateHandle DelegateHandle = OnEventDelegate->AddUObject(this, &ThisClass::OnSmartObjectEventCallback); + SlotCallbacks.Emplace(Handle, DelegateHandle); + } + } + + OnInteractionOptionsChanged(); + } +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_SmartObjectFunctionLibrary.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_SmartObjectFunctionLibrary.cpp new file mode 100644 index 0000000..d812fb2 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/GGS_SmartObjectFunctionLibrary.cpp @@ -0,0 +1,85 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/GGS_SmartObjectFunctionLibrary.h" +#include "GameplayBehaviorSmartObjectBehaviorDefinition.h" +#include "GameplayBehaviorConfig.h" +#include "SmartObjectBlueprintFunctionLibrary.h" +#include "SmartObjectDefinition.h" +#include "Engine/World.h" +#include "SmartObjectSubsystem.h" +#include "Interaction/GGS_InteractionDefinition.h" + +UGameplayBehaviorConfig* UGGS_SmartObjectFunctionLibrary::GetGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition) +{ + if (const UGameplayBehaviorSmartObjectBehaviorDefinition* Definition = Cast(BehaviorDefinition)) + { + return Definition->GameplayBehaviorConfig; + } + + return nullptr; +} + +bool UGGS_SmartObjectFunctionLibrary::FindGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition, TSubclassOf DesiredClass, + UGameplayBehaviorConfig*& OutConfig) +{ + if (UClass* RealClass = DesiredClass) + { + if (UGameplayBehaviorConfig* Config = GetGameplayBehaviorConfig(BehaviorDefinition)) + { + if (Config->GetClass()->IsChildOf(RealClass)) + { + OutConfig = Config; + return true; + } + } + } + return false; +} + +bool UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(const FSmartObjectRequestFilter& Filter, AActor* SearchActor, TArray& OutResults, + const AActor* UserActor) +{ + if (!IsValid(SearchActor)) + { + return false; + } + TArray Results; + USmartObjectBlueprintFunctionLibrary::FindSmartObjectsInActor(Filter, SearchActor, Results, UserActor); + if (Results.IsEmpty()) + { + return false; + } + + // filter results which has definiton entry. + for (int32 i = 0; i < Results.Num(); i++) + { + UGGS_InteractionDefinition* FoundDefinition; + if (FindInteractionDefinitionFromSmartObjectSlot(SearchActor, Results[i].SlotHandle, FoundDefinition)) + { + OutResults.Add(Results[i]); + } + } + return !OutResults.IsEmpty(); +} + +bool UGGS_SmartObjectFunctionLibrary::FindInteractionDefinitionFromSmartObjectSlot(UObject* WorldContext, FSmartObjectSlotHandle SmartObjectSlotHandle, UGGS_InteractionDefinition*& OutDefinition) +{ + if (WorldContext && WorldContext->GetWorld() && SmartObjectSlotHandle.IsValid()) + { + if (USmartObjectSubsystem* Subsystem = WorldContext->GetWorld()->GetSubsystem()) + { + Subsystem->ReadSlotData(SmartObjectSlotHandle, [ &OutDefinition](FConstSmartObjectSlotView SlotView) + { + if (const FGGS_SmartObjectInteractionEntranceData* Entry = SlotView.GetDefinitionDataPtr()) + { + if (!Entry->DefinitionDA.IsNull()) + { + OutDefinition = Entry->DefinitionDA.LoadSynchronous(); + } + } + }); + } + } + return OutDefinition != nullptr; +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.cpp new file mode 100644 index 0000000..9ef51f2 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.cpp @@ -0,0 +1,23 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.h" +#include "Interaction/GGS_InteractionSystemComponent.h" +#include "Interaction/GGS_SmartObjectFunctionLibrary.h" + +bool UGGS_TargetingFilterTask_InteractionSmartObjects::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const +{ + if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle)) + { + if (AActor* Actor = TargetData.HitResult.GetActor()) + { + if (UGGS_InteractionSystemComponent* InteractionSys = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(SourceContext->SourceActor)) + { + TArray Results; + + return !UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(InteractionSys->GetSmartObjectRequestFilter(), Actor, Results, InteractionSys->GetOwner()); + } + } + } + return true; +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.cpp new file mode 100644 index 0000000..78e09ee --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.cpp @@ -0,0 +1,161 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.h" +#include "Engine/World.h" +#include "GameFramework/Pawn.h" +#include "GameplayBehavior.h" +#include "GameplayBehaviorConfig.h" +#include "GameplayBehaviorSmartObjectBehaviorDefinition.h" +#include "GameplayBehaviorSubsystem.h" +#include "GGS_LogChannels.h" +#include "SmartObjectComponent.h" +#include "SmartObjectSubsystem.h" + +UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ + bBehaviorFinished = false; +} + +UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::UseSmartObjectWithGameplayBehavior(UGameplayAbility* OwningAbility, + FSmartObjectClaimHandle ClaimHandle, ESmartObjectClaimPriority ClaimPriority) +{ + if (OwningAbility == nullptr) + { + return nullptr; + } + + + UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* MyTask = NewAbilityTask(OwningAbility); + if (MyTask == nullptr) + { + return nullptr; + } + MyTask->SetClaimHandle(ClaimHandle); + return MyTask; +} + +void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::Activate() +{ + Super::Activate(); + bool bSuccess = false; + ON_SCOPE_EXIT + { + if (!bSuccess) + { + EndTask(); + } + }; + + if (!ensureMsgf(ClaimedHandle.IsValid(), TEXT("SmartObject handle must be valid at this point."))) + { + return; + } + + APawn* Pawn = Cast(GetAvatarActor()); + if (Pawn == nullptr) + { + GGS_CLOG(Error, "Pawn required to use GameplayBehavior with claim handle: %s.", *LexToString(ClaimedHandle)); + return; + } + USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(Pawn->GetWorld()); + if (!ensureMsgf(SmartObjectSubsystem != nullptr, TEXT("SmartObjectSubsystem must be accessible at this point."))) + { + return; + } + + // A valid claimed handle can point to an object that is no longer part of the simulation + if (!SmartObjectSubsystem->IsClaimedSmartObjectValid(ClaimedHandle)) + { + GGS_CLOG(Log, "Claim handle: %s refers to an object that is no longer available.", *LexToString(ClaimedHandle)); + return; + } + + // Register a callback to be notified if the claimed slot became unavailable + SmartObjectSubsystem->RegisterSlotInvalidationCallback(ClaimedHandle, FOnSlotInvalidated::CreateUObject(this, &UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSlotInvalidated)); + + bSuccess = StartInteraction(); +} + +bool UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::StartInteraction() +{ + UWorld* World = GetWorld(); + USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(World); + if (!ensure(SmartObjectSubsystem)) + { + return false; + } + + const UGameplayBehaviorSmartObjectBehaviorDefinition* SmartObjectGameplayBehaviorDefinition = SmartObjectSubsystem->MarkSlotAsOccupied< + UGameplayBehaviorSmartObjectBehaviorDefinition>(ClaimedHandle); + const UGameplayBehaviorConfig* GameplayBehaviorConfig = SmartObjectGameplayBehaviorDefinition != nullptr ? SmartObjectGameplayBehaviorDefinition->GameplayBehaviorConfig : nullptr; + GameplayBehavior = GameplayBehaviorConfig != nullptr ? GameplayBehaviorConfig->GetBehavior(*World) : nullptr; + if (GameplayBehavior == nullptr) + { + return false; + } + + const USmartObjectComponent* SmartObjectComponent = SmartObjectSubsystem->GetSmartObjectComponent(ClaimedHandle); + AActor& InteractorActor = *GetAvatarActor(); + AActor* InteracteeActor = SmartObjectComponent ? SmartObjectComponent->GetOwner() : nullptr; + const bool bBehaviorActive = UGameplayBehaviorSubsystem::TriggerBehavior(*GameplayBehavior, InteractorActor, GameplayBehaviorConfig, InteracteeActor); + // Behavior can be successfully triggered AND ended synchronously. We are only interested to register callback when still running + if (bBehaviorActive) + { + OnBehaviorFinishedNotifyHandle = GameplayBehavior->GetOnBehaviorFinishedDelegate().AddUObject(this, &UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSmartObjectBehaviorFinished); + } + + return bBehaviorActive; +} + +void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSmartObjectBehaviorFinished(UGameplayBehavior& Behavior, AActor& Avatar, const bool bInterrupted) +{ + // Adding an ensure in case the assumptions change in the future. + ensure(GetAvatarActor() != nullptr); + + // make sure we handle the right pawn - we can get this notify for a different + // Avatar if the behavior sending it out is not instanced (CDO is being used to perform actions) + if (GetAvatarActor() == &Avatar) + { + Behavior.GetOnBehaviorFinishedDelegate().Remove(OnBehaviorFinishedNotifyHandle); + bBehaviorFinished = true; + EndTask(); + } +} + +void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnDestroy(bool bInOwnerFinished) +{ + if (ClaimedHandle.IsValid()) + { + USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(GetWorld()); + check(SmartObjectSubsystem); + SmartObjectSubsystem->MarkSlotAsFree(ClaimedHandle); + SmartObjectSubsystem->UnregisterSlotInvalidationCallback(ClaimedHandle); + ClaimedHandle.Invalidate(); + } + + if (TaskState != EGameplayTaskState::Finished) + { + if (GameplayBehavior != nullptr && bBehaviorFinished) + { + OnSucceeded.Broadcast(); + } + else + { + OnFailed.Broadcast(); + } + } + + Super::OnDestroy(bInOwnerFinished); +} + +void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSlotInvalidated(const FSmartObjectClaimHandle& ClaimHandle, const ESmartObjectSlotState State) +{ + if (!bBehaviorFinished && GameplayBehavior != nullptr) + { + check(GetAvatarActor()); + GameplayBehavior->GetOnBehaviorFinishedDelegate().Remove(OnBehaviorFinishedNotifyHandle); + GameplayBehavior->AbortBehavior(*GetAvatarActor()); + } + EndTask(); +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollComponent.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollComponent.cpp new file mode 100644 index 0000000..de36574 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollComponent.cpp @@ -0,0 +1,608 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Ragdoll/GGS_RagdollComponent.h" +#include "GGS_LogChannels.h" +#include "TimerManager.h" +#include "Components/CapsuleComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkinnedAsset.h" +#include "Animation/AnimInstance.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" + + +// Sets default values for this component's properties +UGGS_RagdollComponent::UGGS_RagdollComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = true; + SetIsReplicatedByDefault(true); + + // ... +} + +const FGGS_RagdollState& UGGS_RagdollComponent::GetRagdollState() const +{ + return RagdollState; +} + +bool UGGS_RagdollComponent::IsRagdollAllowedToStart() const +{ + if (!IsValid(MeshComponent)) + { + GGS_CLOG(Warning, "Missing skeletal mesh component for the Ragdoll to work.") + return false; + } + if (bRagdolling) + { + return false; + } + FBodyInstance* PelvisBodyInstance = MeshComponent->GetBodyInstance(PelvisBoneName); + FBodyInstance* SpineBodyInstance = MeshComponent->GetBodyInstance(SpineBoneName); + if (PelvisBodyInstance == nullptr || SpineBodyInstance == nullptr) + { + GGS_CLOG(Warning, "A physics asset with the %s and %s bones are required for the Ragdoll to work.(Also ensure mesh component has collision enabled)", *PelvisBoneName.ToString(), + *SpineBoneName.ToString()) + return false; + } + return true; +} + +void UGGS_RagdollComponent::SetMeshComponent_Implementation(USkeletalMeshComponent* InMeshComponent) +{ + MeshComponent = InMeshComponent; +} + +bool UGGS_RagdollComponent::IsRagdolling_Implementation() const +{ + return bRagdolling; +} + +void UGGS_RagdollComponent::StartRagdoll() +{ + if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy || !IsRagdollAllowedToStart()) + { + return; + } + + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + MulticastStartRagdoll(); + } + else + { + if (ACharacter* Character = Cast(GetOwner())) + { + Character->GetCharacterMovement()->FlushServerMoves(); + } + ServerStartRagdoll(); + } +} + +void UGGS_RagdollComponent::ServerStartRagdoll_Implementation() +{ + if (IsRagdollAllowedToStart()) + { + MulticastStartRagdoll(); + GetOwner()->ForceNetUpdate(); + } +} + +void UGGS_RagdollComponent::MulticastStartRagdoll_Implementation() +{ + LocalStartRagdoll(); +} + +void UGGS_RagdollComponent::LocalStartRagdoll() +{ + if (!IsRagdollAllowedToStart()) + { + return; + } + + MeshComponent->bUpdateJointsFromAnimation = true; // Required for the flail animation to work properly. + + if (!MeshComponent->IsRunningParallelEvaluation() && !MeshComponent->GetBoneSpaceTransforms().IsEmpty()) + { + MeshComponent->UpdateRBJointMotors(); + } + + // Stop any active montages. + + static constexpr auto BlendOutDuration{0.2f}; + + MeshComponent->GetAnimInstance()->Montage_Stop(BlendOutDuration); + + if (IsValid(CharacterOwner)) + { + // Disable movement corrections and reset network smoothing. + CharacterOwner->GetCharacterMovement()->NetworkSmoothingMode = ENetworkSmoothingMode::Disabled; + CharacterOwner->GetCharacterMovement()->bIgnoreClientMovementErrorChecksAndCorrection = true; + } + + // Detach the mesh so that character transformation changes will not affect it in any way. + + MeshComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + + // Disable capsule collision and enable mesh physics simulation. + + UPrimitiveComponent* RootPrimitive = Cast(GetOwner()->GetRootComponent()); + { + RootPrimitive->SetCollisionEnabled(ECollisionEnabled::NoCollision); + } + + MeshComponent->SetCollisionObjectType(ECC_PhysicsBody); + MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + MeshComponent->SetSimulatePhysics(true); + + // This is required for the ragdoll to behave properly when any body instance is set to simulated in a physics asset. + // TODO Check the need for this in future engine versions. + MeshComponent->ResetAllBodiesSimulatePhysics(); + + const auto* PelvisBody{MeshComponent->GetBodyInstance(PelvisBoneName)}; + FVector PelvisLocation; + + FPhysicsCommand::ExecuteRead(PelvisBody->ActorHandle, [this, &PelvisLocation](const FPhysicsActorHandle& ActorHandle) + { + PelvisLocation = FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation(); + RagdollState.Velocity = FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle); + }); + + RagdollState.PullForce = 0.0f; + + if (bLimitInitialRagdollSpeed) + { + // Limit the ragdoll's speed for a few frames, because for some unclear reason, + // it can get a much higher initial speed than the character's last speed. + + // TODO Find a better solution or wait for a fix in future engine versions. + + static constexpr auto MinSpeedLimit{200.0f}; + + RagdollState.SpeedLimitFrameTimeRemaining = 8; + RagdollState.SpeedLimit = FMath::Max(MinSpeedLimit, UE_REAL_TO_FLOAT(GetOwner()->GetVelocity().Size())); + + ConstraintRagdollSpeed(); + } + + if (PawnOwner->GetLocalRole() >= ROLE_Authority) + { + SetRagdollTargetLocation(FVector::ZeroVector); + } + + if (PawnOwner->IsLocallyControlled() || (PawnOwner->GetLocalRole() >= ROLE_Authority && !IsValid(PawnOwner->GetController()))) + { + SetRagdollTargetLocation(PelvisLocation); + } + + // Clear the character movement mode and set the locomotion action to Ragdoll. + + if (IsValid(CharacterOwner)) + { + CharacterOwner->GetCharacterMovement()->SetMovementMode(MOVE_None); + } + bRagdolling = true; + OnRagdollStarted(); +} + +void UGGS_RagdollComponent::OnRagdollStarted_Implementation() +{ + OnRagdollStartedEvent.Broadcast(); +} + +bool UGGS_RagdollComponent::IsRagdollAllowedToStop() const +{ + return bRagdolling; +} + +bool UGGS_RagdollComponent::StopRagdoll() +{ + if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy || !IsRagdollAllowedToStop()) + { + return false; + } + + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + MulticastStopRagdoll(); + } + else + { + ServerStopRagdoll(); + } + + return true; +} + +void UGGS_RagdollComponent::ServerStopRagdoll_Implementation() +{ + if (IsRagdollAllowedToStop()) + { + MulticastStopRagdoll(); + GetOwner()->ForceNetUpdate(); + } +} + +void UGGS_RagdollComponent::MulticastStopRagdoll_Implementation() +{ + LocalStopRagdoll(); +} + +void UGGS_RagdollComponent::LocalStopRagdoll() +{ + if (!IsRagdollAllowedToStop()) + { + return; + } + + MeshComponent->SnapshotPose(RagdollState.FinalRagdollPose); + + const auto PelvisTransform{MeshComponent->GetSocketTransform(PelvisBoneName)}; + const auto PelvisRotation{PelvisTransform.Rotator()}; + + // Disable mesh physics simulation and enable capsule collision. + + MeshComponent->bUpdateJointsFromAnimation = false; + + MeshComponent->SetSimulatePhysics(false); + MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + MeshComponent->SetCollisionObjectType(ECC_Pawn); + + UPrimitiveComponent* RootPrimitive = Cast(GetOwner()->GetRootComponent()); + { + RootPrimitive->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + } + + + bool bGrounded; + const auto NewActorLocation{RagdollTraceGround(bGrounded)}; + + // Determine whether the ragdoll is facing upward or downward and set the actor rotation accordingly. + + const auto bRagdollFacingUpward{FMath::UnwindDegrees(PelvisRotation.Roll) <= 0.0f}; + + auto NewActorRotation{GetOwner()->GetActorRotation()}; + NewActorRotation.Yaw = bRagdollFacingUpward ? PelvisRotation.Yaw - 180.0f : PelvisRotation.Yaw; + + GetOwner()->SetActorLocationAndRotation(NewActorLocation, NewActorRotation, false, nullptr, ETeleportType::TeleportPhysics); + + // Attach the mesh back and restore its default relative location. + + const auto& ActorTransform{GetOwner()->GetActorTransform()}; + + FVector BaseTranslationOffset{FVector::Zero()}; + FQuat BaseRotationOffset; + if (IsValid(CharacterOwner)) + { + BaseTranslationOffset = CharacterOwner->GetBaseTranslationOffset(); + BaseRotationOffset = CharacterOwner->GetBaseRotationOffset(); + } + MeshComponent->SetWorldLocationAndRotationNoPhysics(ActorTransform.TransformPositionNoScale(BaseTranslationOffset), + ActorTransform.TransformRotation(BaseRotationOffset).Rotator()); + + MeshComponent->AttachToComponent(RootPrimitive, FAttachmentTransformRules::KeepWorldTransform); + + if (MeshComponent->ShouldUseUpdateRateOptimizations()) + { + // Disable URO for one frame to force the animation blueprint to update and get rid of the incorrect mesh pose. + + MeshComponent->bEnableUpdateRateOptimizations = false; + + GetWorldTimerManager().SetTimerForNextTick(FTimerDelegate::CreateWeakLambda(this, [this] + { + MeshComponent->bEnableUpdateRateOptimizations = true; + })); + } + + // Restore the pelvis transform to the state it was in before we changed + // the character and mesh transforms to keep its world transform unchanged. + + const auto& ReferenceSkeleton{MeshComponent->GetSkinnedAsset()->GetRefSkeleton()}; + + const auto PelvisBoneIndex{ReferenceSkeleton.FindBoneIndex(PelvisBoneName)}; + if (PelvisBoneIndex >= 0) + { + // We expect the pelvis bone to be the root bone or attached to it, so we can safely use the mesh transform here. + RagdollState.FinalRagdollPose.LocalTransforms[PelvisBoneIndex] = PelvisTransform.GetRelativeTransform(MeshComponent->GetComponentTransform()); + } + + bRagdolling = false; + + if (IsValid(CharacterOwner)) + { + if (bGrounded) + { + CharacterOwner->GetCharacterMovement()->SetMovementMode(CharacterOwner->GetCharacterMovement()->DefaultLandMovementMode); + } + else + { + CharacterOwner->GetCharacterMovement()->SetMovementMode(MOVE_Falling); + CharacterOwner->GetCharacterMovement()->Velocity = RagdollState.Velocity; + } + } + + OnRagdollEnded(bGrounded); + + if (bGrounded && bPlayGetupMontageAfterRagdollEndedOnGround) + { + if (UAnimMontage* SelectedMontage = SelectGetUpMontage(bRagdollFacingUpward)) + { + MeshComponent->GetAnimInstance()->Montage_Play(SelectedMontage); + } + } +} + +void UGGS_RagdollComponent::OnRagdollEnded_Implementation(bool bGrounded) +{ + OnRagdollEndedEvent.Broadcast(bGrounded); + // If the ragdoll is on the ground, set the movement mode to walking and play a get up montage. If not, set + // the movement mode to falling and update the character movement velocity to match the last ragdoll velocity. + + // AlsCharacterMovement->SetMovementModeLocked(false); + // + + // + // SetLocomotionAction(FGameplayTag::EmptyTag); + // + // if (bGrounded && MeshComponent->GetAnimInstance()->Montage_Play(SelectGetUpMontage(bRagdollFacingUpward)) > 0.0f) + // { + // AlsCharacterMovement->SetInputBlocked(true); + // + // SetLocomotionAction(AlsLocomotionActionTags::GettingUp); + // } +} + +void UGGS_RagdollComponent::SetRagdollTargetLocation(const FVector& NewTargetLocation) +{ + if (RagdollTargetLocation != NewTargetLocation) + { + RagdollTargetLocation = NewTargetLocation; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, RagdollTargetLocation, this) + + if (GetOwner()->GetLocalRole() == ROLE_AutonomousProxy) + { + ServerSetRagdollTargetLocation(RagdollTargetLocation); + } + } +} + +void UGGS_RagdollComponent::ServerSetRagdollTargetLocation_Implementation(const FVector_NetQuantize& NewTargetLocation) +{ + SetRagdollTargetLocation(NewTargetLocation); +} + +void UGGS_RagdollComponent::RefreshRagdoll(float DeltaTime) +{ + if (!bRagdolling) + { + return; + } + + // Since we are dealing with physics here, we should not use functions such as USkinnedMeshComponent::GetSocketTransform() as + // they may return an incorrect result in situations like when the animation blueprint is not ticking or when URO is enabled. + + const auto* PelvisBody{MeshComponent->GetBodyInstance(PelvisBoneName)}; + FVector PelvisLocation; + + FPhysicsCommand::ExecuteRead(PelvisBody->ActorHandle, [this, &PelvisLocation](const FPhysicsActorHandle& ActorHandle) + { + PelvisLocation = FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation(); + RagdollState.Velocity = FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle); + }); + + const auto bLocallyControlled{PawnOwner->IsLocallyControlled() || (PawnOwner->GetLocalRole() >= ROLE_Authority && !IsValid(PawnOwner->GetController()))}; + + if (bLocallyControlled) + { + SetRagdollTargetLocation(PelvisLocation); + } + + // Prevent the capsule from going through the ground when the ragdoll is lying on the ground. + + // While we could get rid of the line trace here and just use RagdollTargetLocation + // as the character's location, we don't do that because the camera depends on the + // capsule's bottom location, so its removal will cause the camera to behave erratically. + + bool bGrounded; + PawnOwner->SetActorLocation(RagdollTraceGround(bGrounded), false, nullptr, ETeleportType::TeleportPhysics); + + // Zero target location means that it hasn't been replicated yet, so we can't apply the logic below. + + if (!bLocallyControlled && !RagdollTargetLocation.IsZero()) + { + // Apply ragdoll location corrections. + + static constexpr auto PullForce{750.0f}; + static constexpr auto InterpolationHalfLife{1.2f}; + + RagdollState.PullForce = FMath::Lerp(RagdollState.PullForce, PullForce, DamperExactAlpha(DeltaTime, InterpolationHalfLife)); + + const auto HorizontalSpeedSquared{RagdollState.Velocity.SizeSquared2D()}; + + const auto PullForceBoneName{ + HorizontalSpeedSquared > FMath::Square(300.0f) ? SpineBoneName : PelvisBoneName + }; + + auto* PullForceBody{MeshComponent->GetBodyInstance(PullForceBoneName)}; + + FPhysicsCommand::ExecuteWrite(PullForceBody->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle) + { + if (!FPhysicsInterface::IsRigidBody(ActorHandle)) + { + return; + } + + const auto PullForceVector{ + RagdollTargetLocation - FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation() + }; + + static constexpr auto MinPullForceDistance{5.0f}; + static constexpr auto MaxPullForceDistance{50.0f}; + + if (PullForceVector.SizeSquared() > FMath::Square(MinPullForceDistance)) + { + FPhysicsInterface::AddForce_AssumesLocked( + ActorHandle, PullForceVector.GetClampedToMaxSize(MaxPullForceDistance) * RagdollState.PullForce, true, true); + } + }); + } + + // Use the speed to scale ragdoll joint strength for physical animation. + + static constexpr auto ReferenceSpeed{1000.0f}; + static constexpr auto Stiffness{25000.0f}; + + const auto SpeedAmount{Clamp01(UE_REAL_TO_FLOAT(RagdollState.Velocity.Size() / ReferenceSpeed))}; + + MeshComponent->SetAllMotorsAngularDriveParams(SpeedAmount * Stiffness, 0.0f, 0.0f); + + // Limit the speed of ragdoll bodies. + + if (RagdollState.SpeedLimitFrameTimeRemaining > 0) + { + RagdollState.SpeedLimitFrameTimeRemaining -= 1; + + ConstraintRagdollSpeed(); + } +} + +FVector UGGS_RagdollComponent::RagdollTraceGround(bool& bGrounded) const +{ + auto RagdollLocation{!RagdollTargetLocation.IsZero() ? FVector{RagdollTargetLocation} : GetOwner()->GetActorLocation()}; + + ACharacter* Character = Cast(GetOwner()); + if (!IsValid(Character)) + return RagdollLocation; + + // We use a sphere sweep instead of a simple line trace to keep capsule + // movement consistent between Ragdoll and regular character movement. + + const auto CapsuleRadius{Character->GetCapsuleComponent()->GetScaledCapsuleRadius()}; + const auto CapsuleHalfHeight{Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()}; + + const FVector TraceStart{RagdollLocation.X, RagdollLocation.Y, RagdollLocation.Z + 2.0f * CapsuleRadius}; + const FVector TraceEnd{RagdollLocation.X, RagdollLocation.Y, RagdollLocation.Z - CapsuleHalfHeight + CapsuleRadius}; + + const auto CollisionChannel{Character->GetCharacterMovement()->UpdatedComponent->GetCollisionObjectType()}; + + FCollisionQueryParams QueryParameters{TEXT("RagdollTraceGround"), false, GetOwner()}; + FCollisionResponseParams CollisionResponses; + Character->GetCharacterMovement()->InitCollisionParams(QueryParameters, CollisionResponses); + + FHitResult Hit; + bGrounded = GetWorld()->SweepSingleByChannel(Hit, TraceStart, TraceEnd, FQuat::Identity, + CollisionChannel, FCollisionShape::MakeSphere(CapsuleRadius), + QueryParameters, CollisionResponses); + + // #if ENABLE_DRAW_DEBUG + // UAlsDebugUtility::DrawSweepSingleSphere(GetWorld(), TraceStart, TraceEnd, CapsuleRadius, + // bGrounded, Hit, {0.0f, 0.25f, 1.0f}, + // {0.0f, 0.75f, 1.0f}, 0.0f); + // #endif + + return { + RagdollLocation.X, RagdollLocation.Y, + bGrounded + ? Hit.Location.Z + CapsuleHalfHeight - CapsuleRadius + UCharacterMovementComponent::MIN_FLOOR_DIST + : RagdollLocation.Z + }; +} + +void UGGS_RagdollComponent::ConstraintRagdollSpeed() const +{ + MeshComponent->ForEachBodyBelow(NAME_None, true, false, [this](FBodyInstance* Body) + { + FPhysicsCommand::ExecuteWrite(Body->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle) + { + if (!FPhysicsInterface::IsRigidBody(ActorHandle)) + { + return; + } + + auto Velocity{FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle)}; + if (Velocity.SizeSquared() <= FMath::Square(RagdollState.SpeedLimit)) + { + return; + } + + Velocity.Normalize(); + Velocity *= RagdollState.SpeedLimit; + + FPhysicsInterface::SetLinearVelocity_AssumesLocked(ActorHandle, Velocity); + }); + }); +} + +UAnimMontage* UGGS_RagdollComponent::SelectGetUpMontage_Implementation(bool bRagdollFacingUpward) +{ + if (GetUpBackMontage.IsNull() || GetUpFrontMontage.IsNull()) + { + return nullptr; + } + return bRagdollFacingUpward ? GetUpBackMontage.LoadSynchronous() : GetUpFrontMontage.LoadSynchronous(); +} + + +// Called when the game starts +void UGGS_RagdollComponent::BeginPlay() +{ + Super::BeginPlay(); + + PawnOwner = GetPawnChecked(); + CharacterOwner = GetPawn(); + + if (CharacterOwner) + { + MeshComponent = CharacterOwner->GetMesh(); + } + + if (!MeshComponent) + { + MeshComponent = GetOwner()->FindComponentByClass(); + } + if (!MeshComponent) + { + GGS_CLOG(Warning, "Require skeletal mesh component for the Ragdoll to work.") + } +} + +float UGGS_RagdollComponent::DamperExactAlpha(float DeltaTime, float HalfLife) +{ + return 1.0f - FMath::InvExpApprox(0.6931471805599453f / (HalfLife + UE_SMALL_NUMBER) * DeltaTime); +} + +float UGGS_RagdollComponent::Clamp01(float Value) +{ + return Value > 0.0f + ? Value < 1.0f + ? Value + : 1.0f + : 0.0f; +} + + +void UGGS_RagdollComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + Parameters.Condition = COND_SkipOwner; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, RagdollTargetLocation, Parameters) +} + +// Called every frame +void UGGS_RagdollComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + RefreshRagdoll(DeltaTime); + // ... +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollStructLibrary.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollStructLibrary.cpp new file mode 100644 index 0000000..db27b69 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Ragdoll/GGS_RagdollStructLibrary.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Ragdoll/GGS_RagdollStructLibrary.h" diff --git a/Plugins/GGS/Source/GenericGameSystem/Private/Utilities/GGS_SocketRelationshipMapping.cpp b/Plugins/GGS/Source/GenericGameSystem/Private/Utilities/GGS_SocketRelationshipMapping.cpp new file mode 100644 index 0000000..1ef7346 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Private/Utilities/GGS_SocketRelationshipMapping.cpp @@ -0,0 +1,109 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utilities/GGS_SocketRelationshipMapping.h" +#include "Engine/StreamableRenderAsset.h" +#include "Engine/SkeletalMesh.h" +#include "Components/SkeletalMeshComponent.h" +#include "Animation/Skeleton.h" +#include "UObject/ObjectSaveContext.h" + +bool UGGS_SocketRelationshipMapping::FindSocketAdjustment(const USkeletalMeshComponent* InParentMeshComponent, const UStreamableRenderAsset* InMeshAsset, FName InSocketName, + FGGS_SocketAdjustment& OutAdjustment) const +{ + if (InParentMeshComponent == nullptr || InMeshAsset == nullptr || InSocketName.IsNone()) + { + return false; + } + USkeleton* Skeleton = InParentMeshComponent->GetSkeletalMeshAsset()->GetSkeleton(); + if (!Skeleton) + { + return false; + } + FString SkeletonName = Skeleton->GetName(); + + for (const FGGS_SocketRelationship& Relationship : Relationships) + { + UStreamableRenderAsset* Key{nullptr}; + if (!Relationship.MeshAsset.IsNull()) + { + Key = Relationship.MeshAsset.LoadSynchronous(); + } + if (!Key || Key->GetName() != InMeshAsset->GetName()) + { + continue; + } + for (int32 i = Relationship.Adjustments.Num() - 1; i >= 0; i--) + { + const FGGS_SocketAdjustment& Adjustment = Relationship.Adjustments[i]; + bool bMatchSkeleton = Adjustment.ForSkeletons.IsEmpty() ? true : Adjustment.ForSkeletons.Contains(SkeletonName); + if (bMatchSkeleton && Adjustment.SocketName == InSocketName) + { + OutAdjustment = Adjustment; + return true; + } + } + } + + return false; +} + +bool UGGS_SocketRelationshipMapping::FindSocketAdjustmentInMappings(TArray> InMappings, const USkeletalMeshComponent* InParentMeshComponent, + const UStreamableRenderAsset* InMeshAsset, FName InSocketName, + FGGS_SocketAdjustment& OutAdjustment) +{ + for (TSoftObjectPtr Mapping : InMappings) + { + if (Mapping.IsNull()) + { + continue; + } + if (const UGGS_SocketRelationshipMapping* LoadedMapping = Mapping.LoadSynchronous()) + { + if (LoadedMapping->FindSocketAdjustment(InParentMeshComponent, InMeshAsset, InSocketName, OutAdjustment)) + { + return true; + } + } + } + return false; +} + +#if WITH_EDITORONLY_DATA +void UGGS_SocketRelationshipMapping::PreSave(FObjectPreSaveContext SaveContext) +{ + for (FGGS_SocketRelationship& Relationship : Relationships) + { + if (Relationship.MeshAsset.IsNull()) + { + Relationship.EditorFriendlyName = TEXT("Invalid!"); + } + else + { + UStreamableRenderAsset* MeshAsset = Relationship.MeshAsset.LoadSynchronous(); + Relationship.EditorFriendlyName = MeshAsset->GetName(); + for (FGGS_SocketAdjustment& Adjustment : Relationship.Adjustments) + { + if (Adjustment.SocketName == NAME_None) + { + Adjustment.EditorFriendlyName = "Empty adjustments!"; + } + if (Adjustment.ForSkeletons.IsEmpty()) + { + Adjustment.EditorFriendlyName = Adjustment.SocketName.ToString(); + } + else + { + FString SkeletonNames; + for (const FString& ForSkeleton : Adjustment.ForSkeletons) + { + SkeletonNames = SkeletonNames.Append(ForSkeleton); + } + Adjustment.EditorFriendlyName = FString::Format(TEXT("{0} on {1}"), {Adjustment.SocketName.ToString(), SkeletonNames}); + } + } + } + } + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/GGS_LogChannels.h b/Plugins/GGS/Source/GenericGameSystem/Public/GGS_LogChannels.h new file mode 100644 index 0000000..fc25b90 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/GGS_LogChannels.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + +/** + * Gets the context string for logging purposes. + * 获取用于日志记录的上下文字符串。 + * @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。 + * @return The context string. 上下文字符串。 + */ +FString GetGGSLogContextString(const UObject* ContextObject = nullptr); + +/** + * Log category for generic game system messages. + * 通用游戏系统消息的日志类别。 + */ +GENERICGAMESYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGGS, Log, All); + +/** + * Macro for logging generic game system messages. + * 用于记录通用游戏系统消息的宏。 + * @details Logs messages with function name and formatted message. 记录包含函数名和格式化消息的日志。 + */ +#define GGS_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGGS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging for generic game system. + * 用于通用游戏系统的基于上下文的日志记录宏。 + * @details Logs messages with function name, context, and formatted message. 记录包含函数名、上下文和格式化消息的日志。 + */ +#define GGS_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGGS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGGSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging with an explicit owner. + * 使用显式拥有者进行基于上下文的日志记录宏。 + * @details Logs messages with function name, owner context, and formatted message. 记录包含函数名、拥有者上下文和格式化消息的日志。 + */ +#define GGS_OWNED_CLOG(LogOwner, Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGGS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGGSLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/GenericGameSystem.h b/Plugins/GGS/Source/GenericGameSystem/Public/GenericGameSystem.h new file mode 100644 index 0000000..056152a --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/GenericGameSystem.h @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FGenericGameSystemModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Abilities/GGS_GameplayAbility_Interaction.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Abilities/GGS_GameplayAbility_Interaction.h new file mode 100644 index 0000000..e9c50b6 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Abilities/GGS_GameplayAbility_Interaction.h @@ -0,0 +1,88 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "SmartObjectRuntime.h" +#include "Abilities/GameplayAbility.h" +#include "GGS_GameplayAbility_Interaction.generated.h" + +class UGGS_InteractionSystemComponent; +class USmartObjectComponent; + +/** + * Core gameplay ability for handling interactions. + * 处理交互的核心游戏技能。 + */ +UCLASS(BlueprintType, Blueprintable) +class GENERICGAMESYSTEM_API UGGS_GameplayAbility_Interaction : public UGameplayAbility +{ + GENERATED_BODY() + +public: + /** + * Constructor for the interaction gameplay ability. + * 交互游戏技能构造函数。 + */ + UGGS_GameplayAbility_Interaction(); + + /** + * Activates the interaction ability. + * 激活交互技能。 + * @param Handle The ability specification handle. 技能规格句柄。 + * @param ActorInfo Information about the actor using the ability. 使用技能的Actor信息。 + * @param ActivationInfo Information about the ability activation. 技能激活信息。 + * @param TriggerEventData Optional event data triggering the ability. 触发技能的可选事件数据。 + */ + virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, + const FGameplayEventData* TriggerEventData) override; + + /** + * Ends the interaction ability. + * 结束交互技能。 + * @param Handle The ability specification handle. 技能规格句柄。 + * @param ActorInfo Information about the actor using the ability. 使用技能的Actor信息。 + * @param ActivationInfo Information about the ability activation. 技能激活信息。 + * @param bReplicateEndAbility Whether to replicate the end ability call. 是否同步结束技能调用。 + * @param bWasCancelled Whether the ability was cancelled. 技能是否被取消。 + */ + virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, + bool bWasCancelled) override; + +protected: + /** + * Attempts to claim an interaction with a smart object. + * 尝试认领与智能对象的交互。 + * @param Index The interaction option index. 交互选项索引。 + * @param ClaimedHandle The claimed smart object handle (output). 认领的智能对象句柄(输出)。 + * @return True if the interaction was claimed successfully, false otherwise. 如果交互成功认领返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GCS|Interaction", meta=(ExpandBoolAsExecs=ReturnValue)) + bool TryClaimInteraction(int32 Index, FSmartObjectClaimHandle& ClaimedHandle); + + /** + * Called when the interactable actor changes. + * 可交互演员变更时调用。 + * @param OldActor The previous interactable actor. 之前的可交互演员。 + * @param NewActor The new interactable actor. 新的可交互演员。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GCS|Interaction") + void OnInteractActorChanged(AActor* OldActor, AActor* NewActor); + + /** + * Reference to the interaction system component. + * 交互系统组件的引用。 + */ + UPROPERTY(BlueprintReadOnly, Category="GCS|Interaction") + TObjectPtr InteractionSystem{nullptr}; + +#if WITH_EDITORONLY_DATA + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h new file mode 100644 index 0000000..f12a904 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayBehaviorConfig.h" +#include "GameplayTagContainer.h" +#include "UObject/Object.h" +#include "GGS_GameplayBehaviorConfig_InteractionWithAbility.generated.h" + +class UGameplayAbility; +class UUserWidget; + +/** + * Configuration for ability-based interaction behavior. + * 基于技能的交互行为配置。 + */ +UCLASS(DisplayName="Gameplay Behavior Config Interaction (GGS)") +class GENERICGAMESYSTEM_API UGGS_GameplayBehaviorConfig_InteractionWithAbility : public UGameplayBehaviorConfig +{ + GENERATED_BODY() + +public: + /** + * Constructor for the interaction behavior config. + * 交互行为配置构造函数。 + */ + UGGS_GameplayBehaviorConfig_InteractionWithAbility(); + + /** + * The ability to grant and activate when interaction begins. + * 交互开始时赋予并激活的技能。 + * @note Must be instanced and not LocalOnly. Does not support event-triggered abilities. + * @注意 必须是实例化的技能,不能是LocalOnly。不支持事件触发的技能。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction") + TSoftClassPtr AbilityToGrant; + + /** + * The level of the ability, used for visual distinctions. + * 技能等级,用于视觉区分。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction") + int32 AbilityLevel{0}; + +#if WITH_EDITORONLY_DATA + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h new file mode 100644 index 0000000..b6cc53d --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h @@ -0,0 +1,92 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayAbilitySpecHandle.h" +#include "GameplayBehavior.h" +#include "Abilities/GameplayAbilityTypes.h" +#include "GGS_GameplayBehavior_InteractionWithAbility.generated.h" + +/** + * Gameplay behavior for ability-based interactions. + * 基于技能的交互游戏行为。 + */ +UCLASS(DisplayName="GameplayBehavior_Interaction (GGS)", NotBlueprintable) +class GENERICGAMESYSTEM_API UGGS_GameplayBehavior_InteractionWithAbility : public UGameplayBehavior +{ + GENERATED_BODY() + +public: + /** + * Triggers the interaction behavior. + * 触发交互行为。 + * @param InAvatar The avatar actor. 化身演员。 + * @param Config The behavior config. 行为配置。 + * @param SmartObjectOwner The smart object owner. 智能对象拥有者。 + * @return True if the behavior was triggered successfully, false otherwise. 如果行为成功触发返回true,否则返回false。 + */ + virtual bool Trigger(AActor& InAvatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner) override; + + /** + * Ends the behavior. + * 结束行为。 + * @param Avatar The avatar actor. 化身演员。 + * @param bInterrupted Whether the behavior was interrupted. 行为是否被中断。 + */ + virtual void EndBehavior(AActor& Avatar, const bool bInterrupted) override; + + /** + * Checks the validity of the ability settings. + * 检查技能设置的有效性。 + * @param Config The behavior config. 行为配置。 + * @param OutAbilityClass The ability class (output). 技能类(输出)。 + * @param OutAbilityLevel The ability level (output). 技能等级(输出)。 + * @return True if the settings are valid, false otherwise. 如果设置有效返回true,否则返回false。 + */ + bool CheckValidAbilitySetting(const UGameplayBehaviorConfig* Config, TSubclassOf& OutAbilityClass, int32& OutAbilityLevel); + + /** + * The ability class granted for the interaction. + * 为交互授予的技能类。 + */ + UPROPERTY() + TSubclassOf GrantedAbilityClass{nullptr}; + + /** + * Handle for the granted ability spec. + * 授予技能规格的句柄。 + */ + FGameplayAbilitySpecHandle AbilitySpecHandle; + + /** + * Indicates if the behavior was interrupted. + * 表示行为是否被中断。 + */ + bool bBehaviorWasInterrupted = false; + + /** + * Indicates if the ability has ended. + * 表示技能是否已结束。 + */ + bool bAbilityEnded = false; + + /** + * Indicates if the ability was cancelled. + * 表示技能是否被取消。 + */ + bool bAbilityWasCancelled = false; + + /** + * Delegate handle for ability end notification. + * 技能结束通知的委托句柄。 + */ + FDelegateHandle AbilityEndedDelegateHandle; + + /** + * Called when the ability ends. + * 技能结束时调用。 + * @param EndedData The ability end data. 技能结束数据。 + */ + virtual void OnAbilityEndedCallback(const FAbilityEndedData& EndedData); +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractableInterface.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractableInterface.h new file mode 100644 index 0000000..3196ce4 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractableInterface.h @@ -0,0 +1,79 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GGS_InteractableInterface.generated.h" + +/** + * Interface for actors to handle interaction events. + * 处理交互事件的演员接口。 + */ +UINTERFACE() +class GENERICGAMESYSTEM_API UGGS_InteractableInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Implementation class for interactable actors. + * 可交互演员的实现类。 + */ +class GENERICGAMESYSTEM_API IGGS_InteractableInterface +{ + GENERATED_BODY() + +public: + /** + * Retrieves the display name for the interactable actor. + * 获取可交互演员的显示名称。 + * @return The display name. 显示名称。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + FText GetInteractionDisplayName() const; + virtual FText GetInteractionDisplayNameText_Implementation() const; + + /** + * Called when the actor is selected by the interaction system. + * 演员被交互系统选中时调用。 + * @param Instigator The instigating actor, usually the player. 发起者,通常是玩家。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + void OnInteractionSelected(AActor* Instigator); + + /** + * Called when the actor is deselected by the interaction system. + * 演员被交互系统取消选中时调用。 + * @param Instigator The instigating actor. 发起者。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + void OnInteractionDeselected(AActor* Instigator); + + /** + * Called when interaction with the actor starts. + * 与演员交互开始时调用。 + * @param Instigator The instigating actor. 发起者。 + * @param Index The interaction option index. 交互选项索引。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + void OnInteractionStarted(AActor* Instigator, int32 Index); + + /** + * Called when interaction with the actor ends. + * 与演员交互结束时调用。 + * @param Instigator The instigating actor. 发起者。 + * @param Index The interaction option index. 交互选项索引。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + void OnInteractionEnded(AActor* Instigator, int32 Index); + + /** + * Called when an interaction option is selected. + * 交互选项被选中时调用。 + * @param Instigator The instigating actor. 发起者。 + * @param OptionIndex The selected option index. 选中的选项索引。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction") + void OnInteractionOptionSelected(AActor* Instigator, int32 OptionIndex); +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionDefinition.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionDefinition.h new file mode 100644 index 0000000..d783405 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionDefinition.h @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "Engine/DataAsset.h" +#include "GGS_InteractionDefinition.generated.h" + +/** + * Base class for interaction settings, used in smart object interaction entrances. + * 交互设置基类,用于智能对象交互入口。 + */ +UCLASS(BlueprintType, Blueprintable) +class GENERICGAMESYSTEM_API UGGS_InteractionDefinition : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Display text for the interaction. + * 交互的显示文本。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + FText Text; + + /** + * Sub-text for the interaction. + * 交互的子文本。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + FText SubText; + + /** + * Input action that triggers the interaction. + * 触发交互的输入动作。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase")) + FDataTableRowHandle TriggeringInputAction; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionStructLibrary.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionStructLibrary.h new file mode 100644 index 0000000..dff3bde --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionStructLibrary.h @@ -0,0 +1,108 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "SmartObjectRuntime.h" +#include "SmartObjectSubsystem.h" +#include "SmartObjectTypes.h" +#if ENGINE_MINOR_VERSION >= 6 +#include "SmartObjectRequestTypes.h" +#endif +#include "Engine/DataTable.h" +#include "UObject/Object.h" +#include "GGS_InteractionStructLibrary.generated.h" + +class UGGS_InteractionSystemComponent; +class UAbilitySystemComponent; +class UGGS_InteractionDefinition; + +/** + * Structure wrapping an interaction definition for smart object interaction. + * 封装智能对象交互的交互定义结构。 + */ +USTRUCT(DisplayName="Interaction Entrance") +struct GENERICGAMESYSTEM_API FGGS_SmartObjectInteractionEntranceData : public FSmartObjectDefinitionData +{ + GENERATED_BODY() + + /** + * Interaction definition containing static data for player interaction. + * 包含玩家交互静态数据的交互定义。 + * @note Replicated across the network. + * @注意 通过网络同步。 + */ + UPROPERTY(EditAnywhere, Category="Interaction", meta=(DisplayName="Definition")) + TSoftObjectPtr DefinitionDA{nullptr}; +}; + +/** + * Structure representing an interaction option. + * 表示交互选项的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMESYSTEM_API FGGS_InteractionOption +{ + GENERATED_BODY() + + /** + * Interaction definition associated with this option. + * 与此选项关联的交互定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + TObjectPtr Definition{nullptr}; + + /** + * Smart object request result for this option. Not replicated. + * 此选项的智能对象请求结果。未网络同步。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, NotReplicated, Category="Interaction") + FSmartObjectRequestResult RequestResult; + + /** + * Smart object behavior definition for this option. + * 此选项的智能对象行为定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, NotReplicated, Category="Interaction") + TObjectPtr BehaviorDefinition; + + /** + * Index of the associated smart object slot, used for UI sorting. + * 关联智能对象槽的索引,用于UI排序。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + int32 SlotIndex{-1}; + + /** + * State of the associated smart object slot, used for UI input rules. + * 关联智能对象槽的状态,用于UI输入规则。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + ESmartObjectSlotState SlotState{ESmartObjectSlotState::Free}; + + /** + * Equality operator for comparing interaction options. + * 交互选项的相等比较运算符。 + */ + friend bool operator==(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS); + + /** + * Inequality operator for comparing interaction options. + * 交互选项的不等比较运算符。 + */ + friend bool operator!=(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS); + + /** + * Less-than operator for sorting interaction options by slot index. + * 按槽索引排序交互选项的比较运算符。 + */ + friend bool operator<(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS); + + /** + * Converts the interaction option to a string representation. + * 将交互选项转换为字符串表示。 + * @return String representation of the option. 选项的字符串表示。 + */ + FString ToString() const; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionSystemComponent.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionSystemComponent.h new file mode 100644 index 0000000..d565542 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_InteractionSystemComponent.h @@ -0,0 +1,332 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGS_InteractionStructLibrary.h" +#include "SmartObjectSubsystem.h" +#include "Components/ActorComponent.h" +#include "GGS_InteractionSystemComponent.generated.h" + +class UCommonUserWidget; +class UGameplayBehavior; + +/** + * Delegate for interaction events. + * 交互事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FInteractionEventSignature); + +/** + * Delegate for changes in the interactable actor. + * 可交互演员变更的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FInteractableActorChangedSignature, AActor*, OldActor, AActor*, NewActor); + +/** + * Delegate for changes in the interacting state. + * 交互状态变更的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInteractingStateChangedSignature, bool, bInteracting); + +/** + * Delegate for changes in the number of interactable actors. + * 可交互演员数量变更的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInteractableActorNumChangedSignature, int32, ActorsNum); + +/** + * Component for managing interactions with smart objects. + * 管理与智能对象交互的组件。 + */ +UCLASS(Blueprintable, BlueprintType, ClassGroup=(GGS), meta=(BlueprintSpawnableComponent)) +class GENERICGAMESYSTEM_API UGGS_InteractionSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the interaction system component. + * 交互系统组件构造函数。 + */ + UGGS_InteractionSystemComponent(); + + /** + * Retrieves lifetime replicated properties. + * 获取生命周期内同步的属性。 + * @param OutLifetimeProps The replicated properties. 同步的属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Retrieves the interaction system component from an actor. + * 从演员获取交互系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @return The interaction system component. 交互系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem", meta=(DefaultToSelf="Actor")) + static UGGS_InteractionSystemComponent* GetInteractionSystemComponent(const AActor* Actor); + + /** + * Cycles through interactable actors. + * 循环切换可交互演员。 + * @param bNext Whether to cycle to the next actor. 是否切换到下一个演员。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GGS|InteractionSystem") + void CycleInteractableActors(bool bNext); + + /** + * Triggers a search for potential interactable actors. + * 触发潜在可交互演员的搜索。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + void SearchInteractableActors(); + + /** + * Sets a new array of interactable actors. + * 设置新的可交互演员数组。 + * @param NewActors The new interactable actors. 新的可交互演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + void SetInteractableActors(TArray NewActors); + + /** + * Sets the number of interactable actors. + * 设置可交互演员的数量。 + * @param NewNum The new number of interactable actors. 可交互演员的新数量。 + */ + void SetInteractableActorsNum(int32 NewNum); + + /** + * Retrieves the array of interactable actors. + * 获取可交互演员数组。 + * @return The interactable actors. 可交互演员。 + */ + TArray GetInteractableActors() const { return InteractableActors; } + + /** + * Retrieves the number of interactable actors. + * 获取可交互演员的数量。 + * @return The number of interactable actors. 可交互演员数量。 + */ + int32 GetNumOfInteractableActors() const { return NumsOfInteractableActors; } + + /** + * Sets the current interactable actor. + * 设置当前可交互演员。 + * @param InActor The actor to set. 要设置的演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + void SetInteractableActor(AActor* InActor); + + /** + * Retrieves the current interactable actor. + * 获取当前可交互演员。 + * @return The interactable actor. 可交互演员。 + */ + AActor* GetInteractableActor() const { return InteractableActor; } + + /** + * Delegate for when the interactable actor changes. + * 可交互演员变更时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FInteractableActorChangedSignature OnInteractableActorChangedEvent; + + /** + * Delegate for when the number of interactable actors changes. + * 可交互演员数量变更时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FInteractableActorNumChangedSignature OnInteractableActorNumChangedEvent; + + /** + * Delegate for when the interacting state changes. + * 交互状态变更时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FInteractingStateChangedSignature OnInteractingStateChangedEvent; + + /** + * Delegate for when the interaction options change. + * 交互选项变更时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FInteractionEventSignature OnInteractionOptionsChangedEvent; + + /** + * Delegate for when a search for interactable actors is triggered. + * 触发可交互演员搜索时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FInteractionEventSignature OnSearchInteractableActorsEvent; + + /** + * Retrieves the smart object request filter. + * 获取智能对象请求过滤器。 + * @return The smart object request filter. 智能对象请求过滤器。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GGS|InteractionSystem") + FSmartObjectRequestFilter GetSmartObjectRequestFilter(); + virtual FSmartObjectRequestFilter GetSmartObjectRequestFilter_Implementation(); + + /** + * Starts an interaction with the specified option index. + * 开始与指定选项索引的交互。 + * @param NewIndex The interaction option index. 交互选项索引。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + virtual void StartInteraction(int32 NewIndex = 0); + + /** + * Ends the current interaction. + * 结束当前交互。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + virtual void EndInteraction(); + + /** + * Performs an instant interaction with the specified option index. + * 执行与指定选项索引的即时交互。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem") + void InstantInteraction(int32 NewIndex = 0); + + /** + * Checks if an interaction is in progress. + * 检查是否正在进行交互。 + * @return True if interacting, false otherwise. 如果正在交互返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem") + bool IsInteracting() const; + + /** + * Retrieves the current interacting option index. + * 获取当前交互选项索引。 + * @return The interacting option index. 交互选项索引。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem") + int32 GetInteractingOption() const; + + /** + * Retrieves the current interaction options. + * 获取当前交互选项。 + * @return The interaction options. 交互选项。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem") + const TArray& GetInteractionOptions() const { return InteractionOptions; } + +protected: + /** + * Called when the interactable actor changes. + * 可交互演员变更时调用。 + * @param OldActor The previous interactable actor. 之前的可交互演员。 + */ + UFUNCTION() + virtual void OnInteractableActorChanged(AActor* OldActor); + + /** + * Called when the number of interactable actors changes. + * 可交互演员数量变更时调用。 + */ + UFUNCTION() + virtual void OnInteractableActorsNumChanged(); + + /** + * Called when the potential interactable actors changes. + * 可交互演员变更时调用。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGS|InteractionSystem") + void OnInteractableActorsChanged(); + + /** + * Called when a smart object event occurs. + * 智能对象事件发生时调用。 + * @param EventData The smart object event data. 智能对象事件数据。 + */ + UFUNCTION() + virtual void OnSmartObjectEventCallback(const FSmartObjectEventData& EventData); + + /** + * Called when interaction options change. + * 交互选项变更时调用。 + */ + UFUNCTION() + virtual void OnInteractionOptionsChanged(); + + /** + * Called when the interacting option index changes. + * 交互选项索引变更时调用。 + * @param PrevOptionIndex The previous option index. 之前的选项索引。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GGS|InteractionSystem") + void OnInteractingOptionChanged(int32 PrevOptionIndex); + + /** + * Refreshes interaction options based on smart object request results. + * 根据智能对象请求结果刷新交互选项。 + */ + virtual void RefreshOptionsForActor(); + + /** + * Array of potential interactable actors. Not replicated. + * 潜在可交互演员数组。未网络同步。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem") + TArray> InteractableActors; + + /** + * Number of potential interactable actors, replicated to owning client. + * 潜在可交互演员数量,同步到拥有客户端。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnInteractableActorsNumChanged, Category="GGS|InteractionSystem") + int32 NumsOfInteractableActors{0}; + + /** + * Current selected interactable actor, replicated for owner only. + * 当前选中的可交互演员,仅针对拥有者同步。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GGS|InteractionSystem", ReplicatedUsing=OnInteractableActorChanged) + TObjectPtr InteractableActor; + + /** + * Default filter for searching interactable smart objects. + * 搜索可交互智能对象的默认过滤器。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem") + FSmartObjectRequestFilter DefaultRequestFilter; + + /** + * If checked, whenever potential interactable actors changes, the first actor in the list will be selected as currency interactable actor. + * 如果勾选,始终使用潜在交互演员中的第一个作为当前选择。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnInteractableActorsNumChanged, Category="GGS|InteractionSystem") + bool bNewActorHasPriority{false}; + + /** + * Current available interaction options, replicated for owner only. + * 当前可用的交互选项,仅针对拥有者同步。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem", ReplicatedUsing=OnInteractionOptionsChanged) + TArray InteractionOptions; + + /** + * Indicates if an interaction is in progress. + * 表示是否正在进行交互。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem") + bool bInteracting{false}; + + /** + * Current interacting option index (-1 if no interaction). + * 当前交互选项索引(无交互时为-1)。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem", ReplicatedUsing=OnInteractingOptionChanged) + int32 InteractingOption{INDEX_NONE}; + + /** + * Map of smart object slot handles to delegate handles. + * 智能对象槽句柄到委托句柄的映射。 + */ + TMap SlotCallbacks; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_SmartObjectFunctionLibrary.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_SmartObjectFunctionLibrary.h new file mode 100644 index 0000000..477984e --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/GGS_SmartObjectFunctionLibrary.h @@ -0,0 +1,70 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGS_InteractionStructLibrary.h" +#include "SmartObjectSubsystem.h" +#include "SmartObjectTypes.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GGS_SmartObjectFunctionLibrary.generated.h" + +class UGameplayInteractionSmartObjectBehaviorDefinition; +class UGameplayBehaviorSmartObjectBehaviorDefinition; +class UGameplayBehaviorConfig; +class USmartObjectBehaviorDefinition; + +/** + * Blueprint function library for smart object interactions. + * 智能对象交互的蓝图函数库。 + */ +UCLASS() +class GENERICGAMESYSTEM_API UGGS_SmartObjectFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Retrieves the gameplay behavior config from a smart object behavior definition. + * 从智能对象行为定义获取游戏行为配置。 + * @param BehaviorDefinition The smart object behavior definition. 智能对象行为定义。 + * @return The gameplay behavior config. 游戏行为配置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGS|SmartObject") + static UGameplayBehaviorConfig* GetGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition); + + /** + * Finds a specific gameplay behavior config by class. + * 按类查找特定游戏行为配置。 + * @param BehaviorDefinition The smart object behavior definition. 智能对象行为定义。 + * @param DesiredClass The desired config class. 期望的配置类。 + * @param OutConfig The found config (output). 找到的配置(输出)。 + * @return True if the config was found, false otherwise. 如果找到配置返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GGS|SmartObject", meta=(DeterminesOutputType="DesiredClass", DynamicOutputParam="OutConfig", ExpandBoolAsExecs="ReturnValue")) + static bool FindGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition, TSubclassOf DesiredClass, UGameplayBehaviorConfig*& OutConfig); + + /** + * Searches for smart object slots with interaction entrances on an actor. + * 在演员上搜索带有交互入口的智能对象槽。 + * @param Filter The search filter. 搜索过滤器。 + * @param SearchActor The actor to search. 要搜索的演员。 + * @param OutResults The found smart object slot candidates (output). 找到的智能对象槽候选(输出)。 + * @param UserActor Optional actor for additional data in condition evaluation. 用于条件评估的可选演员。 + * @return True if at least one candidate was found, false otherwise. 如果找到至少一个候选返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = "GGS|SmartObject", Meta = (ReturnDisplayName = "bSuccess")) + static bool FindSmartObjectsWithInteractionEntranceInActor(const FSmartObjectRequestFilter& Filter, AActor* SearchActor, TArray& OutResults, + const AActor* UserActor = nullptr); + + /** + * Finds the interaction definition for a smart object slot. + * 查找智能对象槽的交互定义。 + * @param WorldContext The world context object. 世界上下文对象。 + * @param SmartObjectSlotHandle The smart object slot handle. 智能对象槽句柄。 + * @param OutDefinition The interaction definition (output). 交互定义(输出)。 + * @return True if the definition was found, false otherwise. 如果找到定义返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GGS|SmartObject", meta=(WorldContext="WorldContext", ExpandBoolAsExecs="ReturnValue")) + static bool FindInteractionDefinitionFromSmartObjectSlot(UObject* WorldContext, FSmartObjectSlotHandle SmartObjectSlotHandle, UGGS_InteractionDefinition*& OutDefinition); +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.h new file mode 100644 index 0000000..3d0de38 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.h @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h" +#include "GGS_TargetingFilterTask_InteractionSmartObjects.generated.h" + +/** + * Filter task for selecting interactable smart objects. + * 选择可交互智能对象的过滤任务。 + */ +UCLASS(meta=(DisplayName="(GGS)FilterTask:InteractionSmartObject")) +class GENERICGAMESYSTEM_API UGGS_TargetingFilterTask_InteractionSmartObjects : public UTargetingFilterTask_BasicFilterTemplate +{ + GENERATED_BODY() + +protected: + /** + * Determines if a target should be filtered based on interaction criteria. + * 根据交互标准确定是否过滤目标。 + * @param TargetingHandle The targeting request handle. 目标请求句柄。 + * @param TargetData The target data. 目标数据。 + * @return True if the target should be filtered, false otherwise. 如果目标应被过滤返回true,否则返回false。 + */ + virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.h b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.h new file mode 100644 index 0000000..bf6148f --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.h @@ -0,0 +1,124 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Abilities/Tasks/AbilityTask.h" +#include "SmartObjectRuntime.h" +#include "SmartObjectTypes.h" +#include "GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.generated.h" + +class UGameplayBehavior; + +/** + * Ability task for using a smart object with gameplay behavior. + * 使用智能对象和游戏行为的技能任务。 + */ +UCLASS() +class GENERICGAMESYSTEM_API UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior : public UAbilityTask +{ + GENERATED_BODY() + +public: + /** + * Constructor for the ability task. + * 技能任务构造函数。 + */ + UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Creates an ability task to use a smart object with gameplay behavior. + * 创建使用智能对象和游戏行为的技能任务。 + * @param OwningAbility The owning gameplay ability. 拥有的游戏技能。 + * @param ClaimHandle The smart object claim handle. 智能对象认领句柄。 + * @param ClaimPriority The claim priority. 认领优先级。 + * @return The created ability task. 创建的技能任务。 + */ + UFUNCTION(BlueprintCallable, Category = "GGS|Interaction", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* UseSmartObjectWithGameplayBehavior(UGameplayAbility* OwningAbility, FSmartObjectClaimHandle ClaimHandle, + ESmartObjectClaimPriority ClaimPriority = ESmartObjectClaimPriority::Normal); + + /** + * Sets the smart object claim handle. + * 设置智能对象认领句柄。 + * @param Handle The claim handle. 认领句柄。 + */ + void SetClaimHandle(const FSmartObjectClaimHandle& Handle) { ClaimedHandle = Handle; } + +protected: + /** + * Activates the ability task. + * 激活技能任务。 + */ + virtual void Activate() override; + + /** + * Called when the task is destroyed. + * 任务销毁时调用。 + * @param bInOwnerFinished Whether the owner finished the task. 拥有者是否完成任务。 + */ + virtual void OnDestroy(bool bInOwnerFinished) override; + + /** + * Starts the interaction with the smart object. + * 开始与智能对象的交互。 + * @return True if the interaction started successfully, false otherwise. 如果交互成功开始返回true,否则返回false。 + */ + bool StartInteraction(); + + /** + * Called when the smart object behavior finishes. + * 智能对象行为完成时调用。 + * @param Behavior The gameplay behavior. 游戏行为。 + * @param Avatar The avatar actor. 化身演员。 + * @param bInterrupted Whether the behavior was interrupted. 行为是否被中断。 + */ + void OnSmartObjectBehaviorFinished(UGameplayBehavior& Behavior, AActor& Avatar, const bool bInterrupted); + + /** + * Called when the smart object slot is invalidated. + * 智能对象槽失效时调用。 + * @param ClaimHandle The claim handle. 认领句柄。 + * @param State The slot state. 槽状态。 + */ + void OnSlotInvalidated(const FSmartObjectClaimHandle& ClaimHandle, const ESmartObjectSlotState State); + + /** + * Delegate for when the interaction succeeds. + * 交互成功时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGenericGameplayTaskDelegate OnSucceeded; + + /** + * Delegate for when the interaction fails. + * 交互失败时的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGenericGameplayTaskDelegate OnFailed; + + /** + * The gameplay behavior for the interaction. + * 交互的游戏行为。 + */ + UPROPERTY() + TObjectPtr GameplayBehavior; + + /** + * The claimed smart object handle. + * 认领的智能对象句柄。 + */ + FSmartObjectClaimHandle ClaimedHandle; + + /** + * Delegate handle for behavior finished notification. + * 行为完成通知的委托句柄。 + */ + FDelegateHandle OnBehaviorFinishedNotifyHandle; + + /** + * Indicates if the behavior has finished. + * 表示行为是否已完成。 + */ + bool bBehaviorFinished; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollComponent.h b/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollComponent.h new file mode 100644 index 0000000..a7e1a06 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollComponent.h @@ -0,0 +1,148 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GGS_RagdollStructLibrary.h" +#include "Components/PawnComponent.h" + +#include "GGS_RagdollComponent.generated.h" + +class USkeletalMeshComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGGS_RagdollStartedSignature); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGGS_RagdollEndedSignature, bool, bGrounded); + +UCLASS(ClassGroup=(GGS), Blueprintable, meta=(BlueprintSpawnableComponent)) +class GENERICGAMESYSTEM_API UGGS_RagdollComponent : public UPawnComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UGGS_RagdollComponent(const FObjectInitializer& ObjectInitializer); + + + const FGGS_RagdollState& GetRagdollState() const; + + bool IsRagdollAllowedToStart() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GGS|Ragdoll") + void SetMeshComponent(USkeletalMeshComponent* InMeshComponent); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GGS|Ragdoll") + bool IsRagdolling() const; + + UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll") + void StartRagdoll(); + + UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll") + virtual void LocalStartRagdoll(); + + UPROPERTY(BlueprintAssignable, Category="Event") + FGGS_RagdollStartedSignature OnRagdollStartedEvent; + UPROPERTY(BlueprintAssignable, Category="Event") + FGGS_RagdollEndedSignature OnRagdollEndedEvent; + +private: + UFUNCTION(Server, Reliable) + void ServerStartRagdoll(); + + UFUNCTION(NetMulticast, Reliable) + void MulticastStartRagdoll(); + +protected: + UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll") + void OnRagdollStarted(); + +public: + bool IsRagdollAllowedToStop() const; + + UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll", Meta = (ReturnDisplayName = "Success")) + bool StopRagdoll(); + + UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll") + virtual void LocalStopRagdoll(); + +private: + UFUNCTION(Server, Reliable) + void ServerStopRagdoll(); + + UFUNCTION(NetMulticast, Reliable) + void MulticastStopRagdoll(); + +protected: + UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll") + UAnimMontage* SelectGetUpMontage(bool bRagdollFacingUpward); + + UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll") + void OnRagdollEnded(bool bGrounded); + virtual void OnRagdollEnded_Implementation(bool bGrounded); + +private: + void SetRagdollTargetLocation(const FVector& NewTargetLocation); + + UFUNCTION(Server, Unreliable) + void ServerSetRagdollTargetLocation(const FVector_NetQuantize& NewTargetLocation); + + void RefreshRagdoll(float DeltaTime); + + FVector RagdollTraceGround(bool& bGrounded) const; + + void ConstraintRagdollSpeed() const; + +protected: + // Called when the game starts + virtual void BeginPlay() override; + + float DamperExactAlpha(float DeltaTime, float HalfLife); + + float Clamp01(float Value); + + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ragdoll Settings") + FName PelvisBoneName{TEXT("pelvis")}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ragdoll Settings") + FName SpineBoneName{TEXT("spine_03")}; + + // If checked, the ragdoll's speed will be limited by the character's last speed for a few frames + // after activation. This hack is used to prevent the ragdoll from getting a very high initial speed + // at unstable FPS, which can be reproduced by jumping and activating the ragdoll at the same time. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings") + uint8 bLimitInitialRagdollSpeed : 1 {true}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings") + bool bPlayGetupMontageAfterRagdollEndedOnGround{false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings", meta=(EditCondition="bPlayGetupMontageAfterRagdollEndedOnGround")) + TSoftObjectPtr GetUpFrontMontage; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings", meta=(EditCondition="bPlayGetupMontageAfterRagdollEndedOnGround")) + TSoftObjectPtr GetUpBackMontage; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient) + bool bRagdolling{false}; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient, Replicated) + FVector_NetQuantize RagdollTargetLocation{ForceInit}; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient) + FGGS_RagdollState RagdollState; + + UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient) + TObjectPtr MeshComponent{nullptr}; + + UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient) + TObjectPtr PawnOwner{nullptr}; + + UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient) + TObjectPtr CharacterOwner{nullptr}; + +public: + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + // Called every frame + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollStructLibrary.h b/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollStructLibrary.h new file mode 100644 index 0000000..0b9e808 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Ragdoll/GGS_RagdollStructLibrary.h @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Animation/PoseSnapshot.h" +#include "GGS_RagdollStructLibrary.generated.h" + +USTRUCT(BlueprintType) +struct GENERICGAMESYSTEM_API FGGS_RagdollState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS") + FVector Velocity{ForceInit}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ForceUnits = "N")) + float PullForce{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ClampMin = 0)) + int32 SpeedLimitFrameTimeRemaining{0}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float SpeedLimit{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS") + FPoseSnapshot FinalRagdollPose; +}; diff --git a/Plugins/GGS/Source/GenericGameSystem/Public/Utilities/GGS_SocketRelationshipMapping.h b/Plugins/GGS/Source/GenericGameSystem/Public/Utilities/GGS_SocketRelationshipMapping.h new file mode 100644 index 0000000..ada8230 --- /dev/null +++ b/Plugins/GGS/Source/GenericGameSystem/Public/Utilities/GGS_SocketRelationshipMapping.h @@ -0,0 +1,135 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "GGS_SocketRelationshipMapping.generated.h" + +/** + * Structure for socket adjustments. + * 插槽调整结构。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMESYSTEM_API FGGS_SocketAdjustment +{ + GENERATED_BODY() + + /** + * Array of skeleton names for the adjustment. + * 调整适用的骨骼名称数组。 + */ + UPROPERTY(EditAnywhere, Category="GGS") + TArray ForSkeletons; + + /** + * Name of the socket. + * 插槽名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + FName SocketName{NAME_None}; + + /** + * Relative transform for the socket. + * 插槽的相对变换。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + FTransform RelativeTransform; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the adjustment. + * 调整的编辑器友好名称。 + */ + UPROPERTY(EditAnywhere, Category="GGS", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Structure for socket relationships. + * 插槽关系结构。 + */ +USTRUCT(BlueprintType) +struct GENERICGAMESYSTEM_API FGGS_SocketRelationship +{ + GENERATED_BODY() + + /** + * Mesh asset associated with the relationship. + * 与关系关联的网格资产。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS") + TSoftObjectPtr MeshAsset; + + /** + * Array of socket adjustments for the mesh. + * 网格的插槽调整数组。 + * @note Will look from bottom to top; 从下往上查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS", meta=(TitleProperty="EditorFriendlyName")) + TArray Adjustments; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the relationship. + * 关系的编辑器友好名称。 + */ + UPROPERTY(EditAnywhere, Category="GGS", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Data asset for defining socket relationships for mesh attachments. + * 定义网格附件插槽关系的数据资产。 + */ +UCLASS(BlueprintType) +class GENERICGAMESYSTEM_API UGGS_SocketRelationshipMapping : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Finds a socket adjustment for a given mesh and socket. + * 查找给定网格和插槽的插槽调整。 + * @param InParentMeshComponent The parent mesh component. 父网格组件。 + * @param InMeshAsset The mesh asset. 网格资产。 + * @param InSocketName The socket name. 插槽名称。 + * @param OutAdjustment The found socket adjustment (output). 找到的插槽调整(输出)。 + * @return True if an adjustment was found, false otherwise. 如果找到调整返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=False, Category="GGS|Utilities") + bool FindSocketAdjustment(const USkeletalMeshComponent* InParentMeshComponent, const UStreamableRenderAsset* InMeshAsset, FName InSocketName, + FGGS_SocketAdjustment& OutAdjustment) const; + + /** + * Finds a socket adjustment across multiple mappings. + * 在多个映射中查找插槽调整。 + * @param InMappings The socket relationship mappings. 插槽关系映射。 + * @param InParentMeshComponent The parent mesh component. 父网格组件。 + * @param InMeshAsset The mesh asset. 网格资产。 + * @param InSocketName The socket name. 插槽名称。 + * @param OutAdjustment The found socket adjustment (output). 找到的插槽调整(输出)。 + * @return True if an adjustment was found, false otherwise. 如果找到调整返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GGS|Utilities") + static bool FindSocketAdjustmentInMappings(TArray> InMappings, const USkeletalMeshComponent* InParentMeshComponent, + const UStreamableRenderAsset* InMeshAsset, FName InSocketName, + FGGS_SocketAdjustment& OutAdjustment); + + /** + * Array of socket relationships. + * 插槽关系数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS", meta=(TitleProperty="EditorFriendlyName")) + TArray Relationships; + +#if WITH_EDITORONLY_DATA + /** + * Pre-save processing for editor. + * 编辑器预保存处理。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericUISystem/GenericUISystem.Build.cs b/Plugins/GGS/Source/GenericUISystem/GenericUISystem.Build.cs new file mode 100644 index 0000000..b7a3ccd --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/GenericUISystem.Build.cs @@ -0,0 +1,39 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +using UnrealBuildTool; + +public class GenericUISystem : ModuleRules +{ + public GenericUISystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CommonUI" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "ApplicationCore", + "EnhancedInput", + "PropertyPath", + "GameplayTags", + "UMG", + "InputCore", + "CommonInput", + "DeveloperSettings", + "ModularGameplay" + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Private/GUIS_GenericUISystemSettings.cpp b/Plugins/GGS/Source/GenericUISystem/Private/GUIS_GenericUISystemSettings.cpp new file mode 100644 index 0000000..da46c39 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/GUIS_GenericUISystemSettings.cpp @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GUIS_GenericUISystemSettings.h" + +const UGUIS_GenericUISystemSettings* UGUIS_GenericUISystemSettings::Get() +{ + return GetDefault(); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/GUIS_LogChannels.cpp b/Plugins/GGS/Source/GenericUISystem/Private/GUIS_LogChannels.cpp new file mode 100644 index 0000000..4ef7367 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/GUIS_LogChannels.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GUIS_LogChannels.h" + +DEFINE_LOG_CATEGORY(LogGUIS) +DEFINE_LOG_CATEGORY(LogGUIS_Extension); diff --git a/Plugins/GGS/Source/GenericUISystem/Private/GenericUISystem.cpp b/Plugins/GGS/Source/GenericUISystem/Private/GenericUISystem.cpp new file mode 100644 index 0000000..5ad7107 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/GenericUISystem.cpp @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericUISystem.h" + +#define LOCTEXT_NAMESPACE "FGenericUISystemModule" + +void FGenericUISystemModule::StartupModule() +{ + +} + +void FGenericUISystemModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericUISystemModule, GenericUISystem) \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_CreateWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_CreateWidget.cpp new file mode 100644 index 0000000..68abd6a --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_CreateWidget.cpp @@ -0,0 +1,97 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Actions/GUIS_AsyncAction_CreateWidget.h" + +#include "Blueprint/UserWidget.h" +#include "Blueprint/WidgetBlueprintLibrary.h" +#include "Engine/AssetManager.h" +#include "Engine/Engine.h" +#include "Engine/GameInstance.h" +#include "Engine/StreamableManager.h" +#include "UI/GUIS_GameUIFunctionLibrary.h" +#include "UObject/Stack.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_CreateWidget) + +class UUserWidget; + +static const FName InputFilterReason_Template = FName(TEXT("CreatingWidgetAsync")); + +UGUIS_AsyncAction_CreateWidget::UGUIS_AsyncAction_CreateWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bSuspendInputUntilComplete(true) +{ +} + +UGUIS_AsyncAction_CreateWidget* UGUIS_AsyncAction_CreateWidget::CreateWidgetAsync(UObject* InWorldContextObject, TSoftClassPtr InUserWidgetSoftClass, APlayerController* InOwningPlayer, + bool bSuspendInputUntilComplete) +{ + if (InUserWidgetSoftClass.IsNull()) + { + FFrame::KismetExecutionMessage(TEXT("CreateWidgetAsync was passed a null UserWidgetSoftClass"), ELogVerbosity::Error); + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(InWorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + + UGUIS_AsyncAction_CreateWidget* Action = NewObject(); + Action->UserWidgetSoftClass = InUserWidgetSoftClass; + Action->OwningPlayer = InOwningPlayer; + Action->World = World; + Action->GameInstance = World->GetGameInstance(); + Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete; + Action->RegisterWithGameInstance(World); + + return Action; +} + +void UGUIS_AsyncAction_CreateWidget::Activate() +{ + SuspendInputToken = bSuspendInputUntilComplete ? UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(OwningPlayer.Get(), InputFilterReason_Template) : NAME_None; + + TWeakObjectPtr LocalWeakThis(this); + StreamingHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad( + UserWidgetSoftClass.ToSoftObjectPath(), + FStreamableDelegate::CreateUObject(this, &ThisClass::OnWidgetLoaded), + FStreamableManager::AsyncLoadHighPriority + ); + + // Setup a cancel delegate so that we can resume input if this handler is canceled. + StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this, + [this]() + { + UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken); + }) + ); +} + +void UGUIS_AsyncAction_CreateWidget::Cancel() +{ + Super::Cancel(); + + if (StreamingHandle.IsValid()) + { + StreamingHandle->CancelHandle(); + StreamingHandle.Reset(); + } +} + +void UGUIS_AsyncAction_CreateWidget::OnWidgetLoaded() +{ + if (bSuspendInputUntilComplete) + { + UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken); + } + + // If the load as successful, create it, otherwise don't complete this. + TSubclassOf UserWidgetClass = UserWidgetSoftClass.Get(); + if (UserWidgetClass) + { + UUserWidget* UserWidget = UWidgetBlueprintLibrary::Create(World.Get(), UserWidgetClass, OwningPlayer.Get()); + OnComplete.Broadcast(UserWidget); + } + + StreamingHandle.Reset(); + + SetReadyToDestroy(); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.cpp new file mode 100644 index 0000000..266922c --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.cpp @@ -0,0 +1,130 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h" +#include "Engine/Engine.h" +#include "UI/GUIS_GameUILayout.h" +#include "UObject/Stack.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_PushContentToUILayer) + +UGUIS_AsyncAction_PushContentToUILayer::UGUIS_AsyncAction_PushContentToUILayer(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UGUIS_AsyncAction_PushContentToUILayer* UGUIS_AsyncAction_PushContentToUILayer::PushContentToUILayer(UGUIS_GameUILayout* UILayout, + TSoftClassPtr InWidgetClass, FGameplayTag InLayerName, + bool bSuspendInputUntilComplete) +{ + if (!IsValid(UILayout)) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a invalid Layout"), ELogVerbosity::Error); + return nullptr; + } + if (InWidgetClass.IsNull()) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a null WidgetClass"), ELogVerbosity::Error); + return nullptr; + } + + if (UWorld* World = GEngine->GetWorldFromContextObject(UILayout->GetWorld(), EGetWorldErrorMode::LogAndReturnNull)) + { + UGUIS_AsyncAction_PushContentToUILayer* Action = NewObject(); + Action->WidgetClass = InWidgetClass; + Action->RootLayout = UILayout; + Action->OwningPlayerPtr = UILayout->GetOwningPlayer(); + Action->LayerName = InLayerName; + Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete; + Action->RegisterWithGameInstance(World); + + return Action; + } + + return nullptr; +} + +UGUIS_AsyncAction_PushContentToUILayer* UGUIS_AsyncAction_PushContentToUILayer::PushContentToUILayerForPlayer(APlayerController* PlayerController, + TSoftClassPtr InWidgetClass, + FGameplayTag InLayerName, bool bSuspendInputUntilComplete) +{ + if (!IsValid(PlayerController)) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayerForPlayer was passed a invalid PlayerController"), ELogVerbosity::Error); + return nullptr; + } + if (InWidgetClass.IsNull()) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a null WidgetClass"), ELogVerbosity::Error); + return nullptr; + } + + UGUIS_GameUILayout* UILayout = UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(PlayerController); + + if (UILayout == nullptr) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayerForPlayer failed to find UILayout for player."), ELogVerbosity::Error); + return nullptr; + } + + if (UWorld* World = GEngine->GetWorldFromContextObject(UILayout->GetWorld(), EGetWorldErrorMode::LogAndReturnNull)) + { + UGUIS_AsyncAction_PushContentToUILayer* Action = NewObject(); + Action->WidgetClass = InWidgetClass; + Action->RootLayout = UILayout; + Action->OwningPlayerPtr = PlayerController; + Action->LayerName = InLayerName; + Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete; + Action->RegisterWithGameInstance(World); + + return Action; + } + + return nullptr; +} + + +void UGUIS_AsyncAction_PushContentToUILayer::Cancel() +{ + Super::Cancel(); + + if (StreamingHandle.IsValid()) + { + StreamingHandle->CancelHandle(); + StreamingHandle.Reset(); + } +} + +void UGUIS_AsyncAction_PushContentToUILayer::Activate() +{ + // if (UGUIS_GameUILayout* RootLayout = UGUIS_GameUILayout::GetPrimaryGameLayout(OwningPlayerPtr.Get())) + if (RootLayout.IsValid()) + { + TWeakObjectPtr WeakThis = this; + StreamingHandle = RootLayout->PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, WidgetClass, + [this, WeakThis](EGUIS_AsyncWidgetLayerState State, UCommonActivatableWidget* Widget) + { + if (WeakThis.IsValid()) + { + switch (State) + { + case EGUIS_AsyncWidgetLayerState::Initialize: + BeforePush.Broadcast(Widget); + break; + case EGUIS_AsyncWidgetLayerState::AfterPush: + AfterPush.Broadcast(Widget); + SetReadyToDestroy(); + break; + case EGUIS_AsyncWidgetLayerState::Canceled: + SetReadyToDestroy(); + break; + } + } + SetReadyToDestroy(); + }); + } + else + { + SetReadyToDestroy(); + } +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_ShowModel.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_ShowModel.cpp new file mode 100644 index 0000000..a42e845 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_AsyncAction_ShowModel.cpp @@ -0,0 +1,111 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Actions/GUIS_AsyncAction_ShowModel.h" +#include "GUIS_GenericUISystemSettings.h" +#include "Engine/GameInstance.h" +#include "UI/GUIS_GameUIFunctionLibrary.h" +#include "UI/GUIS_GameUILayout.h" +#include "UI/GUIS_GameplayTags.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_ShowModel) + +UGUIS_AsyncAction_ShowModel::UGUIS_AsyncAction_ShowModel(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +// UGUIS_AsyncAction_ShowModel* UGUIS_AsyncAction_ShowModel::ShowModal(UObject* InWorldContextObject, FGameplayTag ModalTag, UGUIS_ModalDefinition* ModalDefinition) +// { +// const UGUIS_GameUIData* UIData = UGUIS_GenericUISystemSettings::GetGameUIData(); +// if (!UIData) +// return nullptr; +// +// const TSoftClassPtr SoftModalWidgetClass = UIData->FindWidgetClassForModal(ModalTag); +// if (SoftModalWidgetClass.IsNull()) +// return nullptr; +// const TSubclassOf ModalWidgetClass = SoftModalWidgetClass.LoadSynchronous(); +// if (ModalWidgetClass == nullptr) +// return nullptr; +// +// UGUIS_AsyncAction_ShowModel* Action = NewObject(); +// Action->ModalWidgetClass = ModalWidgetClass; +// Action->WorldContextObject = InWorldContextObject; +// Action->ModalDefinition = ModalDefinition; +// Action->RegisterWithGameInstance(InWorldContextObject); +// +// return Action; +// } + +UGUIS_AsyncAction_ShowModel* UGUIS_AsyncAction_ShowModel::ShowModal(UObject* InWorldContextObject, TSoftClassPtr ModalDefinition) +{ + if (ModalDefinition.IsNull()) + { + return nullptr; + } + + ModalDefinition.LoadSynchronous(); + + const UGUIS_ModalDefinition* Modal = ModalDefinition->GetDefaultObject(); + if (Modal == nullptr) + return nullptr; + + if (Modal->ModalWidget.IsNull()) + return nullptr; + + const TSubclassOf ModalWidgetClass = Modal->ModalWidget.LoadSynchronous(); + if (ModalWidgetClass == nullptr) + return nullptr; + + UGUIS_AsyncAction_ShowModel* Action = NewObject(); + Action->ModalWidgetClass = ModalWidgetClass; + Action->WorldContextObject = InWorldContextObject; + Action->ModalDefinition = Modal; + Action->RegisterWithGameInstance(InWorldContextObject); + + return Action; +} + +void UGUIS_AsyncAction_ShowModel::Activate() +{ + if (WorldContextObject && !TargetPlayerController) + { + if (UUserWidget* UserWidget = Cast(WorldContextObject)) + { + TargetPlayerController = UserWidget->GetOwningPlayer(); + } + else if (APlayerController* PC = Cast(WorldContextObject)) + { + TargetPlayerController = PC; + } + else if (UWorld* World = WorldContextObject->GetWorld()) + { + if (UGameInstance* GameInstance = World->GetGameInstance()) + { + TargetPlayerController = GameInstance->GetPrimaryPlayerController(false); + } + } + } + + if (TargetPlayerController) + { + if (UGUIS_GameUILayout* Layout = UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(TargetPlayerController)) + { + FGUIS_ModalActionResultSignature ResultCallback = FGUIS_ModalActionResultSignature::CreateUObject(this, &UGUIS_AsyncAction_ShowModel::HandleModalAction); + const UGUIS_ModalDefinition* TempDescriptor = ModalDefinition; + Layout->PushWidgetToLayerStack(GUIS_GameUILayerTags::Modal, ModalWidgetClass, [TempDescriptor, ResultCallback](UGUIS_GameModalWidget& ModalInstance) + { + ModalInstance.SetupModal(TempDescriptor, ResultCallback); + }); + } + } + + // If we couldn't make the confirmation, just handle an unknown result and broadcast nothing + HandleModalAction(GUIS_GameModalActionTags::Unknown); +} + + +void UGUIS_AsyncAction_ShowModel::HandleModalAction(FGameplayTag ModalActionTag) +{ + OnModalAction.Broadcast(ModalActionTag); + SetReadyToDestroy(); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIAction.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIAction.cpp new file mode 100644 index 0000000..224d789 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIAction.cpp @@ -0,0 +1,62 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Actions/GUIS_UIAction.h" + + +UGUIS_UIAction::UGUIS_UIAction(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +} + +bool UGUIS_UIAction::IsCompatible(const UObject* Data) const +{ + return IsCompatibleInternal(Data); +} + + +bool UGUIS_UIAction::CanInvokeInternal_Implementation(const UObject* Data, APlayerController* PlayerController) const +{ + // 与其他验证不同, 这个默认不通过, Override里修改 + return false; +} + +bool UGUIS_UIAction::CanInvoke(const UObject* Data, APlayerController* PlayerController) const +{ + return CanInvokeInternal(Data, PlayerController); +} + +void UGUIS_UIAction::InvokeAction(const UObject* Data, APlayerController* PlayerController) const +{ + // if (CanInvoke(Data, User)) + { + InvokeActionInternal(Data, PlayerController); + } +} + +FText UGUIS_UIAction::GetActionName() const +{ + return DisplayName; +} + +FName UGUIS_UIAction::GetActionID() const +{ + return ActionID; +} + +bool UGUIS_UIAction::IsCompatibleInternal_Implementation(const UObject* Data) const +{ + return true; +} + +void UGUIS_UIAction::InvokeActionInternal_Implementation(const UObject* Data, APlayerController* PlayerController) const +{ +} + +UWorld* UGUIS_UIAction::GetWorld() const +{ + if (UObject* Outer = GetOuter()) + { + return Outer->GetWorld(); + } + return nullptr; +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionFactory.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionFactory.cpp new file mode 100644 index 0000000..6de34b4 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionFactory.cpp @@ -0,0 +1,36 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Actions/GUIS_UIActionFactory.h" +#include "Misc/DataValidation.h" +#include "UI/Actions/GUIS_UIAction.h" + +TArray UGUIS_UIActionFactory::FindAvailableUIActionsForData(const UObject* Data) const +{ + TArray Ret; + for (UGUIS_UIAction* Action : PotentialActions) + { + if (Action != nullptr && Action->IsCompatible(Data)) + { + Ret.Add(Action); + } + } + return Ret; +} + + +#if WITH_EDITOR +EDataValidationResult UGUIS_UIActionFactory::IsDataValid(FDataValidationContext& Context) const +{ + FText ValidationMessage; + for (int32 i = 0; i < PotentialActions.Num(); i++) + { + if (PotentialActions[0] == nullptr) + { + Context.AddError(FText::FromString(FString::Format(TEXT("Invalid action on index:{0}"), {i}))); + return EDataValidationResult::Invalid; + } + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionWidget.cpp new file mode 100644 index 0000000..643473b --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Actions/GUIS_UIActionWidget.cpp @@ -0,0 +1,139 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Actions/GUIS_UIActionWidget.h" + +#include "GUIS_LogChannels.h" +#include "Input/CommonUIInputTypes.h" +#include "UI/GUIS_GameplayTags.h" +#include "UI/Actions/GUIS_AsyncAction_ShowModel.h" +#include "UI/Actions/GUIS_UIActionFactory.h" + +void UGUIS_UIActionWidget::SetAssociatedData(UObject* Data) +{ + if (Data == nullptr) + { + UnregisterActions(); + } + AssociatedData = Data; +} + +void UGUIS_UIActionWidget::RegisterActions() +{ + if (!AssociatedData.IsValid()) + { + return; + } + + if (!IsValid(ActionFactory)) + { + return; + } + + TArray Actions = ActionFactory->FindAvailableUIActionsForData(AssociatedData.Get()); + + for (const UGUIS_UIAction* Action : Actions) + { + if (Action->CanInvoke(AssociatedData.Get(), GetOwningPlayer())) + { + FBindUIActionArgs BindArgs(Action->GetInputActionData(), Action->GetShouldDisplayInActionBar(), + FSimpleDelegate::CreateLambda([this,Action]() + { + HandleUIAction(Action); + })); + + ActionBindings.Add(RegisterUIActionBinding(BindArgs)); + } + } +} + +void UGUIS_UIActionWidget::RegisterActionsWithFactory(TSoftObjectPtr InActionFactory) +{ + if (InActionFactory.IsNull()) + { + UE_LOG(LogGUIS, Warning, TEXT("Passed invalid action factory!")) + return; + } + + UGUIS_UIActionFactory* Factory = InActionFactory.LoadSynchronous(); + + if (Factory == nullptr) + { + UE_LOG(LogGUIS, Warning, TEXT("Failed to load action factory!")) + return; + } + + ActionFactory = Factory; + + RegisterActions(); +} + +void UGUIS_UIActionWidget::UnregisterActions() +{ + for (FUIActionBindingHandle& ActionBinding : ActionBindings) + { + ActionBinding.Unregister(); + } + + ActionBindings.Empty(); + CancelAction(); +} + +void UGUIS_UIActionWidget::CancelAction() +{ + if (ModalTask) + { + ModalTask->OnModalAction.RemoveDynamic(this, &ThisClass::HandleModalAction); + ModalTask->Cancel(); + ModalTask = nullptr; + } + CurrentAction = nullptr; +} + +#if WITH_EDITOR +const FText UGUIS_UIActionWidget::GetPaletteCategory() +{ + return FText::FromString(TEXT("Generic UI")); +} +#endif + +void UGUIS_UIActionWidget::HandleUIAction(const UGUIS_UIAction* Action) +{ + if (ModalTask && ModalTask->IsActive()) + { + return; + } + if (AssociatedData.IsValid()) + { + if (Action->GetRequiresConfirmation() && !Action->GetConfirmationModalClass().IsNull()) + { + ModalTask = UGUIS_AsyncAction_ShowModel::ShowModal(GetWorld(), Action->GetConfirmationModalClass()); + CurrentAction = Action; + ModalTask->OnModalAction.AddDynamic(this, &ThisClass::HandleModalAction); + ModalTask->Activate(); + } + else + { + if (Action->CanInvoke(AssociatedData.Get(), GetOwningPlayer())) + { + Action->InvokeAction(AssociatedData.Get(), GetOwningPlayer()); + } + } + } +} + +void UGUIS_UIActionWidget::HandleModalAction(FGameplayTag ActionTag) +{ + if (ActionTag == GUIS_GameModalActionTags::Yes || ActionTag == GUIS_GameModalActionTags::Ok) + { + if (CurrentAction && CurrentAction->CanInvoke(AssociatedData.Get(), GetOwningPlayer())) + { + CurrentAction->InvokeAction(AssociatedData.Get(), GetOwningPlayer()); + } + CancelAction(); + } + if (ActionTag == GUIS_GameModalActionTags::No) + { + CancelAction(); + } +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_DetailSectionBuilder.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_DetailSectionBuilder.cpp new file mode 100644 index 0000000..eec8837 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_DetailSectionBuilder.cpp @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_DetailSectionsBuilder.h" + +TArray> UGUIS_DetailSectionsBuilder::GatherDetailSections_Implementation(const UObject* Data) +{ + TArray> Sections; + return Sections; +} + + +TArray> UGUIS_DetailSectionBuilder_Class::GatherDetailSections_Implementation(const UObject* Data) +{ + TArray> Sections; + + // Find extensions for it using the super chain of the setting so that we get any + // class based extensions for this setting. + for (UClass* Class = Data->GetClass(); Class; Class = Class->GetSuperClass()) + { + FGUIS_EntryDetailsClassSections* ExtensionForClass = SectionsForClasses.Find(Class); + if (ExtensionForClass) + { + Sections.Append(ExtensionForClass->Sections); + } + } + + return Sections; +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntry.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntry.cpp new file mode 100644 index 0000000..5b1db27 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntry.cpp @@ -0,0 +1,5 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_ListEntry.h" + diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailSection.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailSection.cpp new file mode 100644 index 0000000..c87698e --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailSection.cpp @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_ListEntryDetailSection.h" + + +void UGUIS_ListEntryDetailSection::SetListItemObject(UObject* ListItemObject) +{ + NativeOnListItemObjectSet(ListItemObject); +} + +void UGUIS_ListEntryDetailSection::NativeOnListItemObjectSet(UObject* ListItemObject) +{ + OnListItemObjectSet(ListItemObject); +} + diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailView.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailView.cpp new file mode 100644 index 0000000..cef8d9f --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListEntryDetailView.cpp @@ -0,0 +1,136 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_ListEntryDetailView.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Engine/AssetManager.h" +#include "Engine/StreamableManager.h" +#include "UI/Common/GUIS_ListEntryDetailSection.h" +#include "UI/Common/GUIS_DetailSectionsBuilder.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_ListEntryDetailView) + +#define LOCTEXT_NAMESPACE "EntryDetailsView" + +UGUIS_ListEntryDetailView::UGUIS_ListEntryDetailView(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , ExtensionWidgetPool(*this) +{ +} + +void UGUIS_ListEntryDetailView::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + + ExtensionWidgetPool.ReleaseAllSlateResources(); +} + +void UGUIS_ListEntryDetailView::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + if (!IsDesignTime()) + { + SetListItemObject(nullptr); + } +} + +void UGUIS_ListEntryDetailView::NativeConstruct() +{ + Super::NativeConstruct(); +} + +void UGUIS_ListEntryDetailView::SetListItemObject(UObject* InListItemObject) +{ + // Ignore requests to show the same setting multiple times in a row. + if (InListItemObject && InListItemObject == CurrentListItemObject) + { + return; + } + + CurrentListItemObject = InListItemObject; + + if (Box_DetailSections) + { + // First release the widgets back into the pool. + for (UWidget* ChildExtension : Box_DetailSections->GetAllChildren()) + { + ExtensionWidgetPool.Release(Cast(ChildExtension)); + } + + // Remove the widgets from their container. + Box_DetailSections->ClearChildren(); + + if (InListItemObject) + { + TArray> SectionClasses; + if (SectionsBuilder) + { + SectionClasses = SectionsBuilder->GatherDetailSections(InListItemObject); + } + + if (StreamingHandle.IsValid()) + { + StreamingHandle->CancelHandle(); + } + + bool bEverythingAlreadyLoaded = true; + + TArray SectionPaths; + SectionPaths.Reserve(SectionClasses.Num()); + for (TSoftClassPtr SoftClassPtr : SectionClasses) + { + bEverythingAlreadyLoaded &= SoftClassPtr.IsValid(); + SectionPaths.Add(SoftClassPtr.ToSoftObjectPath()); + } + + if (bEverythingAlreadyLoaded) + { + for (TSoftClassPtr SoftClassPtr : SectionClasses) + { + CreateDetailsExtension(InListItemObject, SoftClassPtr.Get()); + } + + ExtensionWidgetPool.ReleaseInactiveSlateResources(); + } + else + { + TWeakObjectPtr SettingPtr = InListItemObject; + + StreamingHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad( + MoveTemp(SectionPaths), + FStreamableDelegate::CreateWeakLambda(this, [this, SettingPtr, SectionClasses] + { + for (TSoftClassPtr SoftClassPtr : SectionClasses) + { + CreateDetailsExtension(SettingPtr.Get(), SoftClassPtr.Get()); + } + + ExtensionWidgetPool.ReleaseInactiveSlateResources(); + } + )); + } + } + } +} + +void UGUIS_ListEntryDetailView::SetSectionsBuilder(UGUIS_DetailSectionsBuilder* NewBuilder) +{ + SectionsBuilder = NewBuilder; +} + +void UGUIS_ListEntryDetailView::CreateDetailsExtension(UObject* InData, TSubclassOf SectionClass) +{ + if (InData && SectionClass) + { + if (UGUIS_ListEntryDetailSection* Section = ExtensionWidgetPool.GetOrCreateInstance(SectionClass)) + { + Section->SetListItemObject(InData); + UVerticalBoxSlot* ExtensionSlot = Box_DetailSections->AddChildToVerticalBox(Section); + ExtensionSlot->SetHorizontalAlignment(HAlign_Fill); + } + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListView.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListView.cpp new file mode 100644 index 0000000..1e2a05d --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_ListView.cpp @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_ListView.h" + +#include "UI/Common/GUIS_WidgetFactory.h" + +#if WITH_EDITOR +#include "Editor/WidgetCompilerLog.h" +#endif + + +UGUIS_ListView::UGUIS_ListView(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +} + +#if WITH_EDITOR +void UGUIS_ListView::ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const +{ + Super::ValidateCompiledDefaults(InCompileLog); + // if (EntryWidgetFactories.Num() == 0) + // { + // InCompileLog.Error(FText::Format(FText::FromString("{0} has no Entry widget Factories defined, can't create widgets without them."), FText::FromString(GetName()))); + // } +} +#endif + +void UGUIS_ListView::SetEntryWidgetFactories(TArray NewFactories) +{ + EntryWidgetFactories = NewFactories; +} + +UUserWidget& UGUIS_ListView::OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf DesiredEntryClass, const TSharedRef& OwnerTable) +{ + TSubclassOf WidgetClass = DesiredEntryClass; + + for (const UGUIS_WidgetFactory* Factory : EntryWidgetFactories) + { + if (Factory) + { + if (const TSubclassOf EntryClass = Factory->FindWidgetClassForData(Item)) + { + WidgetClass = EntryClass; + break; + } + } + } + + return Super::OnGenerateEntryWidgetInternal(Item, WidgetClass, OwnerTable); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_TileView.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_TileView.cpp new file mode 100644 index 0000000..aa57aa6 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_TileView.cpp @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_TileView.h" + +#include "UI/Common/GUIS_WidgetFactory.h" + +#if WITH_EDITOR +#include "Editor/WidgetCompilerLog.h" +#endif + + +UGUIS_TileView::UGUIS_TileView(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +} + +#if WITH_EDITOR +void UGUIS_TileView::ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const +{ + Super::ValidateCompiledDefaults(InCompileLog); + // if (EntryWidgetFactories.Num() == 0) + // { + // InCompileLog.Error(FText::Format(FText::FromString("{0} has no Entry widget Factories defined, can't create widgets without them."), FText::FromString(GetName()))); + // } +} +#endif + +void UGUIS_TileView::SetEntryWidgetFactories(TArray NewFactories) +{ + EntryWidgetFactories = NewFactories; +} + +UUserWidget& UGUIS_TileView::OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf DesiredEntryClass, const TSharedRef& OwnerTable) +{ + TSubclassOf WidgetClass = DesiredEntryClass; + + for (const UGUIS_WidgetFactory* Factory : EntryWidgetFactories) + { + if (Factory) + { + if (const TSubclassOf EntryClass = Factory->FindWidgetClassForData(Item)) + { + WidgetClass = EntryClass; + break; + } + } + } + + return Super::OnGenerateEntryWidgetInternal(Item, WidgetClass, OwnerTable); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_UserWidgetInterface.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_UserWidgetInterface.cpp new file mode 100644 index 0000000..990a7f1 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_UserWidgetInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_UserWidgetInterface.h" + + +// Add default functionality here for any IGUIS_UserWidgetInterface functions that are not pure virtual. diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_WidgetFactory.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_WidgetFactory.cpp new file mode 100644 index 0000000..ead3328 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Common/GUIS_WidgetFactory.cpp @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Common/GUIS_WidgetFactory.h" +#include "Blueprint/UserWidget.h" +#include "Misc/DataValidation.h" + + +TSubclassOf UGUIS_WidgetFactory::FindWidgetClassForData_Implementation(const UObject* Data) const +{ + return TSubclassOf(); +} + +UGUIS_WidgetFactory::UGUIS_WidgetFactory() +{ +} + +bool UGUIS_WidgetFactory::OnDataValidation_Implementation(FText& ValidationMessage) const +{ + return true; +} + +#if WITH_EDITOR +EDataValidationResult UGUIS_WidgetFactory::IsDataValid(FDataValidationContext& Context) const +{ + FText ValidationMessage; + if (!OnDataValidation(ValidationMessage)) + { + Context.AddError(ValidationMessage); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_ButtonBase.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_ButtonBase.cpp new file mode 100644 index 0000000..7ec9a73 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_ButtonBase.cpp @@ -0,0 +1,63 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Foundation/GUIS_ButtonBase.h" + +#include "CommonActionWidget.h" + + +void UGUIS_ButtonBase::NativePreConstruct() +{ + Super::NativePreConstruct(); + + OnUpdateButtonStyle(); + RefreshButtonText(); +} + +void UGUIS_ButtonBase::UpdateInputActionWidget() +{ + Super::UpdateInputActionWidget(); + + OnUpdateButtonStyle(); + RefreshButtonText(); +} + +void UGUIS_ButtonBase::SetButtonText(const FText& InText) +{ + bOverride_ButtonText = !InText.IsEmpty(); + ButtonText = InText; + RefreshButtonText(); +} + +void UGUIS_ButtonBase::RefreshButtonText() +{ + if (!bOverride_ButtonText || ButtonText.IsEmpty()) + { + if (InputActionWidget) + { + const FText ActionDisplayText = InputActionWidget->GetDisplayText(); + if (!ActionDisplayText.IsEmpty()) + { + OnUpdateButtonText(ActionDisplayText); + return; + } + } + } + + OnUpdateButtonText(ButtonText); +} + + +void UGUIS_ButtonBase::OnInputMethodChanged(ECommonInputType CurrentInputType) +{ + Super::OnInputMethodChanged(CurrentInputType); + + OnUpdateButtonStyle(); +} + +#if WITH_EDITOR +const FText UGUIS_ButtonBase::GetPaletteCategory() +{ + return PaletteCategory; +} +#endif diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabButtonBase.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabButtonBase.cpp new file mode 100644 index 0000000..4a4aa02 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabButtonBase.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Foundation/GUIS_TabButtonBase.h" + +#include "CommonLazyImage.h" + + +// void UGUIS_TabButtonBase::SetIconFromLazyObject(TSoftObjectPtr LazyObject) +// { +// if (LazyImage_Icon) +// { +// LazyImage_Icon->SetBrushFromLazyDisplayAsset(LazyObject); +// } +// } +// +// void UGUIS_TabButtonBase::SetIconBrush(const FSlateBrush& Brush) +// { +// if (LazyImage_Icon) +// { +// LazyImage_Icon->SetBrush(Brush); +// LazyImage_Icon->SetVisibility(ESlateVisibility::Visible); +// } +// } + +// void UGUIS_TabButtonBase::SetTabLabelInfo_Implementation(const FGUIS_TabDescriptor& TabLabelInfo) +// { +// SetButtonText(TabLabelInfo.TabText); +// SetIconBrush(TabLabelInfo.IconBrush); +// } diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabDefinition.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabDefinition.cpp new file mode 100644 index 0000000..b597c15 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabDefinition.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Foundation/GUIS_TabDefinition.h" diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabListWidgetBase.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabListWidgetBase.cpp new file mode 100644 index 0000000..3e81c72 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Foundation/GUIS_TabListWidgetBase.cpp @@ -0,0 +1,267 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Foundation/GUIS_TabListWidgetBase.h" + +#include "CommonAnimatedSwitcher.h" +#include "CommonButtonBase.h" +#include "CommonActivatableWidget.h" +#include "GUIS_LogChannels.h" +#include "Editor/WidgetCompilerLog.h" +#include "UI/Foundation/GUIS_TabDefinition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_TabListWidgetBase) + +void UGUIS_TabListWidgetBase::NativeOnInitialized() +{ + Super::NativeOnInitialized(); +} + +void UGUIS_TabListWidgetBase::NativeConstruct() +{ + Super::NativeConstruct(); + + SetupTabs(); +} + +void UGUIS_TabListWidgetBase::NativeDestruct() +{ + for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray) + { + if (TabInfo.CreatedTabContentWidget) + { + TabInfo.CreatedTabContentWidget->RemoveFromParent(); + TabInfo.CreatedTabContentWidget = nullptr; + } + } + Super::NativeDestruct(); +} + +FGUIS_TabDescriptor::FGUIS_TabDescriptor() +{ + bHidden = false; +} + +UGUIS_TabListWidgetBase::UGUIS_TabListWidgetBase() +{ + bAutoListenForInput = false; + bDeferRebuildingTabList = true; +} + +bool UGUIS_TabListWidgetBase::GetPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo) +{ + const FGUIS_TabDescriptor* const FoundTabInfo = PreregisteredTabInfoArray.FindByPredicate([&](const FGUIS_TabDescriptor& TabInfo) -> bool + { + return TabInfo.TabId == TabNameId; + }); + + if (!FoundTabInfo) + { + return false; + } + + OutTabInfo = *FoundTabInfo; + return true; +} + +int32 UGUIS_TabListWidgetBase::GetPreregisteredTabIndex(FName TabNameId) const +{ + for (int32 i = 0; i < PreregisteredTabInfoArray.Num(); ++i) + { + if (PreregisteredTabInfoArray[i].TabId == TabNameId) + { + return i; + } + } + return INDEX_NONE; +} + +bool UGUIS_TabListWidgetBase::FindPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo) +{ + return GetPreregisteredTabInfo(TabNameId, OutTabInfo); +} + +void UGUIS_TabListWidgetBase::SetTabHiddenState(FName TabNameId, bool bHidden) +{ + for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray) + { + if (TabInfo.TabId == TabNameId) + { + TabInfo.bHidden = bHidden; + } + } +} + +bool UGUIS_TabListWidgetBase::RegisterDynamicTab(const FGUIS_TabDescriptor& TabDescriptor) +{ + // If it's hidden we just ignore it. + if (TabDescriptor.bHidden) + { + return true; + } + + PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor); + + return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType.LoadSynchronous(), TabDescriptor.CreatedTabContentWidget); +} + +void UGUIS_TabListWidgetBase::HandlePreLinkedSwitcherChanged() +{ + for (const FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray) + { + // Remove tab content widget from linked switcher, as it is being disassociated. + if (TabInfo.CreatedTabContentWidget) + { + TabInfo.CreatedTabContentWidget->RemoveFromParent(); + } + } + + Super::HandlePreLinkedSwitcherChanged(); +} + +void UGUIS_TabListWidgetBase::HandlePostLinkedSwitcherChanged() +{ + if (!IsDesignTime() && GetCachedWidget().IsValid()) + { + // Don't bother making tabs if we're in the designer or haven't been constructed yet + SetupTabs(); + } + + Super::HandlePostLinkedSwitcherChanged(); +} + +void UGUIS_TabListWidgetBase::HandleTabCreation_Implementation(FName TabId, UCommonButtonBase* TabButton) +{ + FGUIS_TabDescriptor* TabInfoPtr = nullptr; + + FGUIS_TabDescriptor TabInfo; + if (GetPreregisteredTabInfo(TabId, TabInfo)) + { + TabInfoPtr = &TabInfo; + } + else + { + TabInfoPtr = PendingTabLabelInfoMap.Find(TabId); + } + + if (TabButton->GetClass()->ImplementsInterface(UGUIS_TabButtonInterface::StaticClass())) + { + if (ensureMsgf(TabInfoPtr, TEXT("A tab button was created with id %s but no label info was specified. RegisterDynamicTab should be used over RegisterTab to provide label info."), + *TabId.ToString())) + { + IGUIS_TabButtonInterface::Execute_SetTabLabelInfo(TabButton, *TabInfoPtr); + } + } + + PendingTabLabelInfoMap.Remove(TabId); +} + +bool UGUIS_TabListWidgetBase::IsFirstTabActive() const +{ + if (PreregisteredTabInfoArray.Num() > 0) + { + return GetActiveTab() == PreregisteredTabInfoArray[0].TabId; + } + + return false; +} + +bool UGUIS_TabListWidgetBase::IsLastTabActive() const +{ + if (PreregisteredTabInfoArray.Num() > 0) + { + return GetActiveTab() == PreregisteredTabInfoArray.Last().TabId; + } + + return false; +} + +bool UGUIS_TabListWidgetBase::IsTabVisible(FName TabId) +{ + if (const UCommonButtonBase* Button = GetTabButtonBaseByID(TabId)) + { + const ESlateVisibility TabVisibility = Button->GetVisibility(); + return (TabVisibility == ESlateVisibility::Visible + || TabVisibility == ESlateVisibility::HitTestInvisible + || TabVisibility == ESlateVisibility::SelfHitTestInvisible); + } + + return false; +} + +int32 UGUIS_TabListWidgetBase::GetVisibleTabCount() +{ + int32 Result = 0; + const int32 TabCount = GetTabCount(); + for (int32 Index = 0; Index < TabCount; Index++) + { + if (IsTabVisible(GetTabIdAtIndex(Index))) + { + Result++; + } + } + + return Result; +} + +void UGUIS_TabListWidgetBase::SetupTabs() +{ + for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray) + { + if (TabInfo.bHidden) + { + continue; + } + + // If the tab content hasn't been created already, create it. + if (!TabInfo.CreatedTabContentWidget && !TabInfo.TabContentType.IsNull()) + { + TabInfo.CreatedTabContentWidget = CreateWidget(GetOwningPlayer(), TabInfo.TabContentType.LoadSynchronous()); + OnTabContentCreatedNative.Broadcast(TabInfo.TabId, Cast(TabInfo.CreatedTabContentWidget)); + OnTabContentCreated.Broadcast(TabInfo.TabId, Cast(TabInfo.CreatedTabContentWidget)); + } + + if (UCommonAnimatedSwitcher* CurrentLinkedSwitcher = GetLinkedSwitcher()) + { + // Add the tab content to the newly linked switcher. + if (!CurrentLinkedSwitcher->HasChild(TabInfo.CreatedTabContentWidget)) + { + CurrentLinkedSwitcher->AddChild(TabInfo.CreatedTabContentWidget); + } + } + + // If the tab is not already registered, register it. + if (GetTabButtonBaseByID(TabInfo.TabId) == nullptr) + { + RegisterTab(TabInfo.TabId, TabInfo.TabButtonType.LoadSynchronous(), TabInfo.CreatedTabContentWidget); + } + } +} + +#if WITH_EDITOR +void UGUIS_TabListWidgetBase::PostLoad() +{ + if (!TabDefinitions_DEPRECATED.IsEmpty()) + { + for (TObjectPtr Def : TabDefinitions_DEPRECATED) + { + if (Def) + { + FGUIS_TabDescriptor Tab; + Tab.TabId = Def->TabId; + Tab.IconBrush = Def->IconBrush; + Tab.TabButtonType = Def->TabButtonType; + Tab.TabText = Def->TabText; + PreregisteredTabInfoArray.Add(Tab); + } + } + TabDefinitions_DEPRECATED.Empty(); + } + + Super::PostLoad(); +} + +void UGUIS_TabListWidgetBase::ValidateCompiledDefaults(class IWidgetCompilerLog& CompileLog) const +{ + Super::ValidateCompiledDefaults(CompileLog); +} +#endif diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_ActivatableWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_ActivatableWidget.cpp new file mode 100644 index 0000000..e146833 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_ActivatableWidget.cpp @@ -0,0 +1,59 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/GUIS_ActivatableWidget.h" + +#include "Editor/WidgetCompilerLog.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_ActivatableWidget) + +#define LOCTEXT_NAMESPACE "GGF" + +UGUIS_ActivatableWidget::UGUIS_ActivatableWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UGUIS_ActivatableWidget::SetIsBackHandler(bool bNewState) +{ +} + +TOptional UGUIS_ActivatableWidget::GetDesiredInputConfig() const +{ + switch (InputConfig) + { + case EGUIS_ActivatableWidgetInputMode::GameAndMenu: + return FUIInputConfig(ECommonInputMode::All, GameMouseCaptureMode); + case EGUIS_ActivatableWidgetInputMode::Game: + return FUIInputConfig(ECommonInputMode::Game, GameMouseCaptureMode); + case EGUIS_ActivatableWidgetInputMode::Menu: + return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture); + case EGUIS_ActivatableWidgetInputMode::Default: + default: + return TOptional(); + } +} + +#if WITH_EDITOR + +void UGUIS_ActivatableWidget::ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const +{ + Super::ValidateCompiledWidgetTree(BlueprintWidgetTree, CompileLog); + + if (!GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UGUIS_ActivatableWidget, BP_GetDesiredFocusTarget))) + { + if (GetParentNativeClass(GetClass()) == StaticClass()) + { + CompileLog.Warning(LOCTEXT("ValidateGetDesiredFocusTarget_Warning", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen.")); + } + else + { + //TODO - Note for now, because we can't guarantee it isn't implemented in a native subclass of this one. + CompileLog.Note(LOCTEXT("ValidateGetDesiredFocusTarget_Note", + "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen. If it was implemented in the native base class you can ignore this message.")); + } + } +} + +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIContext.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIContext.cpp new file mode 100644 index 0000000..2104538 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIContext.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/GUIS_GameUIContext.h" diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIFunctionLibrary.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIFunctionLibrary.cpp new file mode 100644 index 0000000..8195a77 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIFunctionLibrary.cpp @@ -0,0 +1,263 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/GUIS_GameUIFunctionLibrary.h" +#include "CommonInputSubsystem.h" +#include "CommonInputTypeEnum.h" +#include "GUIS_LogChannels.h" +#include "Components/ListView.h" +#include "Engine/GameInstance.h" +#include "UI/GUIS_GameUILayout.h" +#include "UI/GUIS_GameUIPolicy.h" +#include "UI/GUIS_GameUISubsystem.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIFunctionLibrary) + +int32 UGUIS_GameUIFunctionLibrary::InputSuspensions = 0; + +ECommonInputType UGUIS_GameUIFunctionLibrary::GetOwningPlayerInputType(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType(); + } + } + + return ECommonInputType::Count; +} + +bool UGUIS_GameUIFunctionLibrary::IsOwningPlayerUsingTouch(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType() == ECommonInputType::Touch; + } + } + return false; +} + +bool UGUIS_GameUIFunctionLibrary::IsOwningPlayerUsingGamepad(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad; + } + } + return false; +} + +UCommonActivatableWidget* UGUIS_GameUIFunctionLibrary::PushContentToUILayer_ForPlayer(const APlayerController* PlayerController, FGameplayTag LayerName, + TSubclassOf WidgetClass) +{ + if (!ensure(PlayerController) || !ensure(WidgetClass != nullptr)) + { + return nullptr; + } + + UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController); + + if (UILayout == nullptr) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer_ForPlayer failed to find UILayout for player."), ELogVerbosity::Error); + return nullptr; + } + + return UILayout->PushWidgetToLayerStack(LayerName, WidgetClass); +} + +void UGUIS_GameUIFunctionLibrary::PopContentFromUILayer_ForPlayer(const APlayerController* PlayerController, FGameplayTag LayerName, int32 RemainNum) +{ + if (!ensure(PlayerController)) + { + return; + } + + UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController); + + if (UILayout == nullptr) + { + FFrame::KismetExecutionMessage(TEXT("PopContentFromUILayer_ForPlayer failed to find UILayout for player."), ELogVerbosity::Error); + return; + } + + if (UCommonActivatableWidgetContainerBase* Layer = UILayout->GetLayerWidget(LayerName)) + { + const TArray& List = Layer->GetWidgetList(); + + int32 MinIdx = RemainNum >= 1 ? RemainNum - 1 : 0; + + for (int32 i = List.Num() - 1; i >= MinIdx; i--) + { + Layer->RemoveWidget(*List[i]); + } + } +} + +// void UGUIS_GameUIFunctionLibrary::PushStreamedContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, FGameplayTag LayerName, TSoftClassPtr WidgetClass) +// { +// if (!ensure(LocalPlayer) || !ensure(!WidgetClass.IsNull())) +// { +// return; +// } +// +// if (UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem()) +// { +// if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) +// { +// if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked(LocalPlayer))) +// { +// const bool bSuspendInputUntilComplete = true; +// RootLayout->PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, WidgetClass); +// } +// } +// } +// } + +void UGUIS_GameUIFunctionLibrary::PopContentFromUILayer(UCommonActivatableWidget* ActivatableWidget) +{ + if (!ActivatableWidget) + { + // Ignore request to pop an already deleted widget + return; + } + + if (const APlayerController* PlayerController = ActivatableWidget->GetOwningPlayer()) + { + if (UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController)) + { + UE_LOG(LogGUIS, Verbose, TEXT("Popped content:%s from ui layer."), *GetNameSafe(ActivatableWidget)) + UILayout->FindAndRemoveWidgetFromLayer(ActivatableWidget); + } + } +} + +void UGUIS_GameUIFunctionLibrary::PopContentsFromUILayer(TArray ActivatableWidgets, bool bReverse) +{ + if (bReverse) + { + for (int32 i = ActivatableWidgets.Num() - 1; i >= 0; i--) + { + PopContentFromUILayer(ActivatableWidgets[i]); + } + } + else + { + for (int32 i = 0; i < ActivatableWidgets.Num(); i++) + { + PopContentFromUILayer(ActivatableWidgets[i]); + } + } +} + +ULocalPlayer* UGUIS_GameUIFunctionLibrary::GetLocalPlayerFromController(APlayerController* PlayerController) +{ + if (PlayerController) + { + return Cast(PlayerController->Player); + } + + return nullptr; +} + +UGUIS_GameUILayout* UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(const APlayerController* PlayerController) +{ + if (!IsValid(PlayerController)) + { + return nullptr; + } + if (ULocalPlayer* LocalPlayer = Cast(PlayerController->GetLocalPlayer())) + { + const ULocalPlayer* CommonLocalPlayer = CastChecked(LocalPlayer); + if (const UGameInstance* GameInstance = CommonLocalPlayer->GetGameInstance()) + { + if (UGUIS_GameUISubsystem* UIManager = GameInstance->GetSubsystem()) + { + if (const UGUIS_GameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + if (UGUIS_GameUILayout* RootLayout = Policy->GetRootLayout(CommonLocalPlayer)) + { + return RootLayout; + } + } + } + } + } + return nullptr; +} + +FName UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(APlayerController* PlayerController, FName SuspendReason) +{ + return SuspendInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendReason); +} + +FName UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendReason) +{ + if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer)) + { + InputSuspensions++; + FName SuspendToken = SuspendReason; + SuspendToken.SetNumber(InputSuspensions); + + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, true); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, true); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, true); + + return SuspendToken; + } + + return NAME_None; +} + +void UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(APlayerController* PlayerController, FName SuspendToken) +{ + ResumeInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendToken); +} + +void UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendToken) +{ + if (SuspendToken == NAME_None) + { + return; + } + + if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer)) + { + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, false); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, false); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, false); + } +} + +UObject* UGUIS_GameUIFunctionLibrary::GetTypedListItem(TScriptInterface UserObjectListEntry, TSubclassOf DesiredClass) +{ + UUserWidget* EntryWidget = Cast(UserObjectListEntry.GetObject()); + if (!IsValid(EntryWidget)) + { + return nullptr; + } + UListView* OwningListView = Cast(UUserListEntryLibrary::GetOwningListView(EntryWidget)); + if (!IsValid(OwningListView)) + { + return nullptr; + } + + UObject* ListItem = *OwningListView->ItemFromEntryWidget(*EntryWidget); + + if (ListItem->GetClass()->IsChildOf(DesiredClass)) + { + return ListItem; + } + return nullptr; +} + +bool UGUIS_GameUIFunctionLibrary::GetTypedListItemSafely(TScriptInterface UserObjectListEntry, TSubclassOf DesiredClass, UObject*& OutItem) +{ + OutItem = GetTypedListItem(UserObjectListEntry, DesiredClass); + return OutItem != nullptr; +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUILayout.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUILayout.cpp new file mode 100644 index 0000000..f4fbc37 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUILayout.cpp @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/GUIS_GameUILayout.h" +#include "Engine/GameInstance.h" +#include "Kismet/GameplayStatics.h" +#include "GUIS_LogChannels.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUILayout) + +class UObject; + +UGUIS_GameUILayout::UGUIS_GameUILayout(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UGUIS_GameUILayout::SetIsDormant(bool InDormant) +{ + if (bIsDormant != InDormant) + { + const ULocalPlayer* LP = GetOwningLocalPlayer(); + const int32 PlayerId = LP ? LP->GetControllerId() : -1; + const TCHAR* OldDormancyStr = bIsDormant ? TEXT("Dormant") : TEXT("Not-Dormant"); + const TCHAR* NewDormancyStr = InDormant ? TEXT("Dormant") : TEXT("Not-Dormant"); + const TCHAR* PrimaryPlayerStr = LP && LP->IsPrimaryPlayer() ? TEXT("[Primary]") : TEXT("[Non-Primary]"); + UE_LOG(LogGUIS, Display, TEXT("%s PrimaryGameLayout Dormancy changed for [%d] from [%s] to [%s]"), PrimaryPlayerStr, PlayerId, OldDormancyStr, NewDormancyStr); + + bIsDormant = InDormant; + OnIsDormantChanged(); + } +} + +void UGUIS_GameUILayout::OnIsDormantChanged() +{ + //@TODO NDarnell Determine what to do with dormancy, in the past we treated dormancy as a way to shutoff rendering + //and the view for the other local players when we force multiple players to use the player view of a single player. + + //if (ULocalPlayer* LocalPlayer = GetOwningLocalPlayer()) + //{ + // // When the root layout is dormant, we don't want to render anything from the owner's view either + // LocalPlayer->SetIsPlayerViewEnabled(!bIsDormant); + //} + + //SetVisibility(bIsDormant ? ESlateVisibility::Collapsed : ESlateVisibility::SelfHitTestInvisible); + + //OnLayoutDormancyChanged().Broadcast(bIsDormant); +} + +void UGUIS_GameUILayout::RegisterLayer(FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget) +{ + if (!IsDesignTime()) + { + LayerWidget->OnTransitioningChanged.AddUObject(this, &UGUIS_GameUILayout::OnWidgetStackTransitioning); + // TODO: Consider allowing a transition duration, we currently set it to 0, because if it's not 0, the + // transition effect will cause focus to not transition properly to the new widgets when using + // gamepad always. + LayerWidget->SetTransitionDuration(0.0); + + Layers.Add(LayerTag, LayerWidget); + } +} + +void UGUIS_GameUILayout::OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase* Widget, bool bIsTransitioning) +{ + if (bIsTransitioning) + { + const FName SuspendToken = UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(GetOwningLocalPlayer(), TEXT("GlobalStackTransion")); + SuspendInputTokens.Add(SuspendToken); + } + else + { + if (ensure(SuspendInputTokens.Num() > 0)) + { + const FName SuspendToken = SuspendInputTokens.Pop(); + UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(GetOwningLocalPlayer(), SuspendToken); + } + } +} + +void UGUIS_GameUILayout::FindAndRemoveWidgetFromLayer(UCommonActivatableWidget* ActivatableWidget) +{ + // We're not sure what layer the widget is on so go searching. + for (const auto& LayerKVP : Layers) + { + LayerKVP.Value->RemoveWidget(*ActivatableWidget); + } +} + +UCommonActivatableWidgetContainerBase* UGUIS_GameUILayout::GetLayerWidget(FGameplayTag LayerName) +{ + return Layers.FindRef(LayerName); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIPolicy.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIPolicy.cpp new file mode 100644 index 0000000..448bda5 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIPolicy.cpp @@ -0,0 +1,293 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/GUIS_GameUIPolicy.h" +#include "UI/GUIS_GameUISubsystem.h" +#include "Engine/GameInstance.h" +#include "Framework/Application/SlateApplication.h" +#include "Engine/Engine.h" +#include "GUIS_LogChannels.h" +#include "Input/CommonUIInputTypes.h" +#include "UI/GUIS_GameUIContext.h" +#include "UI/GUIS_GameUILayout.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIPolicy) + +// Static +UGUIS_GameUIPolicy* UGUIS_GameUIPolicy::GetGameUIPolicy(const UObject* WorldContextObject) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) + { + if (UGameInstance* GameInstance = World->GetGameInstance()) + { + if (UGUIS_GameUISubsystem* UIManager = UGameInstance::GetSubsystem(GameInstance)) + { + return UIManager->GetCurrentUIPolicy(); + } + } + } + + return nullptr; +} + +UGUIS_GameUISubsystem* UGUIS_GameUIPolicy::GetOwningSubsystem() const +{ + return Cast(GetOuter()); +} + +UWorld* UGUIS_GameUIPolicy::GetWorld() const +{ + if (UGUIS_GameUISubsystem* Subsystem = GetOwningSubsystem()) + { + return Subsystem->GetGameInstance()->GetWorld(); + } + return nullptr; +} + +UGUIS_GameUILayout* UGUIS_GameUIPolicy::GetRootLayout(const ULocalPlayer* LocalPlayer) const +{ + const FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer); + return LayoutInfo ? LayoutInfo->RootLayout : nullptr; +} + +UGUIS_GameUIContext* UGUIS_GameUIPolicy::GetContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass) +{ + if (const FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++) + { + if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass) + { + return LayoutInfo->Contexts[i]; + } + } + } + return nullptr; +} + +bool UGUIS_GameUIPolicy::AddContext(const ULocalPlayer* LocalPlayer, UGUIS_GameUIContext* NewContext) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + if (const UObject* ExistingContext = GetContext(LocalPlayer, NewContext->GetClass())) + { + UE_LOG(LogGUIS, Warning, TEXT("[%s] is trying to add repeat context of type(%s) for %s, which is not allowed!"), *GetName(), *NewContext->GetClass()->GetName(), *GetNameSafe(LocalPlayer)); + return false; + } + LayoutInfo->Contexts.Add(NewContext); + UE_LOG(LogGUIS, Verbose, TEXT("[%s] registered context of type(%s) for %s."), *GetName(), *NewContext->GetClass()->GetName(), *GetNameSafe(LocalPlayer)); + return true; + } + return false; +} + +UGUIS_GameUIContext* UGUIS_GameUIPolicy::FindContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++) + { + if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass) + { + return LayoutInfo->Contexts[i]; + } + } + } + return nullptr; +} + +void UGUIS_GameUIPolicy::RemoveContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + int32 FoundContext = INDEX_NONE; + for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++) + { + if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass) + { + FoundContext = i; + UE_LOG(LogGUIS, Verbose, TEXT("[%s] unregistered context of type(%s) for %s."), *GetName(), *LayoutInfo->Contexts[i]->GetClass()->GetName(), *GetNameSafe(LocalPlayer)); + break; + } + } + LayoutInfo->Contexts.RemoveAt(FoundContext); + } +} + +void UGUIS_GameUIPolicy::AddUIAction(const ULocalPlayer* LocalPlayer, UCommonUserWidget* Target, const FDataTableRowHandle& InputAction, bool bShouldDisplayInActionBar, + const FGUIS_UIActionExecutedDelegate& Callback, FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + if (IsValid(Target)) + { + FBindUIActionArgs BindArgs(InputAction, bShouldDisplayInActionBar, FSimpleDelegate::CreateLambda([InputAction, Callback]() + { + Callback.ExecuteIfBound(InputAction.RowName); + })); + BindingHandle.Handle = Target->RegisterUIActionBinding(BindArgs); + LayoutInfo->BindingHandles.Add(BindingHandle.Handle); + } + } +} + +void UGUIS_GameUIPolicy::RemoveUIAction(const ULocalPlayer* LocalPlayer, FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + if (BindingHandle.Handle.IsValid()) + { + UE_LOG(LogGUIS, Display, TEXT("Unregister binding for %s"), *BindingHandle.Handle.GetDisplayName().ToString()) + + BindingHandle.Handle.Unregister(); + LayoutInfo->BindingHandles.Remove(BindingHandle.Handle); + } + } +} + +void UGUIS_GameUIPolicy::NotifyPlayerAdded(ULocalPlayer* LocalPlayer) +{ + NotifyPlayerRemoved(LocalPlayer); + + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout); + LayoutInfo->bAddedToViewport = true; + } + else + { + CreateLayoutWidget(LocalPlayer); + } +} + +void UGUIS_GameUIPolicy::NotifyPlayerRemoved(ULocalPlayer* LocalPlayer) +{ + if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + RemoveLayoutFromViewport(LocalPlayer, LayoutInfo->RootLayout); + LayoutInfo->bAddedToViewport = false; + + LayoutInfo->Contexts.Empty(); + + if (LocalMultiplayerInteractionMode == EGUIS_LocalMultiplayerInteractionMode::SingleToggle && !LocalPlayer->IsPrimaryPlayer()) + { + UGUIS_GameUILayout* RootLayout = LayoutInfo->RootLayout; + if (RootLayout && !RootLayout->IsDormant()) + { + // We're removing a secondary player's root while it's in control - transfer control back to the primary player's root + RootLayout->SetIsDormant(true); + for (const FGUIS_RootViewportLayoutInfo& RootLayoutInfo : RootViewportLayouts) + { + if (RootLayoutInfo.LocalPlayer->IsPrimaryPlayer()) + { + if (UGUIS_GameUILayout* PrimaryRootLayout = RootLayoutInfo.RootLayout) + { + PrimaryRootLayout->SetIsDormant(false); + } + } + } + } + } + } +} + +void UGUIS_GameUIPolicy::NotifyPlayerDestroyed(ULocalPlayer* LocalPlayer) +{ + NotifyPlayerRemoved(LocalPlayer); + const int32 LayoutInfoIdx = RootViewportLayouts.IndexOfByKey(LocalPlayer); + if (LayoutInfoIdx != INDEX_NONE) + { + UGUIS_GameUILayout* Layout = RootViewportLayouts[LayoutInfoIdx].RootLayout; + RootViewportLayouts.RemoveAt(LayoutInfoIdx); + + RemoveLayoutFromViewport(LocalPlayer, Layout); + + OnRootLayoutReleased(LocalPlayer, Layout); + } +} + +void UGUIS_GameUIPolicy::AddLayoutToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout) +{ + UE_LOG(LogGUIS, Log, TEXT("[%s] is adding player [%s]'s root layout [%s] to the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + + Layout->SetPlayerContext(FLocalPlayerContext(LocalPlayer)); + Layout->AddToPlayerScreen(1000); + + OnRootLayoutAddedToViewport(LocalPlayer, Layout); +} + +void UGUIS_GameUIPolicy::RemoveLayoutFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout) +{ + TWeakPtr LayoutSlateWidget = Layout->GetCachedWidget(); + if (LayoutSlateWidget.IsValid()) + { + UE_LOG(LogGUIS, Log, TEXT("[%s] is removing player [%s]'s root layout [%s] from the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + + Layout->RemoveFromParent(); + if (LayoutSlateWidget.IsValid()) + { + UE_LOG(LogGUIS, Log, TEXT("Player [%s]'s root layout [%s] has been removed from the viewport, but other references to its underlying Slate widget still exist. Noting in case we leak it."), + *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + } + + OnRootLayoutRemovedFromViewport(LocalPlayer, Layout); + } +} + +void UGUIS_GameUIPolicy::OnRootLayoutAddedToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout) +{ +#if WITH_EDITOR + if (GIsEditor && LocalPlayer->IsPrimaryPlayer()) + { + // So our controller will work in PIE without needing to click in the viewport + FSlateApplication::Get().SetUserFocusToGameViewport(0); + } +#endif + BP_OnRootLayoutAddedToViewport(LocalPlayer, Layout); +} + +void UGUIS_GameUIPolicy::OnRootLayoutRemovedFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout) +{ + BP_OnRootLayoutRemovedFromViewport(LocalPlayer, Layout); +} + +void UGUIS_GameUIPolicy::OnRootLayoutReleased(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout) +{ + BP_OnRootLayoutReleased(LocalPlayer, Layout); +} + +void UGUIS_GameUIPolicy::RequestPrimaryControl(UGUIS_GameUILayout* Layout) +{ + if (LocalMultiplayerInteractionMode == EGUIS_LocalMultiplayerInteractionMode::SingleToggle && Layout->IsDormant()) + { + for (const FGUIS_RootViewportLayoutInfo& LayoutInfo : RootViewportLayouts) + { + UGUIS_GameUILayout* RootLayout = LayoutInfo.RootLayout; + if (RootLayout && !RootLayout->IsDormant()) + { + RootLayout->SetIsDormant(true); + break; + } + } + Layout->SetIsDormant(false); + } +} + +void UGUIS_GameUIPolicy::CreateLayoutWidget(ULocalPlayer* LocalPlayer) +{ + if (APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld())) + { + TSubclassOf LayoutWidgetClass = GetLayoutWidgetClass(LocalPlayer); + if (ensure(LayoutWidgetClass && !LayoutWidgetClass->HasAnyClassFlags(CLASS_Abstract))) + { + UGUIS_GameUILayout* NewLayoutObject = CreateWidget(PlayerController, LayoutWidgetClass); + RootViewportLayouts.Emplace(LocalPlayer, NewLayoutObject, true); + + AddLayoutToViewport(LocalPlayer, NewLayoutObject); + } + } +} + +TSubclassOf UGUIS_GameUIPolicy::GetLayoutWidgetClass(ULocalPlayer* LocalPlayer) +{ + return LayoutClass.LoadSynchronous(); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIStructLibrary.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIStructLibrary.cpp new file mode 100644 index 0000000..a0fced0 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUIStructLibrary.cpp @@ -0,0 +1,13 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/GUIS_GameUIStructLibrary.h" +#include "Engine/LocalPlayer.h" +#include "UI/GUIS_GameUIContext.h" +#include "UI/GUIS_GameUILayout.h" + +FGUIS_UIContextBindingHandle::FGUIS_UIContextBindingHandle(ULocalPlayer* InLocalPlayer, UClass* InContextClass) +{ + LocalPlayer = InLocalPlayer; + ContextClass = InContextClass; +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUISubsystem.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUISubsystem.cpp new file mode 100644 index 0000000..33297f7 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameUISubsystem.cpp @@ -0,0 +1,226 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/GUIS_GameUISubsystem.h" +#include "GameFramework/Pawn.h" +#include "GUIS_GenericUISystemSettings.h" +#include "CommonUserWidget.h" +#include "GUIS_LogChannels.h" +#include "Engine/GameInstance.h" +#include "Input/CommonUIInputTypes.h" +#include "UI/GUIS_GameUIContext.h" +#include "UI/GUIS_GameUIPolicy.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUISubsystem) + +class FSubsystemCollectionBase; +class UClass; + +void UGUIS_GameUISubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (UGUIS_GenericUISystemSettings::Get()->GameUIPolicyClass.IsNull()) + { + UE_LOG(LogGUIS, Error, TEXT("GUIS_GameUISubsystem::Initialize Failed, Missing GameUIPolicyClass in GenericUISystemSettings!!!")); + return; + } + + if (!CurrentPolicy) + { + TSubclassOf PolicyClass = UGUIS_GenericUISystemSettings::Get()->GameUIPolicyClass.LoadSynchronous(); + if (PolicyClass) + { + UGUIS_GameUIPolicy* NewPolicy = NewObject(this, PolicyClass); + if (NewPolicy) + { + SwitchToPolicy(NewPolicy); + } + else + { + UE_LOG(LogGUIS, Error, TEXT("GUIS_GameUISubsystem::Initialize Failed, failed to create Game UI Policy from class:%s!"), *PolicyClass->GetName()); + } + } + else + { + UE_LOG(LogGUIS, Error, TEXT("GUIS_GameUISubsystem::Initialize Failed, Missing GameUIPolicyClass in GenericUISystemSettings!!!")); + } + } +} + +void UGUIS_GameUISubsystem::Deinitialize() +{ + Super::Deinitialize(); + + SwitchToPolicy(nullptr); +} + +bool UGUIS_GameUISubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + if (CastChecked(Outer)->IsDedicatedServerInstance()) + { + return false; + } + + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + if (ChildClasses.Num() == 0) + { + UE_LOG(LogGUIS, Display, TEXT("No override implementation found for UGUIS_GameUISubsystem, So creating it.")) + return true; + } + return false; +} + +void UGUIS_GameUISubsystem::AddPlayer(ULocalPlayer* LocalPlayer) +{ + NotifyPlayerAdded(LocalPlayer); +} + +void UGUIS_GameUISubsystem::RemovePlayer(ULocalPlayer* LocalPlayer) +{ + NotifyPlayerDestroyed(LocalPlayer); +} + +void UGUIS_GameUISubsystem::NotifyPlayerAdded(ULocalPlayer* LocalPlayer) +{ + if (ensure(LocalPlayer) && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerAdded(LocalPlayer); + } +} + +void UGUIS_GameUISubsystem::NotifyPlayerRemoved(ULocalPlayer* LocalPlayer) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerRemoved(LocalPlayer); + } +} + +void UGUIS_GameUISubsystem::NotifyPlayerDestroyed(ULocalPlayer* LocalPlayer) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerDestroyed(LocalPlayer); + } +} + +void UGUIS_GameUISubsystem::RegisterUIActionBinding(UCommonUserWidget* Target, FDataTableRowHandle InputAction, bool bShouldDisplayInActionBar, const FGUIS_UIActionExecutedDelegate& Callback, + FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (IsValid(Target)) + { + FBindUIActionArgs BindArgs(InputAction, bShouldDisplayInActionBar, FSimpleDelegate::CreateLambda([InputAction, Callback]() + { + Callback.ExecuteIfBound(InputAction.RowName); + })); + + BindingHandle.Handle = Target->RegisterUIActionBinding(BindArgs); + BindingHandles.Add(BindingHandle.Handle); + } +} + +void UGUIS_GameUISubsystem::UnregisterBinding(FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (BindingHandle.Handle.IsValid()) + { + UE_LOG(LogGUIS, Display, TEXT("Unregister binding for %s"), *BindingHandle.Handle.GetDisplayName().ToString()) + + BindingHandle.Handle.Unregister(); + BindingHandles.Remove(BindingHandle.Handle); + } +} + +void UGUIS_GameUISubsystem::RegisterUIActionBindingForPlayer(ULocalPlayer* LocalPlayer, UCommonUserWidget* Target, FDataTableRowHandle InputAction, bool bShouldDisplayInActionBar, + const FGUIS_UIActionExecutedDelegate& Callback, FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->AddUIAction(LocalPlayer, Target, InputAction, bShouldDisplayInActionBar, Callback, BindingHandle); + } +} + +void UGUIS_GameUISubsystem::UnregisterUIActionBindingForPlayer(ULocalPlayer* LocalPlayer, FGUIS_UIActionBindingHandle& BindingHandle) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->RemoveUIAction(LocalPlayer, BindingHandle); + } +} + +void UGUIS_GameUISubsystem::RegisterUIContextForPlayer(ULocalPlayer* LocalPlayer, UGUIS_GameUIContext* Context, FGUIS_UIContextBindingHandle& BindingHandle) +{ + if (LocalPlayer && CurrentPolicy && Context) + { + if (CurrentPolicy->AddContext(LocalPlayer, Context)) + { + BindingHandle = FGUIS_UIContextBindingHandle(LocalPlayer, Context->GetClass()); + } + } +} + +void UGUIS_GameUISubsystem::RegisterUIContextForActor(AActor* Actor, UGUIS_GameUIContext* Context, FGUIS_UIContextBindingHandle& BindingHandle) +{ + if (!IsValid(Actor)) + { + UE_LOG(LogGUIS, Error, TEXT("Trying to register ui context for invalid pawn!")) + return; + } + APawn* Pawn = Cast(Actor); + if (Pawn == nullptr || !Pawn->IsLocallyControlled()) + { + UE_LOG(LogGUIS, Error, TEXT("Trying to register ui context for actor(%s) which is not locally controlled pawn!"), *Pawn->GetName()) + return; + } + APlayerController* PC = Cast(Pawn->GetController()); + if (PC == nullptr) + { + UE_LOG(LogGUIS, Error, TEXT("Trying to register ui context for pawn(%s) which doesn't have valid player controller"), *Pawn->GetName()) + return; + } + RegisterUIContextForPlayer(PC->GetLocalPlayer(), Context, BindingHandle); +} + +bool UGUIS_GameUISubsystem::FindUIContextForPlayer(ULocalPlayer* LocalPlayer, TSubclassOf ContextClass, UGUIS_GameUIContext*& OutContext) +{ + if (LocalPlayer && CurrentPolicy && ContextClass != nullptr) + { + if (UGUIS_GameUIContext* Context = CurrentPolicy->GetContext(LocalPlayer, ContextClass)) + { + if (Context->GetClass() == ContextClass) + { + OutContext = Context; + return true; + } + } + } + return false; +} + +bool UGUIS_GameUISubsystem::FindUIContextFromHandle(FGUIS_UIContextBindingHandle& BindingHandle, TSubclassOf ContextClass, UGUIS_GameUIContext*& OutContext) +{ + if (BindingHandle.LocalPlayer && BindingHandle.ContextClass && CurrentPolicy) + { + OutContext = CurrentPolicy->FindContext(BindingHandle.LocalPlayer, BindingHandle.ContextClass); + } + return OutContext != nullptr; +} + +void UGUIS_GameUISubsystem::UnregisterUIContextForPlayer(FGUIS_UIContextBindingHandle& BindingHandle) +{ + if (BindingHandle.LocalPlayer && BindingHandle.ContextClass && CurrentPolicy) + { + CurrentPolicy->RemoveContext(BindingHandle.LocalPlayer, BindingHandle.ContextClass); + BindingHandle.LocalPlayer = nullptr; + BindingHandle.ContextClass = nullptr; + } +} + +void UGUIS_GameUISubsystem::SwitchToPolicy(UGUIS_GameUIPolicy* InPolicy) +{ + if (CurrentPolicy != InPolicy) + { + CurrentPolicy = InPolicy; + } +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameplayTags.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameplayTags.cpp new file mode 100644 index 0000000..20fcc83 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/GUIS_GameplayTags.cpp @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/GUIS_GameplayTags.h" + +namespace GUIS_GameModalActionTags +{ + UE_DEFINE_GAMEPLAY_TAG(Ok, "GUIS.Modal.Action.Ok"); + UE_DEFINE_GAMEPLAY_TAG(Cancel, "GUIS.Modal.Action.Cancel"); + UE_DEFINE_GAMEPLAY_TAG(Yes, "GUIS.Modal.Action.Yes"); + UE_DEFINE_GAMEPLAY_TAG(No, "GUIS.Modal.Action.No"); + UE_DEFINE_GAMEPLAY_TAG(Unknown, "GUIS.Modal.Action.Unknown"); +} + +namespace GUIS_GameUILayerTags +{ + UE_DEFINE_GAMEPLAY_TAG(Modal, "GUIS.Layer.Modal"); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_JoystickWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_JoystickWidget.cpp new file mode 100644 index 0000000..b3e6706 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_JoystickWidget.cpp @@ -0,0 +1,108 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "UI/Mobile/GUIS_JoystickWidget.h" + +#include "CommonHardwareVisibilityBorder.h" +#include "Components/Image.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_JoystickWidget) + +#define LOCTEXT_NAMESPACE "GUIS_Joystick" + +UGUIS_JoystickWidget::UGUIS_JoystickWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetConsumePointerInput(true); +} + +FReply UGUIS_JoystickWidget::NativeOnTouchStarted(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) +{ + Super::NativeOnTouchStarted(InGeometry, InGestureEvent); + + TouchOrigin = InGestureEvent.GetScreenSpacePosition(); + + FReply Reply = FReply::Handled(); + if (!HasMouseCaptureByUser(InGestureEvent.GetUserIndex(), InGestureEvent.GetPointerIndex())) + { + Reply.CaptureMouse(GetCachedWidget().ToSharedRef()); + } + return Reply; +} + +FReply UGUIS_JoystickWidget::NativeOnTouchMoved(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) +{ + Super::NativeOnTouchMoved(InGeometry, InGestureEvent); + HandleTouchDelta(InGeometry, InGestureEvent); + + FReply Reply = FReply::Handled(); + if (!HasMouseCaptureByUser(InGestureEvent.GetUserIndex(), InGestureEvent.GetPointerIndex())) + { + Reply.CaptureMouse(GetCachedWidget().ToSharedRef()); + } + return Reply; +} + +FReply UGUIS_JoystickWidget::NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) +{ + StopInputSimulation(); + return FReply::Handled().ReleaseMouseCapture(); +} + +void UGUIS_JoystickWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseLeave(InMouseEvent); + StopInputSimulation(); +} + +void UGUIS_JoystickWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + if (!CommonVisibilityBorder || CommonVisibilityBorder->IsVisible()) + { + // Move the inner stick icon around with the vector + if (JoystickForeground && JoystickBackground) + { + JoystickForeground->SetRenderTranslation( + (bNegateYAxis ? FVector2D(1.0f, -1.0f) : FVector2D(1.0f)) * + StickVector * + (JoystickBackground->GetDesiredSize() * 0.5f) + ); + } + InputKeyValue2D(StickVector); + } +} + +void UGUIS_JoystickWidget::HandleTouchDelta(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) +{ + const FVector2D& ScreenSpacePos = InGestureEvent.GetScreenSpacePosition(); + + // The center of the geo locally is just its size + FVector2D LocalStickCenter = InGeometry.GetAbsoluteSize(); + + FVector2D ScreenSpaceStickCenter = InGeometry.LocalToAbsolute(LocalStickCenter); + // Get the offset from the origin + FVector2D MoveStickOffset = (ScreenSpacePos - ScreenSpaceStickCenter); + if (bNegateYAxis) + { + MoveStickOffset *= FVector2D(1.0f, -1.0f); + } + + FVector2D MoveStickDir = FVector2D::ZeroVector; + float MoveStickLength = 0.0f; + MoveStickOffset.ToDirectionAndLength(MoveStickDir, MoveStickLength); + + MoveStickLength = FMath::Min(MoveStickLength, StickRange); + MoveStickOffset = MoveStickDir * MoveStickLength; + + StickVector = MoveStickOffset / StickRange; +} + +void UGUIS_JoystickWidget::StopInputSimulation() +{ + TouchOrigin = FVector2D::ZeroVector; + StickVector = FVector2D::ZeroVector; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_SimulatedInputWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_SimulatedInputWidget.cpp new file mode 100644 index 0000000..e3a8832 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Mobile/GUIS_SimulatedInputWidget.cpp @@ -0,0 +1,183 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Mobile/GUIS_SimulatedInputWidget.h" +#include "Runtime/Launch/Resources/Version.h" +#include "EnhancedInputSubsystems.h" +#include "GUIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_SimulatedInputWidget) + +#define LOCTEXT_NAMESPACE "GUIS_SimulatedInputWidget" + +UGUIS_SimulatedInputWidget::UGUIS_SimulatedInputWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetConsumePointerInput(true); +} + +#if WITH_EDITOR +const FText UGUIS_SimulatedInputWidget::GetPaletteCategory() +{ + return LOCTEXT("PalleteCategory", "Generic UI"); +} +#endif // WITH_EDITOR + +void UGUIS_SimulatedInputWidget::NativeConstruct() +{ + // Find initial key, then listen for any changes to control mappings + QueryKeyToSimulate(); + + if (UEnhancedInputLocalPlayerSubsystem* System = GetEnhancedInputSubsystem()) + { + System->ControlMappingsRebuiltDelegate.AddUniqueDynamic(this, &UGUIS_SimulatedInputWidget::OnControlMappingsRebuilt); + } + + Super::NativeConstruct(); +} + +void UGUIS_SimulatedInputWidget::NativeDestruct() +{ + if (UEnhancedInputLocalPlayerSubsystem* System = GetEnhancedInputSubsystem()) + { + System->ControlMappingsRebuiltDelegate.RemoveAll(this); + } + + Super::NativeDestruct(); +} + +FReply UGUIS_SimulatedInputWidget::NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) +{ + FlushSimulatedInput(); + + return Super::NativeOnTouchEnded(InGeometry, InGestureEvent); +} + +UEnhancedInputLocalPlayerSubsystem* UGUIS_SimulatedInputWidget::GetEnhancedInputSubsystem() const +{ + if (APlayerController* PC = GetOwningPlayer()) + { + if (ULocalPlayer* LP = GetOwningLocalPlayer()) + { + return LP->GetSubsystem(); + } + } + return nullptr; +} + +UEnhancedPlayerInput* UGUIS_SimulatedInputWidget::GetPlayerInput() const +{ + if (UEnhancedInputLocalPlayerSubsystem* System = GetEnhancedInputSubsystem()) + { + return System->GetPlayerInput(); + } + return nullptr; +} + +void UGUIS_SimulatedInputWidget::InputKeyValue(const FVector& Value) +{ + const APlayerController* PC = GetOwningPlayer(); + const FPlatformUserId UserId = PC ? PC->GetPlatformUserId() : PLATFORMUSERID_NONE; + // If we have an associated input action then we can use it + if (AssociatedAction) + { + if (UEnhancedInputLocalPlayerSubsystem* System = GetEnhancedInputSubsystem()) + { + // We don't want to apply any modifiers or triggers to this action, but they are required for the function signature + TArray Modifiers; + TArray Triggers; + System->InjectInputVectorForAction(AssociatedAction, Value, Modifiers, Triggers); + } + } + // In case there is no associated input action, we can attempt to simulate input on the fallback key + else if (UEnhancedPlayerInput* Input = GetPlayerInput()) + { +#if ENGINE_MINOR_VERSION < 6 + if (KeyToSimulate.IsValid()) + { + FInputKeyParams Params; + Params.Delta = Value; + Params.Key = KeyToSimulate; + Params.NumSamples = 1; + Params.DeltaTime = GetWorld()->GetDeltaSeconds(); + Params.bIsGamepadOverride = KeyToSimulate.IsGamepadKey(); + + Input->InputKey(Params); + } +#else + const FInputDeviceId DeviceToSimulate = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(UserId); + if(KeyToSimulate.IsValid()) + { + const float DeltaTime = GetWorld()->GetDeltaSeconds(); + auto SimulateKeyPress = [Input, DeltaTime, DeviceToSimulate](const FKey& KeyToSim, const float Value, const EInputEvent Event) + { + FInputKeyEventArgs Args = FInputKeyEventArgs::CreateSimulated( + KeyToSim, + Event, + Value, + KeyToSim.IsAnalog() ? 1 : 0, + DeviceToSimulate); + + Args.DeltaTime = DeltaTime; + + Input->InputKey(Args); + }; + + // For keys which are the "root" of the key pair (such as Mouse2D + // being made up of the MouseX and MouseY keys) we should call InputKey for each key in the pair, + // not the paired key itself. This is so that the events accumulate correctly in + // the key state map of UPlayerInput. All input events + // from the message handler and viewport client work this way, so when we simulate key inputs, we should + // do so as well. + if (const EKeys::FPairedKeyDetails* PairDetails = EKeys::GetPairedKeyDetails(KeyToSimulate)) + { + SimulateKeyPress(PairDetails->XKeyDetails->GetKey(), Value.X, IE_Axis); + SimulateKeyPress(PairDetails->YKeyDetails->GetKey(), Value.Y, IE_Axis); + } + else + { + SimulateKeyPress(KeyToSimulate, Value.X, IE_Pressed); + } + } +#endif + } + else + { + UE_LOG(LogGUIS, Error, TEXT("'%s' is attempting to simulate input but has no player input!"), *GetNameSafe(this)); + } +} + +void UGUIS_SimulatedInputWidget::InputKeyValue2D(const FVector2D& Value) +{ + InputKeyValue(FVector(Value.X, Value.Y, 0.0)); +} + +void UGUIS_SimulatedInputWidget::FlushSimulatedInput() +{ + if (UEnhancedPlayerInput* Input = GetPlayerInput()) + { + Input->FlushPressedKeys(); + } +} + +void UGUIS_SimulatedInputWidget::QueryKeyToSimulate() +{ + if (UEnhancedInputLocalPlayerSubsystem* System = GetEnhancedInputSubsystem()) + { + TArray Keys = System->QueryKeysMappedToAction(AssociatedAction); + if (!Keys.IsEmpty() && Keys[0].IsValid()) + { + KeyToSimulate = Keys[0]; + } + else + { + KeyToSimulate = FallbackBindingKey; + } + } +} + +void UGUIS_SimulatedInputWidget::OnControlMappingsRebuilt() +{ + QueryKeyToSimulate(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModal.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModal.cpp new file mode 100644 index 0000000..a420e1f --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModal.cpp @@ -0,0 +1,56 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Modal/GUIS_GameModal.h" + +#include "CommonButtonBase.h" +#include "CommonTextBlock.h" +#include "Components/DynamicEntryBox.h" +#include "UI/Foundation/GUIS_ButtonBase.h" +#include "UI/Modal/GUIS_GameModalTypes.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameModal) + +#define LOCTEXT_NAMESPACE "GUIS_GameModal" + +UGUIS_GameModalWidget::UGUIS_GameModalWidget() +{ + bIsModal = true; +} + +void UGUIS_GameModalWidget::SetupModal(const UGUIS_ModalDefinition* ModalDefinition, FGUIS_ModalActionResultSignature ModalActionCallback) +{ + OnModalActionCallback = ModalActionCallback; + + EntryBox_Buttons->Reset([](UGUIS_ButtonBase& Button) + { + Button.OnClicked().Clear(); + }); + + Text_Header->SetText(ModalDefinition->Header); + Text_Body->SetText(ModalDefinition->Body); + + for (const auto& Pair : ModalDefinition->ModalActions) + { + UGUIS_ButtonBase* Button = EntryBox_Buttons->CreateEntry(!Pair.Value.ButtonType.IsNull() ? Pair.Value.ButtonType.LoadSynchronous() : nullptr); + Button->SetTriggeringInputAction(Pair.Value.InputAction); + Button->OnClicked().AddUObject(this, &ThisClass::CloseModal, Pair.Key); + if (!Pair.Value.DisplayText.IsEmpty()) + { + Button->SetButtonText(Pair.Value.DisplayText); + } + } + + OnSetupModal(ModalDefinition); +} + +void UGUIS_GameModalWidget::CloseModal(FGameplayTag ModalActionResult) +{ + DeactivateWidget(); + OnModalActionCallback.ExecuteIfBound(ModalActionResult); +} + +void UGUIS_GameModalWidget::KillModal() +{ +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModalTypes.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModalTypes.cpp new file mode 100644 index 0000000..ce71e04 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UI/Modal/GUIS_GameModalTypes.cpp @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UI/Modal/GUIS_GameModalTypes.h" + +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "UObject/UObjectHash.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameModalTypes) + + + diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionPointWidget.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionPointWidget.cpp new file mode 100644 index 0000000..e9000fc --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionPointWidget.cpp @@ -0,0 +1,257 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UIExtension/GUIS_GameUIExtensionPointWidget.h" +#include "Widgets/SOverlay.h" +#include "TimerManager.h" +#include "Widgets/Text/STextBlock.h" +#include "Editor/WidgetCompilerLog.h" +#include "Misc/UObjectToken.h" +#include "GameFramework/PlayerState.h" +#include "UI/Common/GUIS_UserWidgetInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIExtensionPointWidget) + +#define LOCTEXT_NAMESPACE "UIExtension" + +///////////////////////////////////////////////////// +// UGUIS_GameUIExtensionPointWidget + +UGUIS_GameUIExtensionPointWidget::UGUIS_GameUIExtensionPointWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UGUIS_GameUIExtensionPointWidget::ReleaseSlateResources(bool bReleaseChildren) +{ + ResetExtensionPoint(); + + Super::ReleaseSlateResources(bReleaseChildren); +} + +TSharedRef UGUIS_GameUIExtensionPointWidget::RebuildWidget() +{ + if (!IsDesignTime() && ExtensionPointTag.IsValid()) + { + ResetExtensionPoint(); + RegisterExtensionPoint(); + + RegisterForPlayerStateIfReady(); + } + + if (IsDesignTime()) + { + auto GetExtensionPointText = [this]() + { + return FText::Format(LOCTEXT("DesignTime_ExtensionPointLabel", "Extension Point\n{0}"), FText::FromName(ExtensionPointTag.GetTagName())); + }; + + TSharedRef MessageBox = SNew(SOverlay); + + MessageBox->AddSlot() + .Padding(5.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Justification(ETextJustify::Center) + .Text_Lambda(GetExtensionPointText) + ]; + + return MessageBox; + } + return Super::RebuildWidget(); +} + +void UGUIS_GameUIExtensionPointWidget::RegisterForPlayerStateIfReady() +{ + if (APlayerController* PC = GetOwningPlayer()) + { + if (APlayerState* PS = PC->GetPlayerState()) + { + RegisterExtensionPointForPlayerState(GetOwningLocalPlayer(), PS); + } + } + else + { + GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &ThisClass::OnCheckPlayerState, 0.2f); + } +} + +bool UGUIS_GameUIExtensionPointWidget::CheckPlayerState() +{ + if (APlayerController* PC = GetOwningPlayer()) + { + if (APlayerState* PS = PC->GetPlayerState()) + { + if (TimerHandle.IsValid()) + { + TimerHandle.Invalidate(); + } + RegisterExtensionPointForPlayerState(GetOwningLocalPlayer(), PS); + return true; + } + } + + return false; +} + +void UGUIS_GameUIExtensionPointWidget::OnCheckPlayerState() +{ + if (APlayerController* PC = GetOwningPlayer()) + { + if (APlayerState* PS = PC->GetPlayerState()) + { + if (TimerHandle.IsValid()) + { + TimerHandle.Invalidate(); + } + RegisterExtensionPointForPlayerState(GetOwningLocalPlayer(), PS); + } + } +} + +void UGUIS_GameUIExtensionPointWidget::ResetExtensionPoint() +{ + ResetInternal(); + + ExtensionMapping.Reset(); + for (FGUIS_GameUIExtPointHandle& Handle : ExtensionPointHandles) + { + Handle.Unregister(); + } + ExtensionPointHandles.Reset(); +} + +void UGUIS_GameUIExtensionPointWidget::RegisterExtensionPoint() +{ + if (UGUIS_ExtensionSubsystem* ExtensionSubsystem = GetWorld()->GetSubsystem()) + { + TArray AllowedDataClasses = LoadAllowedDataClasses(); + + ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPoint( + ExtensionPointTag, ExtensionPointTagMatch, AllowedDataClasses, + FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension) + )); + + ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPointForContext( + ExtensionPointTag, GetOwningLocalPlayer(), ExtensionPointTagMatch, AllowedDataClasses, + FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension) + )); + } +} + +void UGUIS_GameUIExtensionPointWidget::RegisterExtensionPointForPlayerState(ULocalPlayer* LocalPlayer, APlayerState* PlayerState) +{ + if (UGUIS_ExtensionSubsystem* ExtensionSubsystem = GetWorld()->GetSubsystem()) + { + TArray AllowedDataClasses = LoadAllowedDataClasses(); + + ExtensionPointHandles.Add(ExtensionSubsystem->RegisterExtensionPointForContext( + ExtensionPointTag, PlayerState, ExtensionPointTagMatch, AllowedDataClasses, + FExtendExtensionPointDelegate::CreateUObject(this, &ThisClass::OnAddOrRemoveExtension) + )); + } +} + +TArray UGUIS_GameUIExtensionPointWidget::LoadAllowedDataClasses() const +{ + TArray AllowedDataClasses; + AllowedDataClasses.Add(UUserWidget::StaticClass()); + + for (const TSoftClassPtr& DataClass : DataClasses) + { + if (!DataClass.IsNull()) + { + AllowedDataClasses.Add(DataClass.LoadSynchronous()); + } + } + return AllowedDataClasses; +} + +void UGUIS_GameUIExtensionPointWidget::OnAddOrRemoveExtension(EGUIS_GameUIExtAction Action, const FGUIS_GameUIExtRequest& Request) +{ + if (Action == EGUIS_GameUIExtAction::Added) + { + UObject* Data = Request.Data; + TSubclassOf WidgetClass(Cast(Data)); + if (WidgetClass) + { + UUserWidget* Widget = CreateEntryInternal(WidgetClass); + ExtensionMapping.Add(Request.ExtensionHandle, Widget); + + // Use UserWidgetInterface to notify it was registered. + if (Widget->GetClass()->ImplementsInterface(UGUIS_UserWidgetInterface::StaticClass())) + { + if (AActor* Actor = Cast(Request.ContextObject)) + { + IGUIS_UserWidgetInterface::Execute_SetOwningActor(Widget, Actor); + } + IGUIS_UserWidgetInterface::Execute_OnActivated(Widget); + } + } + else if (DataClasses.Num() > 0) + { + if (GetWidgetClassForData.IsBound()) + { + WidgetClass = GetWidgetClassForData.Execute(Data); + + // If the data is irrelevant they can just return no widget class. + if (WidgetClass) + { + if (UUserWidget* Widget = CreateEntryInternal(WidgetClass)) + { + ExtensionMapping.Add(Request.ExtensionHandle, Widget); + ConfigureWidgetForData.ExecuteIfBound(Widget, Data); + if (Widget->GetClass()->ImplementsInterface(UGUIS_UserWidgetInterface::StaticClass())) + { + if (AActor* Actor = Cast(Request.ContextObject)) + { + IGUIS_UserWidgetInterface::Execute_SetOwningActor(Widget, Actor); + } + IGUIS_UserWidgetInterface::Execute_OnActivated(Widget); + } + } + } + } + } + } + else + { + if (UUserWidget* Extension = ExtensionMapping.FindRef(Request.ExtensionHandle)) + { + if (Extension->GetClass()->ImplementsInterface(UGUIS_UserWidgetInterface::StaticClass())) + { + IGUIS_UserWidgetInterface::Execute_OnDeactivated(Extension); + if (AActor* Actor = Cast(Request.ContextObject)) + { + IGUIS_UserWidgetInterface::Execute_SetOwningActor(Extension, nullptr); + } + } + RemoveEntryInternal(Extension); + ExtensionMapping.Remove(Request.ExtensionHandle); + } + } +} + +#if WITH_EDITOR +void UGUIS_GameUIExtensionPointWidget::ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const +{ + Super::ValidateCompiledDefaults(CompileLog); + + // We don't care if the CDO doesn't have a specific tag. + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + if (!ExtensionPointTag.IsValid()) + { + TSharedRef Message = CompileLog.Error(FText::Format( + LOCTEXT("UGUIS_GameUIExtensionPointWidget_NoTag", "{0} has no ExtensionPointTag specified - All extension points must specify a tag so they can be located."), + FText::FromString(GetName()))); + Message->AddToken(FUObjectToken::Create(this)); + } + } +} +#endif + +///////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionSubsystem.cpp b/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionSubsystem.cpp new file mode 100644 index 0000000..7e37032 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Private/UIExtension/GUIS_GameUIExtensionSubsystem.cpp @@ -0,0 +1,414 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "UIExtension/GUIS_GameUIExtensionSubsystem.h" + +#include "GUIS_LogChannels.h" +#include "Blueprint/UserWidget.h" +#include "UObject/Stack.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIExtensionSubsystem) + +class FSubsystemCollectionBase; + +//========================================================= + +void FGUIS_GameUIExtPointHandle::Unregister() +{ + if (UGUIS_ExtensionSubsystem* ExtensionSourcePtr = ExtensionSource.Get()) + { + ExtensionSourcePtr->UnregisterExtensionPoint(*this); + ExtensionSource = nullptr; + DataPtr.Reset(); + } +} + +//========================================================= + +FGUIS_GameUIExtHandle::FGUIS_GameUIExtHandle() +{ +} + +FGUIS_GameUIExtHandle::FGUIS_GameUIExtHandle(UGUIS_ExtensionSubsystem* InExtensionSource, const TSharedPtr& InDataPtr) +{ + ExtensionSource = InExtensionSource; + DataPtr = InDataPtr; +} + +void FGUIS_GameUIExtHandle::Unregister() +{ + if (UGUIS_ExtensionSubsystem* ExtensionSourcePtr = ExtensionSource.Get()) + { + ExtensionSourcePtr->UnregisterExtension(*this); + ExtensionSource = nullptr; + DataPtr.Reset(); + } +} + +//========================================================= + +bool FGUIS_GameUIExtPoint::DoesExtensionPassContract(const FGUIS_GameUIExt* Extension) const +{ + if (UObject* DataPtr = Extension->Data) + { + const bool bMatchesContext = + (ContextObject.IsExplicitlyNull() && Extension->ContextObject.IsExplicitlyNull()) || + ContextObject == Extension->ContextObject; + + // Make sure the contexts match. + if (bMatchesContext) + { + // The data can either be the literal class of the data type, or a instance of the class type. + const UClass* DataClass = DataPtr->IsA(UClass::StaticClass()) ? Cast(DataPtr) : DataPtr->GetClass(); + for (const UClass* AllowedDataClass : AllowedDataClasses) + { + if (DataClass->IsChildOf(AllowedDataClass) || DataClass->ImplementsInterface(AllowedDataClass)) + { + return true; + } + } + } + } + + return false; +} + +FGUIS_GameUIExtPointHandle::FGUIS_GameUIExtPointHandle() +{ +} + +FGUIS_GameUIExtPointHandle::FGUIS_GameUIExtPointHandle(UGUIS_ExtensionSubsystem* InExtensionSource, const TSharedPtr& InDataPtr) +{ + ExtensionSource = InExtensionSource; + DataPtr = InDataPtr; +} + +//========================================================= + +void UGUIS_ExtensionSubsystem::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) +{ + Super::AddReferencedObjects(InThis, Collector); + if (UGUIS_ExtensionSubsystem* ExtensionSubsystem = Cast(InThis)) + { + for (auto MapIt = ExtensionSubsystem->ExtensionPointMap.CreateIterator(); MapIt; ++MapIt) + { + for (const TSharedPtr& ValueElement : MapIt.Value()) + { + Collector.AddReferencedObjects(ValueElement->AllowedDataClasses); + } + } + + for (auto MapIt = ExtensionSubsystem->ExtensionMap.CreateIterator(); MapIt; ++MapIt) + { + for (const TSharedPtr& ValueElement : MapIt.Value()) + { + Collector.AddReferencedObject(ValueElement->Data); + } + } + } +} + +void UGUIS_ExtensionSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); +} + +void UGUIS_ExtensionSubsystem::Deinitialize() +{ + Super::Deinitialize(); +} + +FGUIS_GameUIExtPointHandle UGUIS_ExtensionSubsystem::RegisterExtensionPoint(const FGameplayTag& ExtensionPointTag, EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, + const TArray& AllowedDataClasses, FExtendExtensionPointDelegate ExtensionCallback) +{ + return RegisterExtensionPointForContext(ExtensionPointTag, nullptr, ExtensionPointTagMatchType, AllowedDataClasses, ExtensionCallback); +} + +FGUIS_GameUIExtPointHandle UGUIS_ExtensionSubsystem::RegisterExtensionPointForContext(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, + EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, + const TArray& AllowedDataClasses, FExtendExtensionPointDelegate ExtensionCallback) +{ + if (!ExtensionPointTag.IsValid()) + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to register an invalid extension point.")); + return FGUIS_GameUIExtPointHandle(); + } + + if (!ExtensionCallback.IsBound()) + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to register an invalid extension point.")); + return FGUIS_GameUIExtPointHandle(); + } + + if (AllowedDataClasses.Num() == 0) + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to register an invalid extension point.")); + return FGUIS_GameUIExtPointHandle(); + } + + FExtensionPointList& List = ExtensionPointMap.FindOrAdd(ExtensionPointTag); + + TSharedPtr& Entry = List.Add_GetRef(MakeShared()); + Entry->ExtensionPointTag = ExtensionPointTag; + Entry->ContextObject = ContextObject; + Entry->ExtensionPointTagMatchType = ExtensionPointTagMatchType; + Entry->AllowedDataClasses = AllowedDataClasses; + Entry->Callback = MoveTemp(ExtensionCallback); + + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension Point [%s] Registered"), *ExtensionPointTag.ToString()); + + NotifyExtensionPointOfExtensions(Entry); + + return FGUIS_GameUIExtPointHandle(this, Entry); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::RegisterExtensionAsWidget(const FGameplayTag& ExtensionPointTag, TSubclassOf WidgetClass, int32 Priority) +{ + return RegisterExtensionAsData(ExtensionPointTag, nullptr, WidgetClass, Priority); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::RegisterExtensionAsWidgetForContext(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, TSubclassOf WidgetClass, int32 Priority) +{ + return RegisterExtensionAsData(ExtensionPointTag, ContextObject, WidgetClass, Priority); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::RegisterExtensionAsData(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, UObject* Data, int32 Priority) +{ + if (!ExtensionPointTag.IsValid()) + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to register an invalid extension.")); + return FGUIS_GameUIExtHandle(); + } + + if (!Data) + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to register an invalid extension.")); + return FGUIS_GameUIExtHandle(); + } + + FExtensionList& List = ExtensionMap.FindOrAdd(ExtensionPointTag); + + TSharedPtr& Entry = List.Add_GetRef(MakeShared()); + Entry->ExtensionPointTag = ExtensionPointTag; + Entry->ContextObject = ContextObject; + Entry->Data = Data; + Entry->Priority = Priority; + + if (ContextObject) + { + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension [%s] @ [%s] Registered"), *GetNameSafe(Data), *ExtensionPointTag.ToString()); + } + else + { + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension [%s] for [%s] @ [%s] Registered"), *GetNameSafe(Data), *GetNameSafe(ContextObject), *ExtensionPointTag.ToString()); + } + + NotifyExtensionPointsOfExtension(EGUIS_GameUIExtAction::Added, Entry); + + return FGUIS_GameUIExtHandle(this, Entry); +} + +void UGUIS_ExtensionSubsystem::NotifyExtensionPointOfExtensions(TSharedPtr& ExtensionPoint) +{ + for (FGameplayTag Tag = ExtensionPoint->ExtensionPointTag; Tag.IsValid(); Tag = Tag.RequestDirectParent()) + { + if (const FExtensionList* ListPtr = ExtensionMap.Find(Tag)) + { + // Copy in case there are removals while handling callbacks + FExtensionList ExtensionArray(*ListPtr); + + for (const TSharedPtr& Extension : ExtensionArray) + { + if (ExtensionPoint->DoesExtensionPassContract(Extension.Get())) + { + FGUIS_GameUIExtRequest Request = CreateExtensionRequest(Extension); + ExtensionPoint->Callback.ExecuteIfBound(EGUIS_GameUIExtAction::Added, Request); + } + } + } + + if (ExtensionPoint->ExtensionPointTagMatchType == EGUIS_GameUIExtPointMatchType::ExactMatch) + { + break; + } + } +} + +void UGUIS_ExtensionSubsystem::NotifyExtensionPointsOfExtension(EGUIS_GameUIExtAction Action, TSharedPtr& Extension) +{ + bool bOnInitialTag = true; + for (FGameplayTag Tag = Extension->ExtensionPointTag; Tag.IsValid(); Tag = Tag.RequestDirectParent()) + { + if (const FExtensionPointList* ListPtr = ExtensionPointMap.Find(Tag)) + { + // Copy in case there are removals while handling callbacks + FExtensionPointList ExtensionPointArray(*ListPtr); + + for (const TSharedPtr& ExtensionPoint : ExtensionPointArray) + { + if (bOnInitialTag || (ExtensionPoint->ExtensionPointTagMatchType == EGUIS_GameUIExtPointMatchType::PartialMatch)) + { + if (ExtensionPoint->DoesExtensionPassContract(Extension.Get())) + { + FGUIS_GameUIExtRequest Request = CreateExtensionRequest(Extension); + ExtensionPoint->Callback.ExecuteIfBound(Action, Request); + } + } + } + } + + bOnInitialTag = false; + } +} + +void UGUIS_ExtensionSubsystem::UnregisterExtension(const FGUIS_GameUIExtHandle& ExtensionHandle) +{ + if (ExtensionHandle.IsValid()) + { + checkf(ExtensionHandle.ExtensionSource == this, TEXT("Trying to unregister an extension that's not from this extension subsystem.")); + + TSharedPtr Extension = ExtensionHandle.DataPtr; + if (FExtensionList* ListPtr = ExtensionMap.Find(Extension->ExtensionPointTag)) + { + if (Extension->ContextObject.IsExplicitlyNull()) + { + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension [%s] @ [%s] Unregistered"), *GetNameSafe(Extension->Data), *Extension->ExtensionPointTag.ToString()); + } + else + { + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension [%s] for [%s] @ [%s] Unregistered"), *GetNameSafe(Extension->Data), *GetNameSafe(Extension->ContextObject.Get()), + *Extension->ExtensionPointTag.ToString()); + } + + NotifyExtensionPointsOfExtension(EGUIS_GameUIExtAction::Removed, Extension); + + ListPtr->RemoveSwap(Extension); + + if (ListPtr->Num() == 0) + { + ExtensionMap.Remove(Extension->ExtensionPointTag); + } + } + } + else + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to unregister an invalid Handle.")); + } +} + +void UGUIS_ExtensionSubsystem::UnregisterExtensionPoint(const FGUIS_GameUIExtPointHandle& ExtensionPointHandle) +{ + if (ExtensionPointHandle.IsValid()) + { + check(ExtensionPointHandle.ExtensionSource == this); + + const TSharedPtr ExtensionPoint = ExtensionPointHandle.DataPtr; + if (FExtensionPointList* ListPtr = ExtensionPointMap.Find(ExtensionPoint->ExtensionPointTag)) + { + UE_LOG(LogGUIS_Extension, Verbose, TEXT("Extension Point [%s] Unregistered"), *ExtensionPoint->ExtensionPointTag.ToString()); + + ListPtr->RemoveSwap(ExtensionPoint); + if (ListPtr->Num() == 0) + { + ExtensionPointMap.Remove(ExtensionPoint->ExtensionPointTag); + } + } + } + else + { + UE_LOG(LogGUIS_Extension, Warning, TEXT("Trying to unregister an invalid Handle.")); + } +} + +FGUIS_GameUIExtRequest UGUIS_ExtensionSubsystem::CreateExtensionRequest(const TSharedPtr& Extension) +{ + FGUIS_GameUIExtRequest Request; + Request.ExtensionHandle = FGUIS_GameUIExtHandle(this, Extension); + Request.ExtensionPointTag = Extension->ExtensionPointTag; + Request.Priority = Extension->Priority; + Request.Data = Extension->Data; + Request.ContextObject = Extension->ContextObject.Get(); + + return Request; +} + +FGUIS_GameUIExtPointHandle UGUIS_ExtensionSubsystem::K2_RegisterExtensionPoint(FGameplayTag ExtensionPointTag, EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, + const TArray>& AllowedDataClasses, + FExtendExtensionPointDynamicDelegate ExtensionCallback) +{ + TArray LoadedClasses; + + for (const TSoftClassPtr& DataClass : AllowedDataClasses) + { + if (!DataClass.IsNull()) + { + LoadedClasses.Add(DataClass.LoadSynchronous()); + } + } + return RegisterExtensionPoint(ExtensionPointTag, ExtensionPointTagMatchType, LoadedClasses, FExtendExtensionPointDelegate::CreateWeakLambda( + ExtensionCallback.GetUObject(), [this, ExtensionCallback](EGUIS_GameUIExtAction Action, const FGUIS_GameUIExtRequest& Request) + { + ExtensionCallback.ExecuteIfBound(Action, Request); + })); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::K2_RegisterExtensionAsWidget(FGameplayTag ExtensionPointTag, TSoftClassPtr WidgetClass, int32 Priority) +{ + if (!WidgetClass.IsNull()) + { + return RegisterExtensionAsWidget(ExtensionPointTag, WidgetClass.LoadSynchronous(), Priority); + } + return FGUIS_GameUIExtHandle(); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::K2_RegisterExtensionAsWidgetForContext(FGameplayTag ExtensionPointTag, TSoftClassPtr WidgetClass, UObject* ContextObject, int32 Priority) +{ + if (ContextObject && !WidgetClass.IsNull()) + { + return RegisterExtensionAsWidgetForContext(ExtensionPointTag, ContextObject, WidgetClass.LoadSynchronous(), Priority); + } + FFrame::KismetExecutionMessage(TEXT("A null ContextObject was passed to Register Extension (Widget For Context)"), ELogVerbosity::Error); + return FGUIS_GameUIExtHandle(); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::K2_RegisterExtensionAsData(FGameplayTag ExtensionPointTag, UObject* Data, int32 Priority) +{ + return RegisterExtensionAsData(ExtensionPointTag, nullptr, Data, Priority); +} + +FGUIS_GameUIExtHandle UGUIS_ExtensionSubsystem::K2_RegisterExtensionAsDataForContext(FGameplayTag ExtensionPointTag, UObject* ContextObject, UObject* Data, int32 Priority) +{ + if (ContextObject) + { + return RegisterExtensionAsData(ExtensionPointTag, ContextObject, Data, Priority); + } + FFrame::KismetExecutionMessage(TEXT("A null ContextObject was passed to Register Extension (Data For Context)"), ELogVerbosity::Error); + return FGUIS_GameUIExtHandle(); +} + +//========================================================= + +UGUIS_ExtensionFunctionLibrary::UGUIS_ExtensionFunctionLibrary() +{ +} + +void UGUIS_ExtensionFunctionLibrary::UnregisterExtension(FGUIS_GameUIExtHandle& Handle) +{ + Handle.Unregister(); +} + +bool UGUIS_ExtensionFunctionLibrary::IsValidExtension(FGUIS_GameUIExtHandle& Handle) +{ + return Handle.IsValid(); +} + +//========================================================= + +void UGUIS_ExtensionFunctionLibrary::UnregisterExtensionPoint(FGUIS_GameUIExtPointHandle& Handle) +{ + Handle.Unregister(); +} + +bool UGUIS_ExtensionFunctionLibrary::IsValidExtensionPoint(FGUIS_GameUIExtPointHandle& Handle) +{ + return Handle.IsValid(); +} diff --git a/Plugins/GGS/Source/GenericUISystem/Public/GUIS_GenericUISystemSettings.h b/Plugins/GGS/Source/GenericUISystem/Public/GUIS_GenericUISystemSettings.h new file mode 100644 index 0000000..f465dab --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/GUIS_GenericUISystemSettings.h @@ -0,0 +1,37 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Engine/DeveloperSettings.h" +#include "GUIS_GenericUISystemSettings.generated.h" + +class UGUIS_GameModalWidget; +class UGUIS_GameUIPolicy; + +/** + * Developer settings for the Generic UI System. + * 通用UI系统的开发者设置。 + */ +UCLASS(Config = Game, defaultconfig, meta = (DisplayName = "Generic UI System Settings")) +class GENERICUISYSTEM_API UGUIS_GenericUISystemSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Retrieves the Generic UI System settings. + * 获取通用UI系统设置。 + * @return The UI system settings. UI系统设置。 + */ + UFUNCTION(BlueprintPure, Category="GUIS|Settings", meta = (DisplayName = "Get Generic UI System Settings")) + static const UGUIS_GenericUISystemSettings* Get(); + + /** + * Default UI policy class for the game layout. + * 游戏布局的默认UI策略类。 + */ + UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="GUIS|Settings") + TSoftClassPtr GameUIPolicyClass; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/GUIS_LogChannels.h b/Plugins/GGS/Source/GenericUISystem/Public/GUIS_LogChannels.h new file mode 100644 index 0000000..3666ce1 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/GUIS_LogChannels.h @@ -0,0 +1,9 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + +GENERICUISYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGUIS, Log, All); +GENERICUISYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGUIS_Extension, Log, All); diff --git a/Plugins/GGS/Source/GenericUISystem/Public/GenericUISystem.h b/Plugins/GGS/Source/GenericUISystem/Public/GenericUISystem.h new file mode 100644 index 0000000..9fc3754 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/GenericUISystem.h @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +/** + * Module interface for the Generic UI System. + * 通用UI系统的模块接口。 + */ +class FGenericUISystemModule : public IModuleInterface +{ +public: + /** + * Called when the module is loaded. + * 模块加载时调用。 + */ + virtual void StartupModule() override; + + /** + * Called when the module is unloaded. + * 模块卸载时调用。 + */ + virtual void ShutdownModule() override; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_CreateWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_CreateWidget.h new file mode 100644 index 0000000..d645de0 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_CreateWidget.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/CancellableAsyncAction.h" +#include "UObject/SoftObjectPtr.h" + +#include "GUIS_AsyncAction_CreateWidget.generated.h" + +class APlayerController; +class UGameInstance; +class UUserWidget; +class UWorld; +struct FFrame; +struct FStreamableHandle; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGUIS_CreateWidgetSignature, UUserWidget *, UserWidget); + +/** + * Load the widget class asynchronously, the instance the widget after the loading completes, and return it on OnComplete. + */ +UCLASS(BlueprintType) +class GENERICUISYSTEM_API UGUIS_AsyncAction_CreateWidget : public UCancellableAsyncAction +{ + GENERATED_UCLASS_BODY() + +public: + virtual void Cancel() override; + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) + static UGUIS_AsyncAction_CreateWidget *CreateWidgetAsync(UObject *WorldContextObject, TSoftClassPtr UserWidgetSoftClass, APlayerController *OwningPlayer, + bool bSuspendInputUntilComplete = true); + + virtual void Activate() override; + +public: + UPROPERTY(BlueprintAssignable) + FGUIS_CreateWidgetSignature OnComplete; + +private: + void OnWidgetLoaded(); + + FName SuspendInputToken; + TWeakObjectPtr OwningPlayer; + TWeakObjectPtr World; + TWeakObjectPtr GameInstance; + bool bSuspendInputUntilComplete; + TSoftClassPtr UserWidgetSoftClass; + TSharedPtr StreamingHandle; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h new file mode 100644 index 0000000..fffd754 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h @@ -0,0 +1,59 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Engine/CancellableAsyncAction.h" +#include "GameplayTagContainer.h" +#include "UObject/SoftObjectPtr.h" + +#include "GUIS_AsyncAction_PushContentToUILayer.generated.h" + +class UGUIS_GameUILayout; +class APlayerController; +class UCommonActivatableWidget; +class UObject; +struct FFrame; +struct FStreamableHandle; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPushContentToLayerForPlayerAsyncDelegate, UCommonActivatableWidget *, UserWidget); + +/** + * + */ +UCLASS(BlueprintType) +class GENERICUISYSTEM_API UGUIS_AsyncAction_PushContentToUILayer : public UCancellableAsyncAction +{ + GENERATED_UCLASS_BODY() + virtual void Cancel() override; + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) + static UGUIS_AsyncAction_PushContentToUILayer* PushContentToUILayer(UGUIS_GameUILayout* UILayout, + UPARAM(meta = (AllowAbstract = false)) + TSoftClassPtr WidgetClass, + UPARAM(meta = (Categories = "UI.Layer,GUIS.Layer")) + FGameplayTag LayerName, bool bSuspendInputUntilComplete = true); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) + static UGUIS_AsyncAction_PushContentToUILayer* PushContentToUILayerForPlayer(APlayerController* PlayerController, + UPARAM(meta = (AllowAbstract = false)) + TSoftClassPtr WidgetClass, + UPARAM(meta = (Categories = "UI.Layer,GUIS.Layer")) + FGameplayTag LayerName, bool bSuspendInputUntilComplete = true); + + virtual void Activate() override; + + UPROPERTY(BlueprintAssignable) + FPushContentToLayerForPlayerAsyncDelegate BeforePush; + + UPROPERTY(BlueprintAssignable) + FPushContentToLayerForPlayerAsyncDelegate AfterPush; + +private: + FGameplayTag LayerName; + bool bSuspendInputUntilComplete = false; + TWeakObjectPtr OwningPlayerPtr; + TWeakObjectPtr RootLayout; + TSoftClassPtr WidgetClass; + + TSharedPtr StreamingHandle; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_ShowModel.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_ShowModel.h new file mode 100644 index 0000000..5b4c6f1 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_AsyncAction_ShowModel.h @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Engine/CancellableAsyncAction.h" +#include "UI/Modal/GUIS_GameModal.h" +#include "UObject/ObjectPtr.h" +#include "GUIS_AsyncAction_ShowModel.generated.h" + +enum class EGGF_DialogMessagingResult : uint8; +class FText; +class ULocalPlayer; +struct FFrame; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGUIS_ModalResultSignature, FGameplayTag, Result); + +/** + * Allows easily triggering an async confirmation dialog in blueprints that you can then wait on the result. + */ +UCLASS() +class UGUIS_AsyncAction_ShowModel : public UCancellableAsyncAction +{ + GENERATED_UCLASS_BODY() + +public: + // UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (BlueprintInternalUseOnly = "true", WorldContext = "InWorldContextObject")) + // static UGUIS_AsyncAction_ShowModel* ShowModal(UObject* InWorldContextObject, FGameplayTag ModalTag, UGUIS_ModalDefinition* ModalDefinition); + + /** + * 给定一个Modal定义,然后显示该Modal。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta = (BlueprintInternalUseOnly = "true", WorldContext = "InWorldContextObject")) + static UGUIS_AsyncAction_ShowModel* ShowModal(UObject* InWorldContextObject, TSoftClassPtr ModalDefinition); + + virtual void Activate() override; + +public: + UPROPERTY(BlueprintAssignable) + FGUIS_ModalResultSignature OnModalAction; + +private: + void HandleModalAction(FGameplayTag ModalActionTag); + + + UPROPERTY(Transient) + TObjectPtr WorldContextObject; + + // UPROPERTY(Transient) + // TObjectPtr TargetLocalPlayer; + + UPROPERTY(Transient) + TObjectPtr TargetPlayerController; + + UPROPERTY(Transient) + TSubclassOf ModalWidgetClass; + + UPROPERTY(Transient) + TObjectPtr ModalDefinition; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIAction.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIAction.h new file mode 100644 index 0000000..c36bd7e --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIAction.h @@ -0,0 +1,71 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "GUIS_UIAction.generated.h" + +class UGUIS_ModalDefinition; +class UGUIS_UIAction; + +/** + * Base ui action for single data. + */ +UCLASS(Blueprintable, EditInlineNew, CollapseCategories, DefaultToInstanced, Abstract, Const) +class GENERICUISYSTEM_API UGUIS_UIAction : public UObject +{ + GENERATED_UCLASS_BODY() + virtual UWorld* GetWorld() const override; + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + bool IsCompatible(const UObject* Data) const; + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + bool CanInvoke(const UObject* Data, APlayerController* PlayerController) const; + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + void InvokeAction(const UObject* Data, APlayerController* PlayerController) const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS|UIAction") + FText GetActionName() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS|UIAction") + FName GetActionID() const; + + const FDataTableRowHandle& GetInputActionData() const { return InputActionData; } + + bool GetShouldDisplayInActionBar() const { return bShouldDisplayInActionBar; } + + bool GetRequiresConfirmation() const { return bRequiresConfirmation; } + + TSoftClassPtr GetConfirmationModalClass() const { return ConfirmationModalClass; }; + +protected: + UFUNCTION(BlueprintNativeEvent, Category = "UIAction", meta = (DisplayName = "Is Compatible")) + bool IsCompatibleInternal(const UObject* Data) const; + + UFUNCTION(BlueprintNativeEvent, Category = "UIAction", meta = (DisplayName = "Can Invoke")) + bool CanInvokeInternal(const UObject* Data, APlayerController* PlayerController) const; + + UFUNCTION(BlueprintNativeEvent, Category = "UIAction", meta = (DisplayName = "Invoke Action")) + void InvokeActionInternal(const UObject* Data, APlayerController* PlayerController) const; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UIAction") + FText DisplayName; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UIAction") + FName ActionID; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UIAction", meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase")) + FDataTableRowHandle InputActionData; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="UIAction") + bool bShouldDisplayInActionBar{true}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="UIAction") + bool bRequiresConfirmation{true}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="UIAction", meta=(EditCondition="bRequiresConfirmation")) + TSoftClassPtr ConfirmationModalClass{nullptr}; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionFactory.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionFactory.h new file mode 100644 index 0000000..e86378c --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionFactory.h @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Engine/DataAsset.h" +#include "GUIS_UIActionFactory.generated.h" + +class UGUIS_UIAction; +/** + * 提供一种通用的方式为UI对象选择合适的可用操作。 + */ +UCLASS(BlueprintType, Const) +class GENERICUISYSTEM_API UGUIS_UIActionFactory : public UDataAsset +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + TArray FindAvailableUIActionsForData(const UObject* Data) const; + +protected: + /** + * A list of potential actions for incoming data. + * 针对传入数据的潜在可用ui操作。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GUIS", Instanced, meta=(TitleProperty="ActionId")) + TArray> PotentialActions; + +#if WITH_EDITOR + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionWidget.h new file mode 100644 index 0000000..1761bf5 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Actions/GUIS_UIActionWidget.h @@ -0,0 +1,78 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "GameplayTagContainer.h" +#include "GUIS_UIAction.h" +#include "GUIS_UIActionWidget.generated.h" + +class UGUIS_AsyncAction_ShowModel; +class UGUIS_UIActionFactory; + + +/** + * A widget which can associate data with register ui action dynamically based on passed-in data. + * @attention There's no visual for this widget. + * 此Widget可以关联一个数据,并为其自动注册可用的输入事件。 + */ +UCLASS(ClassGroup = GUIS, meta=(Category="Generic UI System"), AutoExpandCategories=("GUIS")) +class GENERICUISYSTEM_API UGUIS_UIActionWidget : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + void SetAssociatedData(UObject* Data); + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + void RegisterActions(); + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + virtual void RegisterActionsWithFactory(TSoftObjectPtr InActionFactory); + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + virtual void UnregisterActions(); + + UFUNCTION(BlueprintCallable, Category="GUIS|UIAction") + virtual void CancelAction(); + + + //~ Begin UWidget +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override; +#endif + //~ End UWidget interface + +protected: + UFUNCTION() + virtual void HandleUIAction(const UGUIS_UIAction* Action); + + UFUNCTION() + virtual void HandleModalAction(FGameplayTag ActionTag); + + UPROPERTY() + TWeakObjectPtr AssociatedData; + + /** + * A factory to get available ui actions for associated data. + * 该数据资产用于针对关联数据返回所有可用的ui操作。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="UIAction") + TObjectPtr ActionFactory; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGUIS_HandleUIActionSignature, const UGUIS_UIAction*, ActionReference); + + UPROPERTY(BlueprintAssignable, Category="UIAction") + FGUIS_HandleUIActionSignature OnHandleUIAction; + +private: + TArray ActionBindings; + + UPROPERTY() + TObjectPtr CurrentAction{nullptr}; + + UPROPERTY() + TObjectPtr ModalTask{nullptr}; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_DetailSectionsBuilder.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_DetailSectionsBuilder.h new file mode 100644 index 0000000..a380901 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_DetailSectionsBuilder.h @@ -0,0 +1,75 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "GUIS_DetailSectionsBuilder.generated.h" + +class UGUIS_ListEntryDetailSection; +class UGUIS_ListEntry; + +/** + * Base class for customizing how detail sections are gathered for an object. + * 自定义如何为对象收集细节部分的基类。 + */ +UCLASS(Abstract, Blueprintable, meta = (Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_DetailSectionsBuilder : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Gathers detail section classes for the specified data. + * 为指定数据收集细节部分类。 + * @param Data The data object. 数据对象。 + * @return Array of detail section classes. 细节部分类数组。 + */ + UFUNCTION(Blueprintable, BlueprintNativeEvent) + TArray> GatherDetailSections(const UObject* Data); + virtual TArray> GatherDetailSections_Implementation(const UObject* Data); +}; + +/** + * Structure for mapping object classes to detail section classes. + * 将对象类映射到细节部分类的结构。 + */ +USTRUCT() +struct GENERICUISYSTEM_API FGUIS_EntryDetailsClassSections +{ + GENERATED_BODY() + + /** + * Array of detail section classes for an object class. + * 对象类的细节部分类数组。 + */ + UPROPERTY(EditAnywhere, Category="GUIS") + TArray> Sections; +}; + +/** + * Concrete class for mapping object classes to detail sections. + * 将对象类映射到细节部分的实体类。 + */ +UCLASS(NotBlueprintable) +class GENERICUISYSTEM_API UGUIS_DetailSectionBuilder_Class : public UGUIS_DetailSectionsBuilder +{ + GENERATED_BODY() + +public: + /** + * Gathers detail section classes for the specified data. + * 为指定数据收集细节部分类。 + * @param Data The data object. 数据对象。 + * @return Array of detail section classes. 细节部分类数组。 + */ + virtual TArray> GatherDetailSections_Implementation(const UObject* Data) override; + +protected: + /** + * Mapping of object classes to their detail section configurations. + * 对象类到其细节部分配置的映射。 + */ + UPROPERTY(EditDefaultsOnly, Category="GUIS", meta = (AllowAbstract)) + TMap, FGUIS_EntryDetailsClassSections> SectionsForClasses; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntry.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntry.h new file mode 100644 index 0000000..f968376 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntry.h @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/IUserObjectListEntry.h" +#include "UI/Foundation/GUIS_ButtonBase.h" +#include "GUIS_ListEntry.generated.h" + +class UGUIS_UIAction; + +/** + * List entry widget representing an item in a ListView or TileView. + * 表示ListView或TileView中项的列表入口小部件。 + */ +UCLASS(Abstract, meta = (Category = "Generic UI", DisableNativeTick)) +class GENERICUISYSTEM_API UGUIS_ListEntry : public UGUIS_ButtonBase, public IUserObjectListEntry +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailSection.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailSection.h new file mode 100644 index 0000000..083f34c --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailSection.h @@ -0,0 +1,41 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "GUIS_ListEntryDetailSection.generated.h" + +/** + * Detail section widget for composing a detail view. + * 组成细节视图的细节部分小部件。 + */ +UCLASS(Abstract, meta = (Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_ListEntryDetailSection : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + /** + * Sets the object represented by this detail section. + * 设置此细节部分表示的对象。 + * @param ListItemObject The object to display. 要显示的对象。 + */ + void SetListItemObject(UObject* ListItemObject); + +protected: + /** + * Called when the list item object is set. + * 列表项对象设置时调用。 + * @param ListItemObject The object being set. 设置的对象。 + */ + virtual void NativeOnListItemObjectSet(UObject* ListItemObject); + + /** + * Blueprint event for when the list item object is set. + * 列表项对象设置时的蓝图事件。 + * @param ListItemObject The object being set. 设置的对象。 + */ + UFUNCTION(BlueprintImplementableEvent) + void OnListItemObjectSet(UObject* ListItemObject); +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailView.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailView.h new file mode 100644 index 0000000..dcedb01 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListEntryDetailView.h @@ -0,0 +1,109 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonUserWidget.h" +#include "Blueprint/UserWidgetPool.h" +#include "GUIS_ListEntryDetailView.generated.h" + +class UGUIS_DetailSectionsBuilder; +class UGUIS_ListEntryDetailSection; +class UVerticalBox; +class UGUIS_WidgetFactory; +struct FStreamableHandle; + +/** + * Detail view containing multiple sections to represent an item (any UObject). + * 包含多个细节部分的视图,用于展示一个项(任意UObject类型)。 + */ +UCLASS(Abstract, meta=(Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_ListEntryDetailView : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + /** + * Constructor for the detail view widget. + * 细节视图小部件构造函数。 + */ + UGUIS_ListEntryDetailView(const FObjectInitializer& ObjectInitializer); + + /** + * Sets the object represented by this detail view as data. + * 设置此细节视图表示的对象作为数据。 + * @param InListItemObject The object to display. 要显示的对象。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void SetListItemObject(UObject* InListItemObject); + + /** + * Sets the associated detail sections builder. + * 设置关联的细节部分构建器。 + * @param NewBuilder The detail sections builder. 细节部分构建器。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void SetSectionsBuilder(UGUIS_DetailSectionsBuilder* NewBuilder); + + /** + * Releases Slate resources. + * 释放Slate资源。 + * @param bReleaseChildren Whether to release child resources. 是否释放子资源。 + */ + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + +protected: + /** + * Called when the widget is constructed. + * 小部件构造时调用。 + */ + virtual void NativeConstruct() override; + + /** + * Called when the widget is initialized. + * 小部件初始化时调用。 + */ + virtual void NativeOnInitialized() override; + + /** + * Creates a detail section extension for the specified data. + * 为指定数据创建细节部分扩展。 + * @param InData The data object. 数据对象。 + * @param SectionClass The section widget class. 部分小部件类。 + */ + void CreateDetailsExtension(UObject* InData, TSubclassOf SectionClass); + + /** + * Detail sections builder for displaying data based on widget specifications. + * 根据小部件规格显示数据的细节部分构建器。 + */ + UPROPERTY(EditAnywhere, Category="GUIS", meta = (AllowAbstract = false)) + TObjectPtr SectionsBuilder; + + /** + * Pool for managing extension widgets. + * 管理扩展小部件的池。 + */ + UPROPERTY(Transient) + FUserWidgetPool ExtensionWidgetPool; + + /** + * Current object represented by the detail view. + * 细节视图当前表示的对象。 + */ + UPROPERTY(Transient) + TObjectPtr CurrentListItemObject; + + /** + * Handle for streaming assets. + * 流式加载资产的句柄。 + */ + TSharedPtr StreamingHandle; + +private: + /** + * Vertical box for detail sections. + * 细节部分的垂直框。 + */ + UPROPERTY(BlueprintReadOnly, Category="GUIS", meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true)) + TObjectPtr Box_DetailSections; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListView.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListView.h new file mode 100644 index 0000000..772bed6 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_ListView.h @@ -0,0 +1,61 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonListView.h" +#include "GUIS_ListView.generated.h" + +class UGUIS_WidgetFactory; + +/** + * Extended ListView allowing dynamic selection of entry widget class via data asset. + * 通过数据资产动态选择入口小部件类的扩展ListView。 + */ +UCLASS(Blueprintable, meta = (Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_ListView : public UCommonListView +{ + GENERATED_BODY() + +public: + /** + * Constructor for the ListView widget. + * ListView小部件构造函数。 + */ + UGUIS_ListView(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + +#if WITH_EDITOR + /** + * Validates compiled defaults in the editor. + * 在编辑器中验证编译默认值。 + * @param InCompileLog The widget compiler log. 小部件编译日志。 + */ + virtual void ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const override; +#endif + + /** + * Sets the entry widget factories for dynamic widget selection. + * 设置动态小部件选择的入口小部件工厂。 + * @param NewFactories The array of widget factories. 小部件工厂数组。 + */ + UFUNCTION(BlueprintCallable, Category = "ListEntries") + void SetEntryWidgetFactories(TArray NewFactories); + +protected: + /** + * Generates an entry widget for the specified item. + * 为指定项生成入口小部件。 + * @param Item The item to generate a widget for. 要生成小部件的项。 + * @param DesiredEntryClass The desired entry widget class. 期望的入口小部件类。 + * @param OwnerTable The owning table view. 所属表格视图。 + * @return The generated widget. 生成的小部件。 + */ + virtual UUserWidget& OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf DesiredEntryClass, const TSharedRef& OwnerTable) override; + + /** + * Array of widget factories for dynamic entry widget selection. + * 动态入口小部件选择的小部件工厂数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ListEntries", meta = (IsBindableEvent = "True")) + TArray> EntryWidgetFactories; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_TileView.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_TileView.h new file mode 100644 index 0000000..999c7f1 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_TileView.h @@ -0,0 +1,61 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonTileView.h" +#include "GUIS_TileView.generated.h" + +class UGUIS_WidgetFactory; + +/** + * Extended TileView allowing dynamic selection of entry widget class via data asset. + * 通过数据资产动态选择入口小部件类的扩展TileView。 + */ +UCLASS(Blueprintable, meta = (Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_TileView : public UCommonTileView +{ + GENERATED_BODY() + +public: + /** + * Constructor for the TileView widget. + * TileView小部件构造函数。 + */ + UGUIS_TileView(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + +#if WITH_EDITOR + /** + * Validates compiled defaults in the editor. + * 在编辑器中验证编译默认值。 + * @param InCompileLog The widget compiler log. 小部件编译日志。 + */ + virtual void ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const override; +#endif + + /** + * Sets the entry widget factories for dynamic widget selection. + * 设置动态小部件选择的入口小部件工厂。 + * @param NewFactories The array of widget factories. 小部件工厂数组。 + */ + UFUNCTION(BlueprintCallable, Category = "ListEntries") + void SetEntryWidgetFactories(TArray NewFactories); + +protected: + /** + * Generates an entry widget for the specified item. + * 为指定项生成入口小部件。 + * @param Item The item to generate a widget for. 要生成小部件的项。 + * @param DesiredEntryClass The desired entry widget class. 期望的入口小部件类。 + * @param OwnerTable The owning table view. 所属表格视图。 + * @return The generated widget. 生成的小部件。 + */ + virtual UUserWidget& OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf DesiredEntryClass, const TSharedRef& OwnerTable) override; + + /** + * Array of widget factories for dynamic entry widget selection. + * 动态入口小部件选择的小部件工厂数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ListEntries", meta = (IsBindableEvent = "True")) + TArray> EntryWidgetFactories; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_UserWidgetInterface.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_UserWidgetInterface.h new file mode 100644 index 0000000..8cfaf8a --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_UserWidgetInterface.h @@ -0,0 +1,63 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GUIS_UserWidgetInterface.generated.h" + +/** + * Interface for UserWidget functionality. + * 通用UserWidget功能的接口。 + * @note Designed for UserWidgets (except UCommonActivatableWidget and its derivatives). + * @注意 专为UserWidget设计(不包括UCommonActivatableWidget及其派生类)。 + * @details Automatically called when used as an extension UI. + * @细节 用作扩展UI时自动调用。 + */ +UINTERFACE() +class GENERICUISYSTEM_API UGUIS_UserWidgetInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Implementation class for UserWidget interface. + * UserWidget接口的实现类。 + */ +class GENERICUISYSTEM_API IGUIS_UserWidgetInterface +{ + GENERATED_BODY() + +public: + /** + * Retrieves the owning actor of the UserWidget. + * 获取UserWidget的所属演员。 + * @return The logical owning actor. 逻辑所属演员。 + * @note Tracks data for an actor in the game world. + * @注意 跟踪游戏世界中演员的数据。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GUIS") + AActor* GetOwningActor(); + + /** + * Sets the owning actor of the UserWidget. + * 设置UserWidget的所属演员。 + * @param NewOwningActor The new owning actor. 新的所属演员。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GUIS") + void SetOwningActor(AActor* NewOwningActor); + + /** + * Called when the UserWidget is activated. + * UserWidget激活时调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GUIS") + void OnActivated(); + + /** + * Called when the UserWidget is deactivated. + * UserWidget禁用时调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GUIS") + void OnDeactivated(); +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_WidgetFactory.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_WidgetFactory.h new file mode 100644 index 0000000..18d59d7 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Common/GUIS_WidgetFactory.h @@ -0,0 +1,57 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Engine/DataAsset.h" +#include "GUIS_WidgetFactory.generated.h" + +class UUserWidget; + +/** + * Base class for selecting appropriate widget classes for objects. + * 为对象选择合适小部件类的基类。 + */ +UCLASS(Abstract, Blueprintable, BlueprintType, HideDropdown, Const) +class GENERICUISYSTEM_API UGUIS_WidgetFactory : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGUIS_WidgetFactory(); + + /** + * Finds the appropriate widget class for the given data. + * 为给定数据查找合适的小部件类。 + * @param Data The data object. 数据对象。 + * @return The widget class. 小部件类。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GUIS") + TSubclassOf FindWidgetClassForData(const UObject *Data) const; + +protected: + /** + * Validates the data for the widget factory. + * 验证小部件工厂的数据。 + * @param ValidationMessage The validation message (output). 验证消息(输出)。 + * @return True if valid, false otherwise. 如果有效返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GUIS") + bool OnDataValidation(FText &ValidationMessage) const; + virtual bool OnDataValidation_Implementation(FText &ValidationMessage) const; + +#if WITH_EDITOR + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext &Context) const override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_ButtonBase.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_ButtonBase.h new file mode 100644 index 0000000..cf2a158 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_ButtonBase.h @@ -0,0 +1,65 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonButtonBase.h" +#include "GUIS_ButtonBase.generated.h" + +/** + * Base Button + * 基础按钮 + */ +UCLASS(Abstract, BlueprintType, Blueprintable, meta=(Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_ButtonBase : public UCommonButtonBase +{ + GENERATED_BODY() + +public: + /** + * @param InText The override text to display on the button. 该Text会覆盖按钮上的文字。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void SetButtonText(const FText& InText); + +protected: + // UUserWidget interface + virtual void NativePreConstruct() override; + // End of UUserWidget interface + + // UCommonButtonBase interface + virtual void UpdateInputActionWidget() override; + virtual void OnInputMethodChanged(ECommonInputType CurrentInputType) override; + // End of UCommonButtonBase interface + + void RefreshButtonText(); + + // 在PreConstruct,InputActionWidget更新,以及ButtonText变化时会触发。 + UFUNCTION(BlueprintImplementableEvent) + void OnUpdateButtonText(const FText& InText); + + /** + * Will use the text from InputActionWidget if not checked. + * 在PreConstruct,InputActionWidget更新,以及ButtonText变化时会触发。 + */ + UFUNCTION(BlueprintImplementableEvent) + void OnUpdateButtonStyle(); + + /** + * Will use the text from InputActionWidget if not checked. + * 不勾选的情况下,会使用来自InputActionWidget的显示文字。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Button", meta = (ExposeOnSpawn = true, InlineEditConditionToggle)) + bool bOverride_ButtonText{true}; + + /** + * The text to display on the button. + * 按钮的显示文字。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Button", meta = (ExposeOnSpawn = true, EditCondition = "bOverride_ButtonText")) + FText ButtonText; + +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override; +#endif // WITH_EDITOR +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabButtonBase.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabButtonBase.h new file mode 100644 index 0000000..63c8f03 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabButtonBase.h @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GUIS_ButtonBase.h" +#include "GUIS_TabListWidgetBase.h" +#include "GUIS_TabButtonBase.generated.h" + +class UCommonLazyImage; + +/** + * Button used for switching between tabs. + * 用于切换选项卡的按钮。 + */ +UCLASS(Abstract, Blueprintable, meta = (Category = "Generic UI", DisableNativeTick)) +class GENERICUISYSTEM_API UGUIS_TabButtonBase : public UGUIS_ButtonBase, public IGUIS_TabButtonInterface +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabDefinition.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabDefinition.h new file mode 100644 index 0000000..266bbf7 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabDefinition.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Engine/DataAsset.h" +#include "Styling/SlateBrush.h" +#include "GUIS_TabDefinition.generated.h" + +class UCommonActivatableWidget; +class UWidget; +class UCommonUserWidget; +class UCommonButtonBase; + +/** + * Base Tab definition. + * @attention Deprecated as it's unstable. + * 基础选项卡定义。 + * @注意 已经弃用 + */ +UCLASS(Blueprintable, EditInlineNew, CollapseCategories, Const, DefaultToInstanced, Deprecated) +class GENERICUISYSTEM_API UDEPRECATED_GUIS_TabDefinition : public UObject +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FName TabId; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FText TabText; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FSlateBrush IconBrush; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition", Transient) + bool bHidden; + + /** + * A common button which implements GUIS_TabButtonInterface to received Label infomation. + * 指定用作Tab按钮的Widget类型,该类型必须实现GUIS_TabButtonInterface以接收Label信息。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition", meta = (MustImplement = "/Script/GenericUISystem.GUIS_TabButtonInterface", AllowAbstract = "false")) + TSoftClassPtr TabButtonType; + + /** + * 该所呈现的Widget(可选),如果有指定,那么需要调用 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + TSoftClassPtr TabContentType; + + UPROPERTY(Transient) + TObjectPtr CreatedTabContentWidget; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabListWidgetBase.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabListWidgetBase.h new file mode 100644 index 0000000..2b52bc6 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Foundation/GUIS_TabListWidgetBase.h @@ -0,0 +1,320 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonTabListWidgetBase.h" +#include "GUIS_TabListWidgetBase.generated.h" + +class UDEPRECATED_GUIS_TabDefinition; +class UCommonButtonBase; +class UCommonUserWidget; +class UObject; +class UWidget; +struct FFrame; + +/** + * Structure defining a tab descriptor. + * 定义选项卡描述的结构。 + */ +USTRUCT(BlueprintType) +struct GENERICUISYSTEM_API FGUIS_TabDescriptor +{ + GENERATED_BODY() + + /** + * Default constructor. + * 默认构造函数。 + */ + FGUIS_TabDescriptor(); + + /** + * Unique identifier for the tab. + * 选项卡的唯一标识符。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FName TabId; + + /** + * Display text for the tab. + * 选项卡的显示文本。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FText TabText; + + /** + * Icon brush for the tab. + * 选项卡的图标画刷。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + FSlateBrush IconBrush; + + /** + * Whether the tab is hidden. + * 选项卡是否隐藏。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition", Transient) + bool bHidden{false}; + + /** + * Button type for the tab, must implement GUIS_TabButtonInterface. + * 选项卡的按钮类型,必须实现GUIS_TabButtonInterface。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition", meta = (MustImplement = "/Script/GenericUISystem.GUIS_TabButtonInterface", AllowAbstract = "false")) + TSoftClassPtr TabButtonType; + + /** + * Content widget type for the tab (optional). + * 选项卡的内容小部件类型(可选)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Tab Definition") + TSoftClassPtr TabContentType; + + /** + * The created content widget for the tab. + * 选项卡的已创建内容小部件。 + */ + UPROPERTY(Transient) + TObjectPtr CreatedTabContentWidget; +}; + +/** + * Interface for tab buttons. + * 选项卡按钮接口。 + */ +UINTERFACE(BlueprintType) +class GENERICUISYSTEM_API UGUIS_TabButtonInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for tab buttons to receive and update tab label information. + * 选项卡按钮接收和更新标签信息的接口。 + */ +class GENERICUISYSTEM_API IGUIS_TabButtonInterface +{ + GENERATED_BODY() + +public: + /** + * Sets the deprecated tab definition (no longer used). + * 设置已弃用的选项卡定义(不再使用)。 + * @param TabDefinition The tab definition. 选项卡定义。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Tab Button", meta=(DeprecatedFunction, DeprecationMessage="Use Set TabLabelInfo")) + void SetTabDefinition(const UDEPRECATED_GUIS_TabDefinition* TabDefinition); + + /** + * Sets the tab label information. + * 设置选项卡标签信息。 + * @param TabDescriptor The tab descriptor. 选项卡描述。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Tab Button") + void SetTabLabelInfo(const FGUIS_TabDescriptor& TabDescriptor); +}; + +/** + * Tab list widget for managing and displaying tabs. + * 管理和显示选项卡的选项卡列表小部件。 + * @note Can be linked to a switcher for dynamic tab switching. + * @注意 可关联到切换器以实现动态选项卡切换。 + */ +UCLASS(Blueprintable, BlueprintType, Abstract, meta = (DisableNativeTick)) +class GENERICUISYSTEM_API UGUIS_TabListWidgetBase : public UCommonTabListWidgetBase +{ + GENERATED_BODY() + +public: + /** + * Constructor for the tab list widget. + * 选项卡列表小部件构造函数。 + */ + UGUIS_TabListWidgetBase(); + + /** + * Retrieves preregistered tab information by ID. + * 通过ID获取预注册的选项卡信息。 + * @param TabNameId The tab ID. 选项卡ID。 + * @param OutTabInfo The tab descriptor (output). 选项卡描述(输出)。 + * @return True if found, false otherwise. 如果找到返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS|TabList") + bool GetPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo); + + /** + * Retrieves the index of a preregistered tab by ID. + * 通过ID获取预注册选项卡的索引。 + * @param TabNameId The tab ID. 选项卡ID。 + * @return The tab index. 选项卡索引。 + */ + UFUNCTION(BlueprintCallable, Category = "GUIS|TabList") + int32 GetPreregisteredTabIndex(FName TabNameId) const; + + /** + * Finds preregistered tab information by ID. + * 通过ID查找预注册的选项卡信息。 + * @param TabNameId The tab ID. 选项卡ID。 + * @param OutTabInfo The tab descriptor (output). 选项卡描述(输出)。 + * @return True if found, false otherwise. 如果找到返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GUIS|TabList", meta=(ExpandBoolAsExecs="ReturnValue")) + bool FindPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo); + + /** + * Toggles the hidden state of a tab (call before associating a switcher). + * 切换选项卡的隐藏状态(在关联切换器之前调用)。 + * @param TabNameId The tab ID. 选项卡ID。 + * @param bHidden Whether to hide the tab. 是否隐藏选项卡。 + */ + UFUNCTION(BlueprintCallable, Category = "GUIS|TabList") + void SetTabHiddenState(FName TabNameId, bool bHidden); + + /** + * Registers a dynamic tab. + * 注册动态选项卡。 + * @param TabDescriptor The tab descriptor. 选项卡描述。 + * @return True if successful, false otherwise. 如果成功返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS|TabList") + bool RegisterDynamicTab(const FGUIS_TabDescriptor& TabDescriptor); + + /** + * Checks if the first tab is active. + * 检查第一个选项卡是否激活。 + * @return True if the first tab is active. 如果第一个选项卡激活返回true。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS|TabList") + bool IsFirstTabActive() const; + + /** + * Checks if the last tab is active. + * 检查最后一个选项卡是否激活。 + * @return True if the last tab is active. 如果最后一个选项卡激活返回true。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS|TabList") + bool IsLastTabActive() const; + + /** + * Checks if a tab is visible. + * 检查选项卡是否可见。 + * @param TabId The tab ID. 选项卡ID。 + * @return True if the tab is visible. 如果选项卡可见返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS|TabList") + bool IsTabVisible(FName TabId); + + /** + * Retrieves the number of visible tabs. + * 获取可见选项卡的数量。 + * @return The number of visible tabs. 可见选项卡数量。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS|TabList") + int32 GetVisibleTabCount(); + + /** + * Delegate for when a new tab is created. + * 新选项卡创建时的委托。 + */ + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTabContentCreated, FName, TabId, UCommonUserWidget *, TabWidget); + + /** + * Native delegate for tab creation events. + * 选项卡创建事件的本地委托。 + */ + DECLARE_EVENT_TwoParams(UGUIS_TabListWidgetBase, FOnTabContentCreatedNative, FName /* TabId */, UCommonUserWidget * /* TabWidget */); + + /** + * Broadcasts when a new tab is created. + * 新选项卡创建时广播。 + */ + UPROPERTY(BlueprintAssignable, Category="GUIS|TabList") + FOnTabContentCreated OnTabContentCreated; + + /** + * Native event for tab creation. + * 选项卡创建的本地事件。 + */ + FOnTabContentCreatedNative OnTabContentCreatedNative; + +protected: + /** + * Called when the widget is initialized. + * 小部件初始化时调用。 + */ + virtual void NativeOnInitialized() override; + + /** + * Called when the widget is constructed. + * 小部件构造时调用。 + */ + virtual void NativeConstruct() override; + + /** + * Called when the widget is destructed. + * 小部件销毁时调用。 + */ + virtual void NativeDestruct() override; + + /** + * Called before the linked switcher changes. + * 关联切换器更改前调用。 + */ + virtual void HandlePreLinkedSwitcherChanged() override; + + /** + * Called after the linked switcher changes. + * 关联切换器更改后调用。 + */ + virtual void HandlePostLinkedSwitcherChanged() override; + + /** + * Handles tab creation. + * 处理选项卡创建。 + * @param TabId The tab ID. 选项卡ID。 + * @param TabButton The tab button. 选项卡按钮。 + */ + virtual void HandleTabCreation_Implementation(FName TabId, UCommonButtonBase* TabButton) override; + + /** + * Sets up the tabs. + * 设置选项卡。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS|TabList") + void SetupTabs(); + + /** + * Deprecated array of tab definitions. + * 已弃用的选项卡定义数组。 + */ + UPROPERTY(Instanced, meta = (BlueprintProtected, TitleProperty = "TabId")) + TArray> TabDefinitions_DEPRECATED; + + /** + * Array of preregistered tab descriptors. + * 预注册选项卡描述数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="TabList", meta=(TitleProperty="TabId")) + TArray PreregisteredTabInfoArray; + + /** + * Map of pending tab label information for runtime-registered tabs. + * 运行时注册选项卡的待处理标签信息映射。 + */ + UPROPERTY() + TMap PendingTabLabelInfoMap; + +#if WITH_EDITOR + /** + * Called after loading in the editor. + * 编辑器中加载后调用。 + */ + virtual void PostLoad() override; + + /** + * Validates compiled defaults in the editor. + * 在编辑器中验证编译默认值。 + * @param CompileLog The widget compiler log. 小部件编译日志。 + */ + virtual void ValidateCompiledDefaults(class IWidgetCompilerLog& CompileLog) const override; +#endif +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_ActivatableWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_ActivatableWidget.h new file mode 100644 index 0000000..bd3a046 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_ActivatableWidget.h @@ -0,0 +1,97 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonActivatableWidget.h" +#include "GUIS_ActivatableWidget.generated.h" + +struct FUIInputConfig; + +/** + * Enum defining input modes for activatable widgets. + * 定义可激活小部件输入模式的枚举。 + */ +UENUM() +enum class EGUIS_ActivatableWidgetInputMode : uint8 +{ + /** + * Default input mode. + * 默认输入模式。 + */ + Default, + + /** + * Allows both game and menu input. + * 允许游戏和菜单输入。 + */ + GameAndMenu, + + /** + * Game input only. + * 仅游戏输入。 + */ + Game, + + /** + * Menu input only. + * 仅菜单输入。 + */ + Menu +}; + +/** + * Activatable widget that manages input configuration when activated. + * 可激活小部件,激活时管理输入配置。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICUISYSTEM_API UGUIS_ActivatableWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + /** + * Constructor for the activatable widget. + * 可激活小部件构造函数。 + */ + UGUIS_ActivatableWidget(const FObjectInitializer& ObjectInitializer); + + /** + * Sets whether the widget handles back navigation. + * 设置小部件是否处理后退导航。 + * @param bNewState The new back handler state. 新的后退处理状态。 + */ + UFUNCTION(BlueprintCallable, Category = "GUIS|ActivatableWidget") + void SetIsBackHandler(bool bNewState); + + /** + * Retrieves the desired input configuration. + * 获取期望的输入配置。 + * @return The input configuration. 输入配置。 + */ + virtual TOptional GetDesiredInputConfig() const override; + +#if WITH_EDITOR + /** + * Validates the compiled widget tree in the editor. + * 在编辑器中验证编译的小部件树。 + * @param BlueprintWidgetTree The widget tree to validate. 要验证的小部件树。 + * @param CompileLog The widget compiler log. 小部件编译日志。 + */ + virtual void ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const override; +#endif + +protected: + /** + * Desired input mode when the widget is activated. + * 小部件激活时的期望输入模式。 + */ + UPROPERTY(EditDefaultsOnly, Category="Input") + EGUIS_ActivatableWidgetInputMode InputConfig = EGUIS_ActivatableWidgetInputMode::Default; + + /** + * Mouse capture behavior for game input. + * 游戏输入的鼠标捕获行为。 + */ + UPROPERTY(EditDefaultsOnly, Category="Input") + EMouseCaptureMode GameMouseCaptureMode = EMouseCaptureMode::CapturePermanently; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIContext.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIContext.h new file mode 100644 index 0000000..0fa8e16 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIContext.h @@ -0,0 +1,19 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GUIS_GameUIContext.generated.h" + +/** + * Base class for UI context data shared across multiple UI elements. + * 多个UI元素共享的UI上下文数据的基类。 + * @details Allows subclassing to add custom data for UI interactions. + * @细节 允许子类化以添加用于UI交互的自定义数据。 + */ +UCLASS(Abstract, Blueprintable, BlueprintType) +class GENERICUISYSTEM_API UGUIS_GameUIContext : public UObject +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIFunctionLibrary.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIFunctionLibrary.h new file mode 100644 index 0000000..bf61cb3 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIFunctionLibrary.h @@ -0,0 +1,191 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Blueprint/IUserObjectListEntry.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "UObject/SoftObjectPtr.h" + +#include "GUIS_GameUIFunctionLibrary.generated.h" + +class UGUIS_GameUILayout; +enum class ECommonInputType : uint8; +template +class TSubclassOf; + +class APlayerController; +class UCommonActivatableWidget; +class ULocalPlayer; +class UObject; +class UUserWidget; +struct FFrame; +struct FGameplayTag; + +/** + * Common functions for Game UI. + * 游戏UI的通用功能函数库。 + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_GameUIFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UGUIS_GameUIFunctionLibrary() + { + } + + /** + * Gets the input type of the owning player for the specified widget. + * 获取指定控件所属玩家的输入类型。 + * @param WidgetContextObject The widget to query for input type. 要查询输入类型的控件。 + * @return The input type of the owning player. 所属玩家的输入类型。 + */ + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WidgetContextObject")) + static ECommonInputType GetOwningPlayerInputType(const UUserWidget* WidgetContextObject); + + /** + * Checks if the owning player is using touch input. + * 检查所属玩家是否使用触摸输入。 + * @param WidgetContextObject The widget to query for input type. 要查询输入类型的控件。 + * @return True if the owning player is using touch input, false otherwise. 如果所属玩家使用触摸输入则返回true,否则返回false。 + */ + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WidgetContextObject")) + static bool IsOwningPlayerUsingTouch(const UUserWidget* WidgetContextObject); + + /** + * Checks if the owning player is using a gamepad. + * 检查所属玩家是否使用游戏手柄。 + * @param WidgetContextObject The widget to query for input type. 要查询输入类型的控件。 + * @return True if the owning player is using a gamepad, false otherwise. 如果所属玩家使用游戏手柄则返回true,否则返回false。 + */ + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category="GUIS", meta = (WorldContext = "WidgetContextObject")) + static bool IsOwningPlayerUsingGamepad(const UUserWidget* WidgetContextObject); + + /** + * Pushes a widget to the specified UI layer for the given player. + * 将控件推送到指定玩家的UI层。 + * @param PlayerController The player controller to associate with the UI layer. 与UI层关联的玩家控制器。 + * @param LayerName The tag identifying the UI layer. 标识UI层的标签。 + * @param WidgetClass The class of the widget to push. 要推送的控件类。 + * @return The created widget instance. 创建的控件实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta=(DynamicOutputParam="ReturnValue", DeterminesOutputType="WidgetClass")) + static UCommonActivatableWidget* PushContentToUILayer_ForPlayer(const APlayerController* PlayerController, UPARAM(meta = (Categories = "UI.Layer,GUIS.Layer")) + FGameplayTag LayerName, + UPARAM(meta = (AllowAbstract = false)) + TSubclassOf WidgetClass); + + /** + * Pops content from the specified UI layer for the given player. + * 从指定玩家的UI层中弹出内容。 + * @param PlayerController The player controller associated with the UI layer. 与UI层关联的玩家控制器。 + * @param LayerName The tag identifying the UI layer. 标识UI层的标签。 + * @param RemainNum Number of widgets to remain in the layer (-1 means remove all.). 保留在层中的控件数量(-1表示不保留)。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS") + static void PopContentFromUILayer_ForPlayer(const APlayerController* PlayerController, UPARAM(meta = (Categories = "UI.Layer,GUIS.Layer")) + FGameplayTag LayerName, int32 RemainNum = -1); + + /** + * Removes a specific activatable widget from the UI layer. + * 从UI层中移除指定的可激活控件。 + * @param ActivatableWidget The widget to remove. 要移除的控件。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta=(DefaultToSelf="ActivatableWidget")) + static void PopContentFromUILayer(UCommonActivatableWidget* ActivatableWidget); + + /** + * Pops multiple activatable widgets from the UI layer. + * 从UI层中弹出多个可激活控件。 + * @param ActivatableWidgets List of activatable widgets to pop. 要弹出的控件列表。 + * @param bReverse If true, pops in reverse array order. 如果为true,则按数组反向顺序弹出。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS", meta=(DefaultToSelf="ActivatableWidget")) + static void PopContentsFromUILayer(TArray ActivatableWidgets, bool bReverse = true); + + /** + * Gets the local player associated with the given player controller. + * 获取与指定玩家控制器关联的本地玩家。 + * @param PlayerController The player controller to query. 要查询的玩家控制器。 + * @return The associated local player. 关联的本地玩家。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintCosmetic, Category="GUIS") + static ULocalPlayer* GetLocalPlayerFromController(APlayerController* PlayerController); + + /** + * Gets the game UI layout for the specified player. + * 获取指定玩家的游戏UI布局。 + * @param PlayerController The player controller to query. 要查询的玩家控制器。 + * @return The game UI layout for the player. 玩家的游戏UI布局。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS") + static UGUIS_GameUILayout* GetGameUILayoutForPlayer(const APlayerController* PlayerController); + + /** + * Suspends input for the specified player with a reason. + * 暂停指定玩家的输入并提供原因。 + * @param PlayerController The player controller to suspend input for. 要暂停输入的玩家控制器。 + * @param SuspendReason The reason for suspending input. 暂停输入的原因。 + * @return A token representing the suspension. 表示暂停的令牌。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS") + static FName SuspendInputForPlayer(APlayerController* PlayerController, FName SuspendReason); + + /** + * Suspends input for the specified local player with a reason. + * 暂停指定本地玩家的输入并提供原因。 + * @param LocalPlayer The local player to suspend input for. 要暂停输入的本地玩家。 + * @param SuspendReason The reason for suspending input. 暂停输入的原因。 + * @return A token representing the suspension. 表示暂停的令牌。 + */ + static FName SuspendInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendReason); + + /** + * Resumes input for the specified player using the suspension token. + * 使用暂停令牌恢复指定玩家的输入。 + * @param PlayerController The player controller to resume input for. 要恢复输入的玩家控制器。 + * @param SuspendToken The token from the suspension call. 暂停调用时返回的令牌。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="GUIS") + static void ResumeInputForPlayer(APlayerController* PlayerController, FName SuspendToken); + + /** + * Resumes input for the specified local player using the suspension token. + * 使用暂停令牌恢复指定本地玩家的输入。 + * @param LocalPlayer The local player to resume input for. 要恢复输入的本地玩家。 + * @param SuspendToken The token from the suspension call. 暂停调用时返回的令牌。 + */ + static void ResumeInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendToken); + + /** + * Returns the typed item in the owning list view that this entry is currently assigned to represent. + * 以指定类型的方式获取Entry要展示的Item。 + * @param UserObjectListEntry The list entry to query (defaults to self). 要查询的列表条目(默认值为自身)。 + * @param DesiredClass The type of ListItemObject. 列表项对象的类型。 + * @return The typed item object. 类型化的列表项对象。 + */ + UFUNCTION(BlueprintPure, Category="GUIS", + meta = (DeterminesOutputType = "DesiredClass", DynamicOutputParam = "ReturnValue", DefaultToSelf = UserObjectListEntry, DisplayName = "Get List item Object")) + static UObject* GetTypedListItem(TScriptInterface UserObjectListEntry, TSubclassOf DesiredClass); + + /** + * Safely returns the typed item in the owning list view that this entry is currently assigned to represent. + * 安全地以指定类型的方式获取Entry要展示的Item。 + * @param UserObjectListEntry The list entry to query (defaults to self). 要查询的列表条目(默认值为自身)。 + * @param DesiredClass The type of ListItemObject. 列表项对象的类型。 + * @param OutItem The typed item object (output). 类型化的列表项对象(输出)。 + * @return True if the item was successfully retrieved, false otherwise. 如果成功获取项则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", + meta = (ExpandBoolAsExecs = "ReturnValue", DeterminesOutputType = "DesiredClass", DynamicOutputParam = "OutItemObject", DefaultToSelf = UserObjectListEntry, DisplayName = + "Get List item Object")) + static bool GetTypedListItemSafely(TScriptInterface UserObjectListEntry, TSubclassOf DesiredClass, UObject*& OutItem); + +private: + /** + * Tracks the number of active input suspensions. + * 跟踪当前活动的输入暂停数量。 + */ + static int32 InputSuspensions; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUILayout.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUILayout.h new file mode 100644 index 0000000..9b7e8b0 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUILayout.h @@ -0,0 +1,213 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonActivatableWidget.h" +#include "Engine/AssetManager.h" +#include "Engine/StreamableManager.h" +#include "GameplayTagContainer.h" +#include "GUIS_GameUIFunctionLibrary.h" +#include "Widgets/CommonActivatableWidgetContainer.h" // IWYU pragma: keep + +#include "GUIS_GameUILayout.generated.h" + +class APlayerController; +class UClass; +class UCommonActivatableWidgetContainerBase; +class ULocalPlayer; +class UObject; +struct FFrame; + +/** + * Enumeration for the state of an async UI widget layer load operation. + * 异步UI控件层加载操作状态的枚举。 + */ +enum class EGUIS_AsyncWidgetLayerState : uint8 +{ + Canceled, // Operation was canceled. 操作被取消。 + Initialize, // Widget is being initialized. 控件正在初始化。 + AfterPush // Widget has been pushed to the layer. 控件已被推送到层。 +}; + +/** + * Primary game UI layout for managing widget layers for a single player. + * 管理单个玩家控件层的游戏主UI布局。 + * @details Handles the layout, pushing, and display of UI layers for a player in a split-screen game. + * @细节 处理分屏游戏中玩家的UI层布局、推送和显示。 + */ +UCLASS(Abstract, meta = (DisableNativeTick)) +class GENERICUISYSTEM_API UGUIS_GameUILayout : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + /** + * Constructor for the game UI layout. + * 游戏UI布局的构造函数。 + */ + UGUIS_GameUILayout(const FObjectInitializer &ObjectInitializer); + + /** + * Sets the dormant state of the layout, limiting it to persistent actions. + * 设置布局的休眠状态,限制为持久动作。 + * @param Dormant True to set the layout as dormant, false otherwise. 如果设置为休眠则为true,否则为false。 + */ + void SetIsDormant(bool Dormant); + + /** + * Checks if the layout is in a dormant state. + * 检查布局是否处于休眠状态。 + * @return True if the layout is dormant, false otherwise. 如果布局休眠则返回true,否则返回false。 + */ + bool IsDormant() const { return bIsDormant; } + +public: + /** + * Asynchronously pushes a widget to a specified layer. + * 异步将控件推送到指定层。 + * @param LayerName The tag of the layer to push to. 要推送到的层标签。 + * @param bSuspendInputUntilComplete Whether to suspend input until loading is complete. 是否在加载完成前暂停输入。 + * @param ActivatableWidgetClass The class of the widget to push. 要推送的控件类。 + * @return A handle to the async streaming operation. 异步流操作的句柄。 + */ + template + TSharedPtr PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr ActivatableWidgetClass) + { + return PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, ActivatableWidgetClass, [](EGUIS_AsyncWidgetLayerState, ActivatableWidgetT *) {}); + } + + /** + * Asynchronously pushes a widget to a specified layer with a state callback. + * 使用状态回调异步将控件推送到指定层。 + * @param LayerName The tag of the layer to push to. 要推送到的层标签。 + * @param bSuspendInputUntilComplete Whether to suspend input until loading is complete. 是否在加载完成前暂停输入。 + * @param ActivatableWidgetClass The class of the widget to push. 要推送的控件类。 + * @param StateFunc Callback for handling widget state changes. 处理控件状态变化的回调。 + * @return A handle to the async streaming operation. 异步流操作的句柄。 + */ + template + TSharedPtr PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr ActivatableWidgetClass, + TFunction StateFunc) + { + static_assert(TIsDerivedFrom::IsDerived, "Only CommonActivatableWidgets can be used here"); + + static FName NAME_PushingWidgetToLayer("PushingWidgetToLayer"); + const FName SuspendInputToken = bSuspendInputUntilComplete ? UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(GetOwningPlayer(), NAME_PushingWidgetToLayer) : NAME_None; + + FStreamableManager &StreamableManager = UAssetManager::Get().GetStreamableManager(); + TSharedPtr StreamingHandle = StreamableManager.RequestAsyncLoad(ActivatableWidgetClass.ToSoftObjectPath(), FStreamableDelegate::CreateWeakLambda(this, + [this, LayerName, ActivatableWidgetClass, StateFunc, SuspendInputToken]() + { + UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken); + + ActivatableWidgetT *Widget = PushWidgetToLayerStack( + LayerName, ActivatableWidgetClass.Get(), [StateFunc](ActivatableWidgetT &WidgetToInit) + { StateFunc(EGUIS_AsyncWidgetLayerState::Initialize, &WidgetToInit); }); + + StateFunc(EGUIS_AsyncWidgetLayerState::AfterPush, Widget); + })); + + // Setup a cancel delegate to resume input if the operation is canceled. + StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this, + [this, StateFunc, SuspendInputToken]() + { + UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken); + StateFunc(EGUIS_AsyncWidgetLayerState::Canceled, nullptr); + })); + + return StreamingHandle; + } + + /** + * Pushes a widget to a specified layer. + * 将控件推送到指定层。 + * @param LayerName The tag of the layer to push to. 要推送到的层标签。 + * @param ActivatableWidgetClass The class of the widget to push. 要推送的控件类。 + * @return Pointer to the pushed widget, or nullptr if failed. 推送的控件指针,失败时为nullptr。 + */ + template + ActivatableWidgetT *PushWidgetToLayerStack(FGameplayTag LayerName, UClass *ActivatableWidgetClass) + { + return PushWidgetToLayerStack(LayerName, ActivatableWidgetClass, [](ActivatableWidgetT &) {}); + } + + /** + * Pushes a widget to a specified layer with initialization. + olduğuna * 将控件推送到指定层并进行初始化。 + * @param LayerName The tag of the layer to push to. 要推送到的层标签。 + * @param ActivatableWidgetClass The class of the widget to push. 要推送的控件类。 + * @param InitInstanceFunc Function to initialize the widget instance. 初始化控件实例的函数。 + * @return Pointer to the pushed widget, or nullptr if failed. 推送的控件指针,失败时为nullptr。 + */ + template + ActivatableWidgetT *PushWidgetToLayerStack(FGameplayTag LayerName, UClass *ActivatableWidgetClass, TFunctionRef InitInstanceFunc) + { + static_assert(TIsDerivedFrom::IsDerived, "Only CommonActivatableWidgets can be used here"); + + if (UCommonActivatableWidgetContainerBase *Layer = GetLayerWidget(LayerName)) + { + return Layer->AddWidget(ActivatableWidgetClass, InitInstanceFunc); + } + + return nullptr; + } + + /** + * Finds and removes a widget from any layer. + * 从任意层查找并移除控件。 + * @param ActivatableWidget The widget to remove. 要移除的控件。 + */ + void FindAndRemoveWidgetFromLayer(UCommonActivatableWidget *ActivatableWidget); + + /** + * Retrieves the layer widget for a given layer tag. + * 获取给定层标签的层控件。 + * @param LayerName The tag of the layer to retrieve. 要检索的层标签。 + * @return Pointer to the layer widget, or nullptr if not found. 层控件指针,未找到时为nullptr。 + */ + UCommonActivatableWidgetContainerBase *GetLayerWidget(FGameplayTag LayerName); + +protected: + /** + * Registers a layer for pushing widgets. + * 注册一个用于推送控件的层。 + * @param LayerTag The tag identifying the layer. 标识层的标签。 + * @param LayerWidget The widget container for the layer. 层的控件容器。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void RegisterLayer(UPARAM(meta = (Categories = "UI.Layer,GUIS.Layer")) FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase *LayerWidget); + + /** + * Called when the dormant state changes. + * 当休眠状态更改时调用。 + */ + virtual void OnIsDormantChanged(); + + /** + * Handles widget stack transitioning events. + * 处理控件堆栈转换事件。 + * @param Widget The widget container transitioning. 转换的控件容器。 + * @param bIsTransitioning True if transitioning, false otherwise. 如果正在转换则为true,否则为false。 + */ + void OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase *Widget, bool bIsTransitioning); + +private: + /** + * Indicates if the layout is dormant. + * 指示布局是否休眠。 + */ + bool bIsDormant = false; + + /** + * Tracks all suspended input tokens for async UI loading. + * 跟踪异步UI加载的所有暂停输入令牌。 + */ + TArray SuspendInputTokens; + + /** + * Registered layers for the primary layout. + * 主布局的注册层。 + */ + UPROPERTY(Transient, meta = (Categories = "UI.Layer,GUIS.Layer")) + TMap> Layers; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIPolicy.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIPolicy.h new file mode 100644 index 0000000..7491f33 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIPolicy.h @@ -0,0 +1,279 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GUIS_GameUIStructLibrary.h" +#include "Engine/World.h" +#include "Engine/DataTable.h" +#include "GUIS_GameUIPolicy.generated.h" + +class UCommonUserWidget; +class UGUIS_GameUIContext; +class ULocalPlayer; +class UGUIS_GameUISubsystem; +class UGUIS_GameUILayout; + +/** + * Enumeration for local multiplayer UI interaction modes. + * 本地多人UI交互模式的枚举。 + */ +UENUM() +enum class EGUIS_LocalMultiplayerInteractionMode : uint8 +{ + PrimaryOnly, // Fullscreen viewport for the primary player only. 仅为主玩家显示全屏视口。 + SingleToggle, // Fullscreen viewport with player swapping control. 全屏视口,玩家可切换控制。 + Simultaneous // Simultaneous viewports for all players. 为所有玩家同时显示视口。 +}; + +/** + * Manages UI layouts for different local players. + * 为不同本地玩家管理UI布局。 + * @details Controls the creation, addition, and removal of UI layouts and contexts. + * @细节 控制UI布局和上下文的创建、添加和移除。 + */ +UCLASS(Abstract, Blueprintable, Within = GUIS_GameUISubsystem) +class GENERICUISYSTEM_API UGUIS_GameUIPolicy : public UObject +{ + GENERATED_BODY() + +public: + /** + * Retrieves the UI policy for the specified world context as a specific type. + * 获取指定世界上下文的UI策略,转换为特定类型。 + * @param WorldContextObject The object providing world context. 提供世界上下文的对象。 + * @return The UI policy cast to the specified type. 转换为指定类型的UI策略。 + */ + template + static GameUIPolicyClass* GetGameUIPolicyAs(const UObject* WorldContextObject) + { + return Cast(GetGameUIPolicy(WorldContextObject)); + } + + /** + * Retrieves the UI policy for the specified world context. + * 获取指定世界上下文的UI策略。 + * @param WorldContextObject The object providing world context. 提供世界上下文的对象。 + * @return The UI policy. UI策略。 + */ + static UGUIS_GameUIPolicy* GetGameUIPolicy(const UObject* WorldContextObject); + + /** + * Retrieves the world associated with the UI policy. + * 获取与UI策略关联的世界。 + * @return The associated world. 关联的世界。 + */ + virtual UWorld* GetWorld() const override; + + /** + * Retrieves the owning game UI subsystem. + * 获取所属的游戏UI子系统。 + * @return The owning subsystem. 所属子系统。 + */ + UGUIS_GameUISubsystem* GetOwningSubsystem() const; + + /** + * Retrieves the root layout for a local player. + * 获取本地玩家的根布局。 + * @param LocalPlayer The local player. 本地玩家。 + * @return The root layout for the player. 玩家的根布局。 + */ + UGUIS_GameUILayout* GetRootLayout(const ULocalPlayer* LocalPlayer) const; + + /** + * Retrieves a UI context for a local player. + * 获取本地玩家的UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param ContextClass The class of the context to retrieve. 要检索的上下文类。 + * @return The UI context, or nullptr if not found. UI上下文,未找到时为nullptr。 + */ + virtual UGUIS_GameUIContext* GetContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass); + + /** + * Adds a UI context for a local player. + * 为本地玩家添加UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param NewContext The UI context to add. 要添加的UI上下文。 + * @return True if the context was added, false otherwise. 如果上下文添加成功则返回true,否则返回false。 + */ + virtual bool AddContext(const ULocalPlayer* LocalPlayer, UGUIS_GameUIContext* NewContext); + + /** + * Finds a UI context for a local player. + * 查找本地玩家的UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param ContextClass The class of the context to find. 要查找的上下文类。 + * @return The found UI context, or nullptr if not found. 找到的UI上下文,未找到时为nullptr。 + */ + virtual UGUIS_GameUIContext* FindContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass); + + /** + * Removes a UI context for a local player. + * 移除本地玩家的UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param ContextClass The class of the context to remove. 要移除的上下文类。 + */ + virtual void RemoveContext(const ULocalPlayer* LocalPlayer, TSubclassOf ContextClass); + + /** + * Adds a UI action binding for a local player. + * 为本地玩家添加UI动作绑定。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Target The target widget for the action. 动作的目标控件。 + * @param InputAction The input action to bind. 要绑定的输入动作。 + * @param bShouldDisplayInActionBar Whether to display in the action bar. 是否在动作栏中显示。 + * @param Callback Delegate called when the action is executed. 动作执行时调用的委托。 + * @param BindingHandle The handle for the binding. 绑定的句柄。 + */ + virtual void AddUIAction(const ULocalPlayer* LocalPlayer, UCommonUserWidget* Target, const FDataTableRowHandle& InputAction, bool bShouldDisplayInActionBar, + const FGUIS_UIActionExecutedDelegate& Callback, + FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Removes a UI action binding for a local player. + * 为本地玩家移除UI动作绑定。 + * @param LocalPlayer The local player. 本地玩家。 + * @param BindingHandle The handle of the binding to remove. 要移除的绑定句柄。 + */ + virtual void RemoveUIAction(const ULocalPlayer* LocalPlayer, FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Gets the current multiplayer interaction mode. + * 获取当前的多人交互模式。 + * @return The current interaction mode. 当前交互模式。 + */ + EGUIS_LocalMultiplayerInteractionMode GetLocalMultiplayerInteractionMode() const { return LocalMultiplayerInteractionMode; } + + /** + * Requests primary control for a specific layout. + * 为特定布局请求主要控制权。 + * @param Layout The layout requesting primary control. 请求主要控制权的布局。 + */ + void RequestPrimaryControl(UGUIS_GameUILayout* Layout); + +protected: + /** + * Adds a layout to the viewport for a local player. + * 为本地玩家将布局添加到视口。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The layout to add. 要添加的布局。 + */ + void AddLayoutToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Removes a layout from the viewport for a local player. + * 为本地玩家从视口中移除布局。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The layout to remove. 要移除的布局。 + */ + void RemoveLayoutFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Called when a root layout is added to the viewport. + * 当根布局添加到视口时调用。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The added layout. 添加的布局。 + */ + virtual void OnRootLayoutAddedToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Blueprint event for when a root layout is added to the viewport. + * 当根布局添加到视口时的蓝图事件。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The added layout. 添加的布局。 + */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnRootLayoutAddedToViewport")) + void BP_OnRootLayoutAddedToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Called when a root layout is removed from the viewport. + * 当根布局从视口中移除时调用。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The removed layout. 移除的布局。 + */ + virtual void OnRootLayoutRemovedFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Blueprint event for when a root layout is removed from the viewport. + * 当根布局从视口中移除时的蓝图事件。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The removed layout. 移除的布局。 + */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnRootLayoutRemovedFromViewport")) + void BP_OnRootLayoutRemovedFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Called when a root layout is released. + * 当根布局被释放时调用。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The released layout. 释放的布局。 + */ + virtual void OnRootLayoutReleased(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Blueprint event for when a root layout is released. + * 当根布局被释放时的蓝图事件。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Layout The released layout. 释放的布局。 + */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnRootLayoutReleased")) + void BP_OnRootLayoutReleased(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout); + + /** + * Creates a layout widget for a local player. + * 为本地玩家创建布局控件。 + * @param LocalPlayer The local player. 本地玩家。 + */ + void CreateLayoutWidget(ULocalPlayer* LocalPlayer); + + /** + * Retrieves the layout widget class for a local player. + * 获取本地玩家的布局控件类。 + * @param LocalPlayer The local player. 本地玩家。 + * @return The layout widget class. 布局控件类。 + */ + TSubclassOf GetLayoutWidgetClass(ULocalPlayer* LocalPlayer); + +private: + /** + * Current multiplayer interaction mode. + * 当前的多人交互模式。 + */ + EGUIS_LocalMultiplayerInteractionMode LocalMultiplayerInteractionMode = EGUIS_LocalMultiplayerInteractionMode::PrimaryOnly; + + /** + * The class used for the game UI layout. + * 用于游戏UI布局的类。 + */ + UPROPERTY(EditAnywhere, Category="GUIS") + TSoftClassPtr LayoutClass; + + /** + * Array of root viewport layout information. + * 根视口布局信息的数组。 + */ + UPROPERTY(Transient) + TArray RootViewportLayouts; + + /** + * Notifies when a player is added. + * 当玩家被添加时通知。 + * @param LocalPlayer The added local player. 添加的本地玩家。 + */ + void NotifyPlayerAdded(ULocalPlayer* LocalPlayer); + + /** + * Notifies when a player is removed. + * 当玩家被移除时通知。 + * @param LocalPlayer The removed local player. 移除的本地玩家。 + */ + void NotifyPlayerRemoved(ULocalPlayer* LocalPlayer); + + /** + * Notifies when a player is destroyed. + * 当玩家被销毁时通知。 + * @param LocalPlayer The destroyed local player. 销毁的本地玩家。 + */ + void NotifyPlayerDestroyed(ULocalPlayer* LocalPlayer); + + friend class UGUIS_GameUISubsystem; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIStructLibrary.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIStructLibrary.h new file mode 100644 index 0000000..0290da5 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUIStructLibrary.h @@ -0,0 +1,146 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Input/UIActionBindingHandle.h" +#include "UObject/Object.h" +#include "GUIS_GameUIStructLibrary.generated.h" + +class UGUIS_GameUIContext; +class UGUIS_GameUILayout; +class ULocalPlayer; + +/** + * Delegate for handling UI action execution. + * 处理UI动作执行的委托。 + * @param ActionName The name of the executed action. 执行的动作名称。 + */ +DECLARE_DYNAMIC_DELEGATE_OneParam(FGUIS_UIActionExecutedDelegate, FName, ActionName); + +/** + * Struct for storing UI action binding information. + * 存储UI动作绑定信息的结构体。 + */ +USTRUCT(BlueprintType) +struct FGUIS_UIActionBindingHandle +{ + GENERATED_BODY() + + /** + * Unique identifier for the binding. + * 绑定的唯一标识符。 + */ + FName Id; + + /** + * Handle for the UI action binding. + * UI动作绑定的句柄。 + */ + FUIActionBindingHandle Handle; +}; + +/** + * Struct for storing UI context binding information. + * 存储UI上下文绑定信息的结构体。 + */ +USTRUCT(BlueprintType) +struct FGUIS_UIContextBindingHandle +{ + GENERATED_BODY() + + FGUIS_UIContextBindingHandle() + { + }; + + /** + * Constructor for UI context binding handle. + * UI上下文绑定句柄的构造函数。 + * @param InLocalPlayer The local player associated with the context. 与上下文关联的本地玩家。 + * @param InContextClass The class of the context. 上下文的类。 + */ + FGUIS_UIContextBindingHandle(ULocalPlayer* InLocalPlayer, UClass* InContextClass); + + /** + * The local player associated with the context. + * 与上下文关联的本地玩家。 + */ + UPROPERTY() + TObjectPtr LocalPlayer; + + /** + * The class of the UI context. + * UI上下文的类。 + */ + UPROPERTY() + UClass* ContextClass{nullptr}; +}; + +/** + * Struct for storing root viewport layout information. + * 存储根视口布局信息的结构体。 + */ +USTRUCT() +struct FGUIS_RootViewportLayoutInfo +{ + GENERATED_BODY() + + /** + * The local player associated with the layout. + * 与布局关联的本地玩家。 + */ + UPROPERTY(Transient) + TObjectPtr LocalPlayer = nullptr; + + /** + * The root layout widget. + * 根布局控件。 + */ + UPROPERTY(Transient) + TObjectPtr RootLayout = nullptr; + + /** + * Indicates if the layout is added to the viewport. + * 指示布局是否已添加到视口。 + */ + UPROPERTY(Transient) + bool bAddedToViewport = false; + + /** + * Array of UI contexts associated with the layout. + * 与布局关联的UI上下文数组。 + */ + UPROPERTY(Transient) + TArray> Contexts; + + /** + * Array of UI action binding handles. + * UI动作绑定句柄的数组。 + */ + UPROPERTY(Transient) + TArray BindingHandles; + + FGUIS_RootViewportLayoutInfo() + { + } + + /** + * Constructor for root viewport layout information. + * 根视口布局信息的构造函数。 + * @param InLocalPlayer The local player. 本地玩家。 + * @param InRootLayout The root layout widget. 根布局控件。 + * @param bIsInViewport Whether the layout is in the viewport. 布局是否在视口中。 + */ + FGUIS_RootViewportLayoutInfo(ULocalPlayer* InLocalPlayer, UGUIS_GameUILayout* InRootLayout, bool bIsInViewport) + : LocalPlayer(InLocalPlayer), RootLayout(InRootLayout), bAddedToViewport(bIsInViewport) + { + } + + /** + * Equality operator to compare with a local player. + * 与本地玩家比较的相等运算符。 + * @param OtherLocalPlayer The local player to compare with. 要比较的本地玩家。 + * @return True if the local players match, false otherwise. 如果本地玩家匹配则返回true,否则返回false。 + */ + bool operator==(const ULocalPlayer* OtherLocalPlayer) const { return LocalPlayer == OtherLocalPlayer; } +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUISubsystem.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUISubsystem.h new file mode 100644 index 0000000..718409f --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameUISubsystem.h @@ -0,0 +1,223 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Input/UIActionBindingHandle.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "GUIS_GameUIStructLibrary.h" +#include "UObject/SoftObjectPtr.h" +#include "GUIS_GameUISubsystem.generated.h" + +class UCommonUserWidget; +class UGUIS_GameUIContext; +class FSubsystemCollectionBase; +class ULocalPlayer; +class UGUIS_GameUIPolicy; +class UObject; + +/** + * Game UI subsystem for managing UI policies and player UI interactions. + * 管理UI策略和玩家UI交互的游戏UI子系统。 + * @details Intended to be subclassed for game-specific UI functionality. + * @细节 旨在为特定游戏的UI功能进行子类化。 + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_GameUISubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UGUIS_GameUISubsystem() + { + } + + /** + * Initializes the UI subsystem. + * 初始化UI子系统。 + * @param Collection The subsystem collection base. 子系统集合基类。 + */ + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + /** + * Deinitializes the UI subsystem. + * 取消初始化UI子系统。 + */ + virtual void Deinitialize() override; + + /** + * Determines if the subsystem should be created. + * 确定是否应创建子系统。 + * @param Outer The outer object. 外部对象。 + * @return True if the subsystem should be created, false otherwise. 如果应创建子系统则返回true,否则返回false。 + */ + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + /** + * Gets the current UI policy (const version). + * 获取当前UI策略(常量版本)。 + * @return The current UI policy. 当前UI策略。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS") + const UGUIS_GameUIPolicy* GetCurrentUIPolicy() const { return CurrentPolicy; } + + /** + * Gets the current UI policy (non-const version). + * 获取当前UI策略(非常量版本)。 + * @return The current UI policy. 当前UI策略。 + */ + UGUIS_GameUIPolicy* GetCurrentUIPolicy() { return CurrentPolicy; } + + /** + * Adds a player to the UI subsystem. + * 将玩家添加到UI子系统。 + * @param LocalPlayer The local player to add. 要添加的本地玩家。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + virtual void AddPlayer(ULocalPlayer* LocalPlayer); + + /** + * Removes a player from the UI subsystem. + * 从UI子系统中移除玩家。 + * @param LocalPlayer The local player to remove. 要移除的本地玩家。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + virtual void RemovePlayer(ULocalPlayer* LocalPlayer); + + /** + * Notifies the subsystem when a player is added. + * 当玩家被添加时通知子系统。 + * @param LocalPlayer The added local player. 添加的本地玩家。 + */ + virtual void NotifyPlayerAdded(ULocalPlayer* LocalPlayer); + + /** + * Notifies the subsystem when a player is removed. + * 当玩家被移除时通知子系统。 + * @param LocalPlayer The removed local player. 移除的本地玩家。 + */ + virtual void NotifyPlayerRemoved(ULocalPlayer* LocalPlayer); + + /** + * Notifies the subsystem when a player is destroyed. + * 当玩家被销毁时通知子系统。 + * @param LocalPlayer The destroyed local player. 销毁的本地玩家。 + */ + virtual void NotifyPlayerDestroyed(ULocalPlayer* LocalPlayer); + + /** + * Registers a UI action binding for a widget (deprecated). + * 为控件注册UI动作绑定(已弃用)。 + * @param Target The target widget. 目标控件。 + * @param InputAction The input action to bind. 要绑定的输入动作。 + * @param bShouldDisplayInActionBar Whether to display in the action bar. 是否在动作栏中显示。 + * @param Callback Delegate called when the action is executed. 动作执行时调用的委托。 + * @param BindingHandle The handle for the binding. 绑定的句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="Target", DeprecatedFunction, DeprecationMessage="Use RegisterUIActionBindingForPlayer")) + void RegisterUIActionBinding(UCommonUserWidget* Target, FDataTableRowHandle InputAction, bool bShouldDisplayInActionBar, const FGUIS_UIActionExecutedDelegate& Callback, + FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Unregisters a UI action binding (deprecated). + * 取消注册UI动作绑定(已弃用)。 + * @param BindingHandle The handle of the binding to unregister. 要取消注册的绑定句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DeprecatedFunction, DeprecationMessage="Use UnregisterUIActionBindingForPlayer")) + void UnregisterBinding(UPARAM(ref) FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Registers a UI action binding for a specific player. + * 为特定玩家注册UI动作绑定。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Target The target widget. 目标控件。 + * @param InputAction The input action to bind. 要绑定的输入动作。 + * @param bShouldDisplayInActionBar Whether to display in the action bar. 是否在动作栏中显示。 + * @param Callback Delegate called when the action is executed. 动作执行时调用的委托。 + * @param BindingHandle The handle for the binding. 绑定的句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="Target")) + virtual void RegisterUIActionBindingForPlayer(ULocalPlayer* LocalPlayer, UCommonUserWidget* Target, FDataTableRowHandle InputAction, bool bShouldDisplayInActionBar, + const FGUIS_UIActionExecutedDelegate& Callback, + FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Unregisters a UI action binding for a specific player. + * 为特定玩家取消注册UI动作绑定。 + * @param LocalPlayer The local player. 本地玩家。 + * @param BindingHandle The handle of the binding to unregister. 要取消注册的绑定句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + virtual void UnregisterUIActionBindingForPlayer(ULocalPlayer* LocalPlayer, UPARAM(ref) FGUIS_UIActionBindingHandle& BindingHandle); + + /** + * Registers a UI context for a specific player. + * 为特定玩家注册UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param Context The UI context to register. 要注册的UI上下文。 + * @param BindingHandle The handle for the context binding. 上下文绑定的句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="LocalPlayer")) + void RegisterUIContextForPlayer(ULocalPlayer* LocalPlayer, UGUIS_GameUIContext* Context, FGUIS_UIContextBindingHandle& BindingHandle); + + /** + * Registers a UI context for an actor. + * 为演员注册UI上下文。 + * @param Actor The actor to associate with the context. 与上下文关联的演员。 + * @param Context The UI context to register. 要注册的UI上下文。 + * @param BindingHandle The handle for the context binding. 上下文绑定的句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="Actor")) + void RegisterUIContextForActor(AActor* Actor, UGUIS_GameUIContext* Context, FGUIS_UIContextBindingHandle& BindingHandle); + + /** + * Finds a UI context for a specific player. + * 为特定玩家查找UI上下文。 + * @param LocalPlayer The local player. 本地玩家。 + * @param ContextClass The class of the context to find. 要查找的上下文类。 + * @param OutContext The found UI context (output). 找到的UI上下文(输出)。 + * @return True if the context was found, false otherwise. 如果找到上下文则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="LocalPlayer", DeterminesOutputType="ContextClass", DynamicOutputParam="OutContext", ExpandBoolAsExecs="ReturnValue")) + bool FindUIContextForPlayer(ULocalPlayer* LocalPlayer, TSubclassOf ContextClass, UGUIS_GameUIContext*& OutContext); + + /** + * Finds a UI context from a binding handle. + * 通过绑定句柄查找UI上下文。 + * @param BindingHandle The binding handle to query. 要查询的绑定句柄。 + * @param ContextClass The class of the context to find. 要查找的上下文类。 + * @param OutContext The found UI context (output). 找到的UI上下文(输出)。 + * @return True if the context was found, false otherwise. 如果找到上下文则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="LocalPlayer", DeterminesOutputType="ContextClass", DynamicOutputParam="OutContext", ExpandBoolAsExecs="ReturnValue")) + bool FindUIContextFromHandle(UPARAM(ref) FGUIS_UIContextBindingHandle& BindingHandle, TSubclassOf ContextClass, UGUIS_GameUIContext*& OutContext); + + /** + * Unregisters a UI context for a specific player. + * 为特定玩家取消注册UI上下文。 + * @param BindingHandle The handle of the context binding to unregister. 要取消注册的上下文绑定句柄。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta=(DefaultToSelf="LocalPlayer")) + void UnregisterUIContextForPlayer(UPARAM(ref) FGUIS_UIContextBindingHandle& BindingHandle); + +protected: + /** + * Switches to a specified UI policy. + * 切换到指定的UI策略。 + * @param InPolicy The UI policy to switch to. 要切换到的UI策略。 + */ + void SwitchToPolicy(UGUIS_GameUIPolicy* InPolicy); + +private: + /** + * The current UI policy in use. + * 当前使用的UI策略。 + */ + UPROPERTY(Transient) + TObjectPtr CurrentPolicy = nullptr; + + /** + * Array of UI action binding handles. + * UI动作绑定句柄的数组。 + */ + TArray BindingHandles; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameplayTags.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameplayTags.h new file mode 100644 index 0000000..e2018e7 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/GUIS_GameplayTags.h @@ -0,0 +1,54 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once +#include "NativeGameplayTags.h" + +/** + * Namespace for modal action gameplay tags. + * 模态动作游戏标签的命名空间。 + */ +namespace GUIS_GameModalActionTags +{ + /** + * Tag for "Ok" modal action. + * "确定"模态动作的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Ok); + + /** + * Tag for "Cancel" modal action. + * "取消"模态动作的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Cancel); + + /** + * Tag for "Yes" modal action. + * "是"模态动作的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Yes); + + /** + * Tag for "No" modal action. + * "否"模态动作的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(No); + + /** + * Tag for unknown modal action. + * 未知模态动作的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Unknown); +} + +/** + * Namespace for UI layer gameplay tags. + * UI层游戏标签的命名空间。 + */ +namespace GUIS_GameUILayerTags +{ + /** + * Tag for modal UI layer. + * 模态UI层的标签。 + */ + GENERICUISYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Modal); +} \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_JoystickWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_JoystickWidget.h new file mode 100644 index 0000000..723305f --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_JoystickWidget.h @@ -0,0 +1,74 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "UI/Mobile/GUIS_SimulatedInputWidget.h" +#include "GUIS_JoystickWidget.generated.h" + + +class UImage; +class UObject; +struct FGeometry; +struct FPointerEvent; + +/** + * A UMG wrapper for the lyra virtual joystick. + * + * This will calculate a 2D vector clamped between -1 and 1 + * to input as a key value to the player, simulating a gamepad analog stick. + * + * This is intended for use with and Enhanced Input player. + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_JoystickWidget : public UGUIS_SimulatedInputWidget +{ + GENERATED_BODY() + +public: + UGUIS_JoystickWidget(const FObjectInitializer& ObjectInitializer); + + //~ Begin UUserWidget + virtual FReply NativeOnTouchStarted(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override; + virtual FReply NativeOnTouchMoved(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override; + virtual FReply NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override; + virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override; + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + //~ End UUserWidget interface + +protected: + /** + * Calculate the delta position of the current touch from the origin. + * + * Input the associated gamepad key on the player + * + * Move the foreground joystick image in association with the given input to give the appearance that it + * is moving along with the player's finger + */ + void HandleTouchDelta(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent); + + /** Flush any player input that has been injected and disable the use of this analog stick. */ + void StopInputSimulation(); + + /** How far can the inner image of the joystick be moved? */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GUIS") + float StickRange = 50.0f; + + /** Image to be used as the background of the joystick */ + UPROPERTY(BlueprintReadWrite, Category="GUIS", meta = (BindWidget)) + TObjectPtr JoystickBackground; + + /** Image to be used as the foreground of the joystick */ + UPROPERTY(BlueprintReadWrite, Category="GUIS", meta = (BindWidget)) + TObjectPtr JoystickForeground; + + /** Should we negate the Y-axis value of the joystick? This is common for "movement" sticks */ + UPROPERTY(BlueprintReadWrite, Category="GUIS", EditAnywhere) + bool bNegateYAxis = false; + + /** The origin of the touch. Set on NativeOnTouchStarted */ + UPROPERTY(Transient) + FVector2D TouchOrigin = FVector2D::ZeroVector; + + UPROPERTY(Transient) + FVector2D StickVector = FVector2D::ZeroVector; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_SimulatedInputWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_SimulatedInputWidget.h new file mode 100644 index 0000000..0c6b4db --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Mobile/GUIS_SimulatedInputWidget.h @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonUserWidget.h" +#include "InputCoreTypes.h" +#include "CommonUserWidget.h" +#include "GUIS_SimulatedInputWidget.generated.h" + +class UEnhancedInputLocalPlayerSubsystem; +class UInputAction; +class UCommonHardwareVisibilityBorder; +class UEnhancedPlayerInput; + +/** + * A UMG widget with base functionality to inject input (keys or input actions) + * to the enhanced input subsystem. + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_SimulatedInputWidget : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + UGUIS_SimulatedInputWidget(const FObjectInitializer& ObjectInitializer); + + //~ Begin UWidget +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override; +#endif + //~ End UWidget interface + + //~ Begin UUserWidget + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual FReply NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override; + //~ End UUserWidget interface + + /** Get the enhanced input subsystem based on the owning local player of this widget. Will return null if there is no owning player */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS") + UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem() const; + + /** Get the current player input from the current input subsystem */ + UEnhancedPlayerInput* GetPlayerInput() const; + + /** */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS") + const UInputAction* GetAssociatedAction() const { return AssociatedAction; } + + /** Returns the current key that will be used to input any values. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GUIS") + FKey GetSimulatedKey() const { return KeyToSimulate; } + + /** + * Injects the given vector as an input to the current simulated key. + * This calls "InputKey" on the current player. + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void InputKeyValue(const FVector& Value); + + /** + * Injects the given vector as an input to the current simulated key. + * This calls "InputKey" on the current player. + */ + UFUNCTION(BlueprintCallable, Category="GUIS") + void InputKeyValue2D(const FVector2D& Value); + + UFUNCTION(BlueprintCallable, Category="GUIS") + void FlushSimulatedInput(); + +protected: + /** Set the KeyToSimulate based on a query from enhanced input about what keys are mapped to the associated action */ + void QueryKeyToSimulate(); + + /** Called whenever control mappings change, so we have a chance to adapt our own keys */ + UFUNCTION() + void OnControlMappingsRebuilt(); + + /** The common visibility border will allow you to specify UI for only specific platforms if desired */ + UPROPERTY(BlueprintReadWrite, Category="GUIS", meta = (BindWidget)) + TObjectPtr CommonVisibilityBorder = nullptr; + + /** The associated input action that we should simulate input for */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + TObjectPtr AssociatedAction = nullptr; + + /** The Key to simulate input for in the case where none are currently bound to the associated action */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GUIS") + FKey FallbackBindingKey = EKeys::Gamepad_Right2D; + + /** The key that should be input via InputKey on the player input */ + FKey KeyToSimulate; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModal.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModal.h new file mode 100644 index 0000000..9a5d738 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModal.h @@ -0,0 +1,129 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CommonActivatableWidget.h" +#include "GameplayTagContainer.h" +#include "GUIS_GameModalTypes.h" +#include "GUIS_GameModal.generated.h" + +class UCommonTextBlock; +class UDynamicEntryBox; +class UGUIS_GameModalWidget; + +/** + * Definition for a modal dialog. + * 模态对话框的定义。 + */ +UCLASS(Abstract, BlueprintType, Blueprintable, Const) +class GENERICUISYSTEM_API UGUIS_ModalDefinition : public UObject +{ + GENERATED_BODY() + +public: + /** + * Header text for the modal. + * 模态对话框的标题文本。 + */ + UPROPERTY(EditAnywhere, Category="GUIS", BlueprintReadWrite) + FText Header; + + /** + * Body text for the modal. + * 模态对话框的正文文本。 + */ + UPROPERTY(EditAnywhere, Category="GUIS", BlueprintReadWrite) + FText Body; + + /** + * Widget class used to represent the modal. + * 表示模态对话框的小部件类。 + */ + UPROPERTY(EditAnywhere, Category="GUIS", BlueprintReadWrite) + TSoftClassPtr ModalWidget; + + /** + * Map of modal actions to their configurations. + * 模态动作及其配置的映射。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GUIS", meta = (ForceInlineRow, Categories = "GUIS.Modal.Action")) + TMap ModalActions; +}; + +/** + * Base widget for modal dialogs. + * 模态对话框的基础小部件。 + * @note Must bind a DynamicEntryBox named "EntryBox_Buttons" for button registration. + * @注意 必须绑定一个名为"EntryBox_Buttons"的DynamicEntryBox以注册按钮。 + */ +UCLASS(Abstract, meta = (Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_GameModalWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + /** + * Constructor for the modal widget. + * 模态小部件构造函数。 + */ + UGUIS_GameModalWidget(); + + /** + * Sets up the modal with the provided definition. + * 使用提供的定义设置模态对话框。 + * @param ModalDefinition The modal definition. 模态定义。 + * @param ModalActionCallback Callback for modal actions. 模态动作回调。 + */ + virtual void SetupModal(const UGUIS_ModalDefinition* ModalDefinition, FGUIS_ModalActionResultSignature ModalActionCallback); + + /** + * Closes the modal with the specified result. + * 以指定结果关闭模态对话框。 + * @param ModalActionResult The modal action result. 模态动作结果。 + */ + UFUNCTION(BlueprintCallable, Category="GUIS", meta = (Categories = "UI.Modal.Action,GUIS.Modal.Action")) + void CloseModal(FGameplayTag ModalActionResult); + + /** + * Terminates the modal. + * 终止模态对话框。 + */ + virtual void KillModal(); + +protected: + /** + * Event to apply modal definition data to UI elements. + * 将模态定义数据应用于UI元素的事件。 + * @param ModalDefinition The modal definition. 模态定义。 + */ + UFUNCTION(BlueprintImplementableEvent, Category="GUIS") + void OnSetupModal(const UGUIS_ModalDefinition* ModalDefinition); + + /** + * Callback for modal action results. + * 模态动作结果的回调。 + */ + FGUIS_ModalActionResultSignature OnModalActionCallback; + +private: + /** + * Dynamic entry box for modal buttons. + * 模态按钮的动态入口框。 + */ + UPROPERTY(Meta = (BindWidget)) + TObjectPtr EntryBox_Buttons; + + /** + * Text block for the modal header. + * 模态标题的文本块。 + */ + UPROPERTY(Meta = (BindWidget)) + TObjectPtr Text_Header; + + /** + * Text block for the modal body. + * 模态正文的文本块。 + */ + UPROPERTY(Meta = (BindWidget)) + TObjectPtr Text_Body; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModalTypes.h b/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModalTypes.h new file mode 100644 index 0000000..cd95566 --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UI/Modal/GUIS_GameModalTypes.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Subsystems/LocalPlayerSubsystem.h" +#include "Engine/DataTable.h" +#include "GUIS_GameModalTypes.generated.h" + +class UGUIS_ButtonBase; +class FSubsystemCollectionBase; +class UGUIS_ModalDefinition; +class UObject; + +/** + * Delegate for modal action results. + * 模态动作结果的委托。 + */ +DECLARE_DELEGATE_OneParam(FGUIS_ModalActionResultSignature, FGameplayTag /* Result */); + +/** + * Configuration for a modal action. + * 模态动作的配置。 + */ +USTRUCT(BlueprintType) +struct GENERICUISYSTEM_API FGUIS_GameModalAction +{ + GENERATED_BODY() + + /** + * Display text for the modal action. + * 模态动作的显示文本。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GUIS") + FText DisplayText; + + /** + * Button widget class for the modal action. + * 模态动作的按钮小部件类。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GUIS", NoClear) + TSoftClassPtr ButtonType; + + /** + * Input action associated with the modal action. + * 模态动作关联的输入动作。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GUIS", meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase")) + FDataTableRowHandle InputAction; +}; diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionPointWidget.h b/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionPointWidget.h new file mode 100644 index 0000000..addcc1a --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionPointWidget.h @@ -0,0 +1,173 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GUIS_GameUIExtensionSubsystem.h" +#include "Components/DynamicEntryBoxBase.h" +#include "GUIS_GameUIExtensionPointWidget.generated.h" + +class IWidgetCompilerLog; + +/** + * Delegate for retrieving widget class for data. + * 获取数据小部件类的委托。 + */ +DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(TSubclassOf, FOnGetWidgetClassForData, UObject*, DataItem); + +/** + * Delegate for configuring widget for data. + * 配置数据小部件的委托。 + */ +DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnConfigureWidgetForData, UUserWidget*, Widget, UObject*, DataItem); + +/** + * Widget representing a UI extension point in a layout. + * 表示布局中UI扩展点的小部件。 + * @note Context is LocalPlayer. + * @注意 上下文是LocalPlayer。 + */ +UCLASS(meta=(Category = "Generic UI")) +class GENERICUISYSTEM_API UGUIS_GameUIExtensionPointWidget : public UDynamicEntryBoxBase +{ + GENERATED_BODY() + +public: + /** + * Constructor for the extension point widget. + * 扩展点小部件构造函数。 + */ + UGUIS_GameUIExtensionPointWidget(const FObjectInitializer& ObjectInitializer); + + /** + * Releases Slate resources. + * 释放Slate资源。 + * @param bReleaseChildren Whether to release child resources. 是否释放子资源。 + */ + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + + /** + * Rebuilds the Slate widget. + * 重建Slate小部件。 + * @return The rebuilt Slate widget. 重建的Slate小部件。 + */ + virtual TSharedRef RebuildWidget() override; + + /** + * Registers the widget for the player state if ready. + * 如果准备好,为玩家状态注册小部件。 + */ + void RegisterForPlayerStateIfReady(); + + /** + * Checks the player state. + * 检查玩家状态。 + * @return True if player state is valid, false otherwise. 如果玩家状态有效返回true,否则返回false。 + */ + bool CheckPlayerState(); + + /** + * Called to check the player state. + * 检查玩家状态时调用。 + */ + void OnCheckPlayerState(); + + /** + * Timer handle for player state checks. + * 玩家状态检查的定时器句柄。 + */ + FTimerHandle TimerHandle; + +#if WITH_EDITOR + /** + * Validates compiled defaults in the editor. + * 在编辑器中验证编译默认值。 + * @param CompileLog The widget compiler log. 小部件编译日志。 + */ + virtual void ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const override; +#endif + +private: + /** + * Resets the extension point. + * 重置扩展点。 + */ + void ResetExtensionPoint(); + + /** + * Registers the extension point. + * 注册扩展点。 + */ + void RegisterExtensionPoint(); + + /** + * Registers the extension point for a specific player state. + * 为特定玩家状态注册扩展点。 + * @param LocalPlayer The local player. 本地玩家。 + * @param PlayerState The player state. 玩家状态。 + */ + void RegisterExtensionPointForPlayerState(ULocalPlayer* LocalPlayer, APlayerState* PlayerState); + + /** + * Loads allowed data classes. + * 加载允许的数据类。 + * @return The allowed data classes. 允许的数据类。 + */ + TArray LoadAllowedDataClasses() const; + + /** + * Called when an extension is added or removed. + * 扩展添加或移除时调用。 + * @param Action The extension action (Added/Removed). 扩展动作(添加/移除)。 + * @param Request The extension request. 扩展请求。 + */ + void OnAddOrRemoveExtension(EGUIS_GameUIExtAction Action, const FGUIS_GameUIExtRequest& Request); + +protected: + /** + * Tag defining the extension point. + * 定义扩展点的标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension") + FGameplayTag ExtensionPointTag; + + /** + * Match type for the extension point tag. + * 扩展点标签的匹配类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension") + EGUIS_GameUIExtPointMatchType ExtensionPointTagMatch = EGUIS_GameUIExtPointMatchType::ExactMatch; + + /** + * Allowed data classes for the extension point. + * 扩展点允许的数据类。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Extension") + TArray> DataClasses; + + /** + * Event to get the widget class for non-widget data. + * 为非小部件数据获取小部件类的事件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="UI Extension", meta=( IsBindableEvent="True" )) + FOnGetWidgetClassForData GetWidgetClassForData; + + /** + * Event to configure widget instance for non-widget data. + * 为非小部件数据配置小部件实例的事件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="UI Extension", meta=( IsBindableEvent="True" )) + FOnConfigureWidgetForData ConfigureWidgetForData; + + /** + * Array of extension point handles. + * 扩展点句柄数组。 + */ + TArray ExtensionPointHandles; + + /** + * Mapping of extension handles to widgets. + * 扩展句柄到小部件的映射。 + */ + UPROPERTY(Transient) + TMap> ExtensionMapping; +}; \ No newline at end of file diff --git a/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionSubsystem.h b/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionSubsystem.h new file mode 100644 index 0000000..018776f --- /dev/null +++ b/Plugins/GGS/Source/GenericUISystem/Public/UIExtension/GUIS_GameUIExtensionSubsystem.h @@ -0,0 +1,617 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Subsystems/WorldSubsystem.h" +#include "GUIS_GameUIExtensionSubsystem.generated.h" + +class UGUIS_ExtensionSubsystem; +struct FGUIS_GameUIExtRequest; +template +class TSubclassOf; +template +class TSoftClassPtr; +class FSubsystemCollectionBase; +class UUserWidget; +struct FFrame; + +/** + * Enum defining match rules for UI extension points. + * 定义UI扩展点匹配规则的枚举。 + */ +UENUM(BlueprintType) +enum class EGUIS_GameUIExtPointMatchType : uint8 +{ + /** + * Requires an exact match for the extension point tag. + * 要求扩展点标签完全匹配。 + */ + ExactMatch, + + /** + * Allows partial matches for the extension point tag. + * 允许扩展点标签部分匹配。 + */ + PartialMatch +}; + +/** + * Enum defining actions for UI extensions. + * 定义UI扩展动作的枚举。 + */ +UENUM(BlueprintType) +enum class EGUIS_GameUIExtAction : uint8 +{ + /** + * Extension is added. + * 扩展被添加。 + */ + Added, + + /** + * Extension is removed. + * 扩展被移除。 + */ + Removed +}; + +/** + * Delegate for extension point events. + * 扩展点事件的委托。 + */ +DECLARE_DELEGATE_TwoParams(FExtendExtensionPointDelegate, EGUIS_GameUIExtAction Action, const FGUIS_GameUIExtRequest& Request); + +/** + * Structure representing a UI extension. + * 表示UI扩展的结构。 + */ +struct FGUIS_GameUIExt : TSharedFromThis +{ + /** + * Tag identifying the extension point. + * 标识扩展点的标签。 + */ + FGameplayTag ExtensionPointTag; + + /** + * Priority of the extension. + * 扩展的优先级。 + */ + int32 Priority = INDEX_NONE; + + /** + * Context object for the extension. + * 扩展的上下文对象。 + */ + TWeakObjectPtr ContextObject; + + /** + * Data object for the extension, kept alive by the subsystem. + * 扩展的数据对象,由子系统保持存活。 + */ + TObjectPtr Data = nullptr; +}; + +/** + * Structure representing a UI extension point. + * 表示UI扩展点的结构。 + */ +struct FGUIS_GameUIExtPoint : TSharedFromThis +{ + /** + * Tag identifying the extension point. + * 标识扩展点的标签。 + */ + FGameplayTag ExtensionPointTag; + + /** + * Context object for the extension point. + * 扩展点的上下文对象。 + */ + TWeakObjectPtr ContextObject; + + /** + * Match type for the extension point tag. + * 扩展点标签的匹配类型。 + */ + EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType = EGUIS_GameUIExtPointMatchType::ExactMatch; + + /** + * Allowed data classes for the extension point. + * 扩展点允许的数据类。 + */ + TArray> AllowedDataClasses; + + /** + * Callback for extension point events. + * 扩展点事件的回调。 + */ + FExtendExtensionPointDelegate Callback; + + /** + * Checks if an extension matches the extension point. + * 检查扩展是否与扩展点匹配。 + * @param Extension The extension to check. 要检查的扩展。 + * @return True if the extension matches, false otherwise. 如果扩展匹配返回true,否则返回false。 + */ + bool DoesExtensionPassContract(const FGUIS_GameUIExt* Extension) const; +}; + +/** + * Handle for a UI extension point. + * UI扩展点的句柄。 + */ +USTRUCT(BlueprintType) +struct GENERICUISYSTEM_API FGUIS_GameUIExtPointHandle +{ + GENERATED_BODY() + + /** + * Default constructor. + * 默认构造函数。 + */ + FGUIS_GameUIExtPointHandle(); + + /** + * Unregisters the extension point. + * 注销扩展点。 + */ + void Unregister(); + + /** + * Checks if the handle is valid. + * 检查句柄是否有效。 + * @return True if valid, false otherwise. 如果有效返回true,否则返回false。 + */ + bool IsValid() const { return DataPtr.IsValid(); } + + /** + * Equality operator for extension point handles. + * 扩展点句柄的相等比较运算符。 + */ + bool operator==(const FGUIS_GameUIExtPointHandle& Other) const { return DataPtr == Other.DataPtr; } + + /** + * Inequality operator for extension point handles. + * 扩展点句柄的不等比较运算符。 + */ + bool operator!=(const FGUIS_GameUIExtPointHandle& Other) const { return !operator==(Other); } + + /** + * Hash function for the extension point handle. + * 扩展点句柄的哈希函数。 + * @param Handle The handle to hash. 要哈希的句柄。 + * @return The hash value. 哈希值。 + */ + friend uint32 GetTypeHash(const FGUIS_GameUIExtPointHandle& Handle) + { + return PointerHash(Handle.DataPtr.Get()); + }; + +private: + /** + * The extension subsystem source. + * 扩展子系统源。 + */ + TWeakObjectPtr ExtensionSource; + + /** + * Shared pointer to the extension point data. + * 扩展点数据的共享指针。 + */ + TSharedPtr DataPtr; + + friend UGUIS_ExtensionSubsystem; + + /** + * Constructor for the extension point handle. + * 扩展点句柄构造函数。 + * @param InExtensionSource The extension subsystem source. 扩展子系统源。 + * @param InDataPtr The extension point data. 扩展点数据。 + */ + FGUIS_GameUIExtPointHandle(UGUIS_ExtensionSubsystem* InExtensionSource, const TSharedPtr& InDataPtr); +}; + +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithCopy = true, + WithIdenticalViaEquality = true, + }; +}; + +/** + * Handle for a UI extension. + * UI扩展的句柄。 + */ +USTRUCT(BlueprintType) +struct GENERICUISYSTEM_API FGUIS_GameUIExtHandle +{ + GENERATED_BODY() + + /** + * Default constructor. + * 默认构造函数。 + */ + FGUIS_GameUIExtHandle(); + + /** + * Unregisters the extension. + * 注销扩展。 + */ + void Unregister(); + + /** + * Checks if the handle is valid. + * 检查句柄是否有效。 + * @return True if valid, false otherwise. 如果有效返回true,否则返回false。 + */ + bool IsValid() const { return DataPtr.IsValid(); } + + /** + * Equality operator for extension handles. + * 扩展句柄的相等比较运算符。 + */ + bool operator==(const FGUIS_GameUIExtHandle& Other) const { return DataPtr == Other.DataPtr; } + + /** + * Inequality operator for extension handles. + * 扩展句柄的不等比较运算符。 + */ + bool operator!=(const FGUIS_GameUIExtHandle& Other) const { return !operator==(Other); } + + /** + * Hash function for the extension handle. + * 扩展句柄的哈希函数。 + * @param Handle The handle to hash. 要哈希的句柄。 + * @return The hash value. 哈希值。 + */ + friend uint32 GetTypeHash(FGUIS_GameUIExtHandle Handle) { return PointerHash(Handle.DataPtr.Get()); }; + +private: + /** + * The extension subsystem source. + * 扩展子系统源。 + */ + TWeakObjectPtr ExtensionSource; + + /** + * Shared pointer to the extension data. + * 扩展数据的共享指针。 + */ + TSharedPtr DataPtr; + + friend UGUIS_ExtensionSubsystem; + + /** + * Constructor for the extension handle. + * 扩展句柄构造函数。 + * @param InExtensionSource The extension subsystem source. 扩展子系统源。 + * @param InDataPtr The extension data. 扩展数据。 + */ + FGUIS_GameUIExtHandle(UGUIS_ExtensionSubsystem* InExtensionSource, const TSharedPtr& InDataPtr); +}; + +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithCopy = true, + WithIdenticalViaEquality = true, + }; +}; + +/** + * Structure representing a UI extension request. + * 表示UI扩展请求的结构。 + */ +USTRUCT(BlueprintType) +struct FGUIS_GameUIExtRequest +{ + GENERATED_BODY() + + /** + * Handle for the extension. + * 扩展的句柄。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + FGUIS_GameUIExtHandle ExtensionHandle; + + /** + * Tag identifying the extension point. + * 标识扩展点的标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + FGameplayTag ExtensionPointTag; + + /** + * Priority of the extension. + * 扩展的优先级。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + int32 Priority = INDEX_NONE; + + /** + * Data object for the extension. + * 扩展的数据对象。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + TObjectPtr Data = nullptr; + + /** + * Context object for the extension. + * 扩展的上下文对象。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GUIS") + TObjectPtr ContextObject = nullptr; +}; + +/** + * Dynamic delegate for extension point events. + * 扩展点事件的动态委托。 + */ +DECLARE_DYNAMIC_DELEGATE_TwoParams(FExtendExtensionPointDynamicDelegate, EGUIS_GameUIExtAction, Action, const FGUIS_GameUIExtRequest&, ExtensionRequest); + +/** + * World subsystem for managing UI extensions. + * 管理UI扩展的世界子系统。 + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_ExtensionSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + /** + * Registers an extension point. + * 注册扩展点。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ExtensionPointTagMatchType The match type for the tag. 标签匹配类型。 + * @param AllowedDataClasses The allowed data classes. 允许的数据类。 + * @param ExtensionCallback The callback for extension events. 扩展事件回调。 + * @return The extension point handle. 扩展点句柄。 + */ + FGUIS_GameUIExtPointHandle RegisterExtensionPoint(const FGameplayTag& ExtensionPointTag, EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, const TArray& AllowedDataClasses, + FExtendExtensionPointDelegate ExtensionCallback); + + /** + * Registers an extension point for a specific context. + * 为特定上下文注册扩展点。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ContextObject The context object. 上下文对象。 + * @param ExtensionPointTagMatchType The match type for the tag. 标签匹配类型。 + * @param AllowedDataClasses The allowed data classes. 允许的数据类。 + * @param ExtensionCallback The callback for extension events. 扩展事件回调。 + * @return The extension point handle. 扩展点句柄。 + */ + FGUIS_GameUIExtPointHandle RegisterExtensionPointForContext(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, + const TArray& AllowedDataClasses, FExtendExtensionPointDelegate ExtensionCallback); + + /** + * Registers a widget as a UI extension. + * 将小部件注册为UI扩展。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param WidgetClass The widget class. 小部件类。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + FGUIS_GameUIExtHandle RegisterExtensionAsWidget(const FGameplayTag& ExtensionPointTag, TSubclassOf WidgetClass, int32 Priority); + + /** + * Registers a widget as a UI extension for a specific context. + * 为特定上下文将小部件注册为UI扩展。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ContextObject The context object. 上下文对象。 + * @param WidgetClass The widget class. 小部件类。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + FGUIS_GameUIExtHandle RegisterExtensionAsWidgetForContext(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, TSubclassOf WidgetClass, int32 Priority); + + /** + * Registers data as a UI extension. + * 将数据注册为UI扩展。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ContextObject The context object. 上下文对象。 + * @param Data The data object. 数据对象。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + FGUIS_GameUIExtHandle RegisterExtensionAsData(const FGameplayTag& ExtensionPointTag, UObject* ContextObject, UObject* Data, int32 Priority); + + /** + * Unregisters a UI extension. + * 注销UI扩展。 + * @param ExtensionHandle The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension") + void UnregisterExtension(const FGUIS_GameUIExtHandle& ExtensionHandle); + + /** + * Unregisters a UI extension point. + * 注销UI扩展点。 + * @param ExtensionPointHandle The extension point handle. 扩展点句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension") + void UnregisterExtensionPoint(const FGUIS_GameUIExtPointHandle& ExtensionPointHandle); + + /** + * Adds referenced objects to the garbage collector. + * 将引用的对象添加到垃圾回收器。 + * @param InThis The subsystem instance. 子系统实例。 + * @param Collector The reference collector. 引用收集器。 + */ + static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); + +protected: + /** + * Initializes the subsystem. + * 初始化子系统。 + * @param Collection The subsystem collection. 子系统集合。 + */ + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + /** + * Deinitializes the subsystem. + * 反初始化子系统。 + */ + virtual void Deinitialize() override; + + /** + * Notifies an extension point of new or removed extensions. + * 通知扩展点有关新添加或移除的扩展。 + * @param ExtensionPoint The extension point to notify. 要通知的扩展点。 + */ + void NotifyExtensionPointOfExtensions(TSharedPtr& ExtensionPoint); + + /** + * Notifies extension points of an extension action. + * 通知扩展点有关扩展动作。 + * @param Action The extension action (Added/Removed). 扩展动作(添加/移除)。 + * @param Extension The extension data. 扩展数据。 + */ + void NotifyExtensionPointsOfExtension(EGUIS_GameUIExtAction Action, TSharedPtr& Extension); + + /** + * Registers an extension point (Blueprint version). + * 注册扩展点(蓝图版本)。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ExtensionPointTagMatchType The match type for the tag. 标签匹配类型。 + * @param AllowedDataClasses The allowed data classes. 允许的数据类。 + * @param ExtensionCallback The callback for extension events. 扩展事件回调。 + * @return The extension point handle. 扩展点句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="UI Extension", meta = (DisplayName = "Register Extension Point")) + FGUIS_GameUIExtPointHandle K2_RegisterExtensionPoint(FGameplayTag ExtensionPointTag, EGUIS_GameUIExtPointMatchType ExtensionPointTagMatchType, + const TArray>& AllowedDataClasses, + FExtendExtensionPointDynamicDelegate ExtensionCallback); + + /** + * Registers a widget as a UI extension (Blueprint version). + * 将小部件注册为UI扩展(蓝图版本)。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param WidgetClass The widget class. 小部件类。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension", meta = (DisplayName = "Register Extension (Widget)")) + FGUIS_GameUIExtHandle K2_RegisterExtensionAsWidget(FGameplayTag ExtensionPointTag, TSoftClassPtr WidgetClass, int32 Priority = -1); + + /** + * Registers a widget as a UI extension for a specific context (Blueprint version). + * 为特定上下文将小部件注册为UI扩展(蓝图版本)。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param WidgetClass The widget class. 小部件类。 + * @param ContextObject The context object. 上下文对象。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension", meta = (DisplayName = "Register Extension (Widget For Context)")) + FGUIS_GameUIExtHandle K2_RegisterExtensionAsWidgetForContext(FGameplayTag ExtensionPointTag, TSoftClassPtr WidgetClass, UObject* ContextObject, int32 Priority = -1); + + /** + * Registers data as a UI extension (Blueprint version). + * 将数据注册为UI扩展(蓝图版本)。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param Data The data object. 数据对象。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="UI Extension", meta = (DisplayName = "Register Extension (Data)")) + FGUIS_GameUIExtHandle K2_RegisterExtensionAsData(FGameplayTag ExtensionPointTag, UObject* Data, int32 Priority = -1); + + /** + * Registers data as a UI extension for a specific context (Blueprint version). + * 为特定上下文将数据注册为UI扩展(蓝图版本)。 + * @param ExtensionPointTag The extension point tag. 扩展点标签。 + * @param ContextObject The context object. 上下文对象。 + * @param Data The data object. 数据对象。 + * @param Priority The extension priority. 扩展优先级。 + * @return The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="UI Extension", meta = (DisplayName = "Register Extension (Data For Context)")) + FGUIS_GameUIExtHandle K2_RegisterExtensionAsDataForContext(FGameplayTag ExtensionPointTag, UObject* ContextObject, UObject* Data, int32 Priority = -1); + + /** + * Creates an extension request from extension data. + * 从扩展数据创建扩展请求。 + * @param Extension The extension data. 扩展数据。 + * @return The extension request. 扩展请求。 + */ + FGUIS_GameUIExtRequest CreateExtensionRequest(const TSharedPtr& Extension); + +private: + /** + * Map of extension point tags to extension point lists. + * 扩展点标签到扩展点列表的映射。 + */ + using FExtensionPointList = TArray>; + TMap ExtensionPointMap; + + /** + * Map of extension tags to extension lists. + * 扩展标签到扩展列表的映射。 + */ + using FExtensionList = TArray>; + TMap ExtensionMap; +}; + +/** + * Blueprint function library for UI extension operations. + * UI扩展操作的蓝图函数库。 + */ +UCLASS() +class GENERICUISYSTEM_API UGUIS_ExtensionFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Default constructor. + * 默认构造函数。 + */ + UGUIS_ExtensionFunctionLibrary(); + + /** + * Unregisters a UI extension. + * 注销UI扩展。 + * @param Handle The extension handle. 扩展句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension") + static void UnregisterExtension(UPARAM(ref) + FGUIS_GameUIExtHandle& Handle); + + /** + * Checks if a UI extension is valid. + * 检查UI扩展是否有效。 + * @param Handle The extension handle. 扩展句柄。 + * @return True if valid, false otherwise. 如果有效返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintCosmetic, Category = "UI Extension") + static bool IsValidExtension(UPARAM(ref) + FGUIS_GameUIExtHandle& Handle); + + /** + * Unregisters a UI extension point. + * 注销UI扩展点。 + * @param Handle The extension point handle. 扩展点句柄。 + */ + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "UI Extension") + static void UnregisterExtensionPoint(UPARAM(ref) + FGUIS_GameUIExtPointHandle& Handle); + + /** + * Checks if a UI extension point is valid. + * 检查UI扩展点是否有效。 + * @param Handle The extension point handle. 扩展点句柄。 + * @return True if valid, false otherwise. 如果有效返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintCosmetic, Category = "UI Extension") + static bool IsValidExtensionPoint(UPARAM(ref) + FGUIS_GameUIExtPointHandle& Handle); +}; diff --git a/Plugins/GIS/Config/BaseGenericInventorySystem.ini b/Plugins/GIS/Config/BaseGenericInventorySystem.ini new file mode 100644 index 0000000..a3ff04c --- /dev/null +++ b/Plugins/GIS/Config/BaseGenericInventorySystem.ini @@ -0,0 +1,8 @@ +[CoreRedirects] +;GIS 1.1 migration. ++FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerCycleGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerCycleGroupActiveSlot") ++FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.CycleGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.CycleGroupActiveSlot") ++FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerSetGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerSetGroupActiveSlot") ++FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.SetGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.SetGroupActiveSlot") + + diff --git a/Plugins/GIS/Config/FilterPlugin.ini b/Plugins/GIS/Config/FilterPlugin.ini new file mode 100644 index 0000000..386e260 --- /dev/null +++ b/Plugins/GIS/Config/FilterPlugin.ini @@ -0,0 +1,9 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll +/Config/* \ No newline at end of file diff --git a/Plugins/GIS/GenericInventorySystem.uplugin b/Plugins/GIS/GenericInventorySystem.uplugin new file mode 100644 index 0000000..3f83920 --- /dev/null +++ b/Plugins/GIS/GenericInventorySystem.uplugin @@ -0,0 +1,42 @@ +{ + "FileVersion": 3, + "Version": 3, + "VersionName": "1.1.1", + "FriendlyName": "GenericInventorySystem", + "Description": "Advanced and Flexible Inventory framework.", + "Category": "Gameplay", + "CreatedBy": "YuewuDev", + "CreatedByURL": "https://yuewu.dev/en", + "DocsURL": "https://www.yuewu.dev/en/wiki", + "MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/9a9b6f10-4d4c-4897-90ec-809854653402", + "SupportURL": "https://discord.com/invite/xMRXAB2", + "EngineVersion": "5.7.0", + "CanContainContent": false, + "Installed": true, + "Modules": [ + { + "Name": "GenericInventorySystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericInventoryEditor", + "Type": "UncookedOnly", + "LoadingPhase": "PreDefault", + "PlatformAllowList": [ + "Win64" + ] + } + ], + "Plugins": [ + { + "Name": "ModularGameplay", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/GIS/Resources/Icon128.png b/Plugins/GIS/Resources/Icon128.png new file mode 100644 index 0000000..d617ef1 Binary files /dev/null and b/Plugins/GIS/Resources/Icon128.png differ diff --git a/Plugins/GIS/Source/GenericInventoryEditor/GenericInventoryEditor.Build.cs b/Plugins/GIS/Source/GenericInventoryEditor/GenericInventoryEditor.Build.cs new file mode 100644 index 0000000..545bfe9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/GenericInventoryEditor.Build.cs @@ -0,0 +1,27 @@ +using UnrealBuildTool; + +public class GenericInventoryEditor : ModuleRules +{ + public GenericInventoryEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "GenericInventorySystem", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "UnrealEd", + "AssetTools", + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_AssetTypeActions.cpp b/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_AssetTypeActions.cpp new file mode 100644 index 0000000..2426dfc --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_AssetTypeActions.cpp @@ -0,0 +1,49 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Factories/GIS_AssetTypeActions.h" +#include "GenericInventoryEditor.h" +#include "GIS_CurrencyDefinition.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" +#include "GIS_ItemDefinitionSchema.h" +#include "GIS_ItemMultiStackCollection.h" +#include "GIS_ItemSlotCollection.h" + + +uint32 FGIS_AssetTypeAction::GetCategories() +{ + return FGenericInventoryEditorModule::GetAssetsCategory(); +} + +FColor FGIS_AssetTypeAction::GetTypeColor() const +{ + return FColor(114, 40, 199); +} + +#define IMPLEMENT_GIS_ASSET_ACTION(ActionName, NameText, DescText) \ +FText FGIS_AssetTypeAction_##ActionName::GetName() const \ +{ \ +return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Name", NameText); \ +} \ +FText FGIS_AssetTypeAction_##ActionName::GetAssetDescription(const FAssetData& AssetData) const \ +{ \ +return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Description", DescText); \ +} \ +UClass* FGIS_AssetTypeAction_##ActionName::GetSupportedClass() const \ +{ \ +return UGIS_##ActionName::StaticClass(); \ +} + +IMPLEMENT_GIS_ASSET_ACTION(ItemDefinition, "Item Definition", + "Data Asset that defines your item design.") +IMPLEMENT_GIS_ASSET_ACTION(ItemDefinitionSchema, "Item Definition Schema", + "Data Asset that defines the validation rules for item definition.") +IMPLEMENT_GIS_ASSET_ACTION(ItemCollectionDefinition, "Item Collection Definition(Normal)", + "Data Asset that defines the design for normal item collection.") +IMPLEMENT_GIS_ASSET_ACTION(ItemSlotCollectionDefinition, "Item Collection Definition(Slot)", + "Data Asset that defines the design for slot based item collection.") +IMPLEMENT_GIS_ASSET_ACTION(ItemMultiStackCollectionDefinition, "Item Collection Definition(MultiStack)", + "Data Asset that defines the design for multi stack based item collection.") +IMPLEMENT_GIS_ASSET_ACTION(CurrencyDefinition, "CurrencyDefinition", + "Data Asset that defines the in game currency.") diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_DataAssetsFactories.cpp b/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_DataAssetsFactories.cpp new file mode 100644 index 0000000..ac2ea97 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Private/Factories/GIS_DataAssetsFactories.cpp @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Factories/GIS_DataAssetsFactories.h" + +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" +#include "GIS_ItemDefinitionSchema.h" +#include "GIS_ItemMultiStackCollection.h" +#include "GIS_ItemSlotCollection.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_DataAssetsFactories) + +UGIS_Factory::UGIS_Factory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + bEditAfterNew = true; + bCreateNew = true; +} + +uint32 UGIS_Factory::GetMenuCategories() const +{ + return Super::GetMenuCategories(); +} + +const TArray& UGIS_Factory::GetMenuCategorySubMenus() const +{ + return Super::GetMenuCategorySubMenus(); +} + +#define IMPLEMENT_GIS_FACTORY(FactoryName) \ +UGIS_Factory_##FactoryName::UGIS_Factory_##FactoryName(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) \ +{ \ +SupportedClass = UGIS_##FactoryName::StaticClass(); \ +} \ +UObject* UGIS_Factory_##FactoryName::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) \ +{ \ +check(Class->IsChildOf(UGIS_##FactoryName::StaticClass())); \ +return NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); \ +} + +IMPLEMENT_GIS_FACTORY(ItemDefinition) + +IMPLEMENT_GIS_FACTORY(ItemDefinitionSchema) + +IMPLEMENT_GIS_FACTORY(ItemCollectionDefinition) + +IMPLEMENT_GIS_FACTORY(ItemSlotCollectionDefinition) + +IMPLEMENT_GIS_FACTORY(ItemMultiStackCollectionDefinition) diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Private/GenericInventoryEditor.cpp b/Plugins/GIS/Source/GenericInventoryEditor/Private/GenericInventoryEditor.cpp new file mode 100644 index 0000000..7d7ac9a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Private/GenericInventoryEditor.cpp @@ -0,0 +1,42 @@ +#include "GenericInventoryEditor.h" + +#include "Factories/GIS_AssetTypeActions.h" + +#define LOCTEXT_NAMESPACE "FGenericInventoryEditorModule" + +TArray> FGenericInventoryEditorModule::AssetTypeActions = { + MakeShared(), + MakeShared(), + MakeShared(), + MakeShared(), + MakeShared() + +}; + +EAssetTypeCategories::Type FGenericInventoryEditorModule::AssetsCategory; + + +void FGenericInventoryEditorModule::StartupModule() +{ + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + AssetsCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("GenericInventorySystem")), LOCTEXT("GIS_AssetsCategory", "Generic Inventory System")); + for (TSharedPtr& Action : AssetTypeActions) + { + AssetTools.RegisterAssetTypeActions(Action.ToSharedRef()); + } +} + +void FGenericInventoryEditorModule::ShutdownModule() +{ + if (const FAssetToolsModule* AssetTools = FModuleManager::GetModulePtr("AssetTools")) + { + for (TSharedPtr& Action : AssetTypeActions) + { + AssetTools->Get().UnregisterAssetTypeActions(Action.ToSharedRef()); + } + } +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericInventoryEditorModule, GenericInventoryEditor) diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_AssetTypeActions.h b/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_AssetTypeActions.h new file mode 100644 index 0000000..45a6cff --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_AssetTypeActions.h @@ -0,0 +1,33 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "AssetTypeActions/AssetTypeActions_DataAsset.h" + +class FGIS_AssetTypeAction : public FAssetTypeActions_DataAsset +{ +public: + virtual uint32 GetCategories() override; + virtual FColor GetTypeColor() const override; +}; + +#define DEFINE_GIS_ASSET_ACTION(ActionName) \ +class FGIS_AssetTypeAction_##ActionName final : public FGIS_AssetTypeAction \ +{ \ +public: \ +virtual FText GetName() const override; \ +virtual FText GetAssetDescription(const FAssetData& AssetData) const override; \ +virtual UClass* GetSupportedClass() const override; \ +}; + +DEFINE_GIS_ASSET_ACTION(ItemDefinition) + +DEFINE_GIS_ASSET_ACTION(ItemDefinitionSchema) + +DEFINE_GIS_ASSET_ACTION(ItemCollectionDefinition) + +DEFINE_GIS_ASSET_ACTION(ItemSlotCollectionDefinition) + +DEFINE_GIS_ASSET_ACTION(ItemMultiStackCollectionDefinition) + +DEFINE_GIS_ASSET_ACTION(CurrencyDefinition) diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_DataAssetsFactories.h b/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_DataAssetsFactories.h new file mode 100644 index 0000000..b4e89d9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Public/Factories/GIS_DataAssetsFactories.h @@ -0,0 +1,69 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "UObject/ObjectMacros.h" +#include "Templates/SubclassOf.h" +#include "Factories/Factory.h" +#include "GIS_DataAssetsFactories.generated.h" + +UCLASS(Abstract) +class UGIS_Factory : public UFactory +{ + GENERATED_BODY() + +public: + UGIS_Factory(const FObjectInitializer& ObjectInitializer); + virtual uint32 GetMenuCategories() const override; + virtual const TArray& GetMenuCategorySubMenus() const override; +}; + +UCLASS() +class UGIS_Factory_ItemDefinition : public UGIS_Factory +{ + GENERATED_BODY() + +public: + UGIS_Factory_ItemDefinition(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGIS_Factory_ItemDefinitionSchema : public UGIS_Factory +{ + GENERATED_BODY() + +public: + UGIS_Factory_ItemDefinitionSchema(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGIS_Factory_ItemCollectionDefinition : public UGIS_Factory +{ + GENERATED_BODY() + +public: + UGIS_Factory_ItemCollectionDefinition(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGIS_Factory_ItemSlotCollectionDefinition : public UGIS_Factory +{ + GENERATED_BODY() + +public: + UGIS_Factory_ItemSlotCollectionDefinition(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGIS_Factory_ItemMultiStackCollectionDefinition : public UGIS_Factory +{ + GENERATED_BODY() + +public: + UGIS_Factory_ItemMultiStackCollectionDefinition(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; diff --git a/Plugins/GIS/Source/GenericInventoryEditor/Public/GenericInventoryEditor.h b/Plugins/GIS/Source/GenericInventoryEditor/Public/GenericInventoryEditor.h new file mode 100644 index 0000000..f539f10 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventoryEditor/Public/GenericInventoryEditor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeCategories.h" +#include "Modules/ModuleManager.h" + +class IAssetTypeActions; + +class FGenericInventoryEditorModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + static EAssetTypeCategories::Type GetAssetsCategory() + { + return AssetsCategory; + } + +private: + static TArray> AssetTypeActions; + static EAssetTypeCategories::Type AssetsCategory; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/GenericInventorySystem.Build.cs b/Plugins/GIS/Source/GenericInventorySystem/GenericInventorySystem.Build.cs new file mode 100644 index 0000000..8ed867a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/GenericInventorySystem.Build.cs @@ -0,0 +1,73 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using System.IO; +using UnrealBuildTool; + +public class GenericInventorySystem : ModuleRules +{ + public GenericInventorySystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + CppCompileWarningSettings.NonInlinedGenCppWarningLevel = WarningLevel.Warning; + + PublicIncludePaths.AddRange( + new[] + { + Path.Combine(ModuleDirectory, "Public/Core"), + Path.Combine(ModuleDirectory, "Public/Core/Items"), + Path.Combine(ModuleDirectory, "Public/Core/Attributes"), + Path.Combine(ModuleDirectory, "Public/Core/Collections"), + Path.Combine(ModuleDirectory, "Public/Core/Fragments"), + Path.Combine(ModuleDirectory, "Public/Equipping"), + Path.Combine(ModuleDirectory, "Public/Crafting"), + Path.Combine(ModuleDirectory, "Public/Exchange"), + Path.Combine(ModuleDirectory, "Public/Serialization") + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] + { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new[] + { + "Core", + "GameplayTags", + "DeveloperSettings", + "ModularGameplay" + // ... add other public dependencies that you statically link with here ... + } + ); + + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", + "Engine", + "Slate", + "UMG", + "SlateCore", + "NetCore", + "AssetRegistry" + // ... add private dependencies that you statically link with here ... + } + ); + + SetupIrisSupport(Target); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_Wait.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_Wait.cpp new file mode 100644 index 0000000..2a70c50 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_Wait.cpp @@ -0,0 +1,138 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Async/GIS_AsyncAction_Wait.h" +#include "TimerManager.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_Wait) + + +UGIS_AsyncAction_Wait::UGIS_AsyncAction_Wait() +{ +} + +bool UGIS_AsyncAction_Wait::ShouldBroadcastDelegates() const +{ + return Super::ShouldBroadcastDelegates() && IsValid(GetActor()); +} + +void UGIS_AsyncAction_Wait::StopWaiting() +{ + const UWorld* World = GetWorld(); + if (TimerHandle.IsValid() && IsValid(World)) + { + FTimerManager& TimerManager = World->GetTimerManager(); + TimerManager.ClearTimer(TimerHandle); + } +} + +void UGIS_AsyncAction_Wait::Cleanup() +{ + AActor* Actor = GetActor(); + + if (IsValid(Actor)) + { + Actor->OnDestroyed.RemoveDynamic(this, &ThisClass::OnTargetDestroyed); + } + + StopWaiting(); +} + +void UGIS_AsyncAction_Wait::Activate() +{ + const UWorld* World = GetWorld(); + AActor* Actor = GetActor(); + if (IsValid(World) && IsValid(Actor)) + { + FTimerManager& TimerManager = World->GetTimerManager(); + TimerManager.SetTimer(TimerHandle, this, &ThisClass::OnTimer, WaitInterval, true, 0); + Actor->OnDestroyed.AddDynamic(this, &ThisClass::OnTargetDestroyed); + } + else + { + Cancel(); + } +} + +UWorld* UGIS_AsyncAction_Wait::GetWorld() const +{ + if (WorldPtr.IsValid() && WorldPtr->IsValidLowLevelFast()) + { + return WorldPtr.Get(); + } + + return nullptr; +} + +AActor* UGIS_AsyncAction_Wait::GetActor() const +{ + if (TargetActorPtr.IsValid() && TargetActorPtr->IsValidLowLevelFast()) + { + return TargetActorPtr.Get(); + } + + return nullptr; +} + +void UGIS_AsyncAction_Wait::Cancel() +{ + Super::Cancel(); + + Cleanup(); +} + +void UGIS_AsyncAction_Wait::OnTargetDestroyed(AActor* DestroyedActor) +{ + Cancel(); +} + +void UGIS_AsyncAction_Wait::SetWorld(UWorld* NewWorld) +{ + WorldPtr = NewWorld; +} + +void UGIS_AsyncAction_Wait::SetTargetActor(AActor* NewTargetActor) +{ + TargetActorPtr = NewTargetActor; +} + +void UGIS_AsyncAction_Wait::SetWaitInterval(float NewWaitInterval) +{ + WaitInterval = NewWaitInterval; +} + +void UGIS_AsyncAction_Wait::SetMaxWaitTimes(int32 NewMaxWaitTimes) +{ + MaxWaitTimes = NewMaxWaitTimes; +} + +void UGIS_AsyncAction_Wait::OnTimer() +{ + AActor* Actor = GetActor(); + if (!IsValid(Actor)) + { + Cancel(); + return; + } + + OnExecutionAction(); + + if (MaxWaitTimes > 0) + { + WaitTimes++; + if (WaitTimes > MaxWaitTimes) + { + Cancel(); + } + } +} + +void UGIS_AsyncAction_Wait::OnExecutionAction() +{ +} + +void UGIS_AsyncAction_Wait::Complete() +{ + Super::Cancel(); + OnCompleted.Broadcast(); + Cleanup(); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitEquipmentSystem.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitEquipmentSystem.cpp new file mode 100644 index 0000000..c6bf2e4 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitEquipmentSystem.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Async/GIS_AsyncAction_WaitEquipmentSystem.h" + +#include "GIS_EquipmentSystemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitEquipmentSystem) + +UGIS_AsyncAction_WaitEquipmentSystem* UGIS_AsyncAction_WaitEquipmentSystem::WaitEquipmentSystem(UObject* WorldContext, AActor* TargetActor) +{ + return CreateWaitAction(WorldContext, TargetActor, 0.5, -1); +} + +void UGIS_AsyncAction_WaitEquipmentSystem::OnExecutionAction() +{ + AActor* Actor = GetActor(); + + if (UGIS_EquipmentSystemComponent* EquipmentSystem = UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(Actor)) + { + Complete(); + } +} + +UGIS_AsyncAction_WaitEquipmentSystem* UGIS_AsyncAction_WaitEquipmentSystemInitialized::WaitEquipmentSystemInitialized(UObject* WorldContext, AActor* TargetActor) +{ + return CreateWaitAction(WorldContext, TargetActor, 0.5, -1); +} + +void UGIS_AsyncAction_WaitEquipmentSystemInitialized::OnExecutionAction() +{ + // Already found. + UGIS_EquipmentSystemComponent* ExistingOne = EquipmentSystemPtr.IsValid() ? EquipmentSystemPtr.Get() : nullptr; + if (IsValid(ExistingOne)) + { + return; + } + + AActor* Actor = GetActor(); + if (UGIS_EquipmentSystemComponent* EquipmentSys = UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(Actor)) + { + if (EquipmentSys->IsEquipmentSystemInitialized()) + { + Complete(); + return; + } + EquipmentSystemPtr = EquipmentSys; + EquipmentSystemPtr->OnEquipmentSystemInitializedEvent.AddDynamic(this, &ThisClass::OnSystemInitialized); + } +} + +void UGIS_AsyncAction_WaitEquipmentSystemInitialized::Cleanup() +{ + Super::Cleanup(); + UGIS_EquipmentSystemComponent* ExistingOne = EquipmentSystemPtr.IsValid() ? EquipmentSystemPtr.Get() : nullptr; + + if (IsValid(ExistingOne)) + { + ExistingOne->OnEquipmentSystemInitializedEvent.RemoveAll(this); + } +} + +void UGIS_AsyncAction_WaitEquipmentSystemInitialized::OnSystemInitialized() +{ + Complete(); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitInventorySystem.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitInventorySystem.cpp new file mode 100644 index 0000000..0faf44e --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitInventorySystem.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Async/GIS_AsyncAction_WaitInventorySystem.h" + +#include "GIS_InventorySystemComponent.h" +#include "GameFramework/PlayerState.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitInventorySystem) + +UGIS_AsyncAction_WaitInventorySystem* UGIS_AsyncAction_WaitInventorySystem::WaitInventorySystem(UObject* WorldContext, AActor* TargetActor) +{ + return CreateWaitAction(WorldContext, TargetActor, 0.5, -1); +} + +void UGIS_AsyncAction_WaitInventorySystem::OnExecutionAction() +{ + AActor* Actor = GetActor(); + + if (UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::GetInventorySystemComponent(Actor)) + { + Complete(); + } +} + +UGIS_AsyncAction_WaitInventorySystem* UGIS_AsyncAction_WaitInventorySystemInitialized::WaitInventorySystemInitialized(UObject* WorldContext, AActor* TargetActor) +{ + return CreateWaitAction(WorldContext, TargetActor, 0.5, -1); +} + +void UGIS_AsyncAction_WaitInventorySystemInitialized::OnExecutionAction() +{ + // Already found. + UGIS_InventorySystemComponent* ExistingOne = InventorySysPtr.IsValid() ? InventorySysPtr.Get() : nullptr; + if (IsValid(ExistingOne)) + { + return; + } + + AActor* Actor = GetActor(); + if (UGIS_InventorySystemComponent* InventorySys = UGIS_InventorySystemComponent::GetInventorySystemComponent(Actor)) + { + if (InventorySys->IsInventoryInitialized()) + { + Complete(); + return; + } + InventorySysPtr = InventorySys; + InventorySysPtr->OnInventorySystemInitializedEvent.AddDynamic(this, &ThisClass::OnSystemInitialized); + } +} + +void UGIS_AsyncAction_WaitInventorySystemInitialized::Cleanup() +{ + Super::Cleanup(); + UGIS_InventorySystemComponent* ExistingOne = InventorySysPtr.IsValid() ? InventorySysPtr.Get() : nullptr; + if (IsValid(ExistingOne)) + { + ExistingOne->OnInventorySystemInitializedEvent.RemoveAll(this); + } +} + +void UGIS_AsyncAction_WaitInventorySystemInitialized::OnSystemInitialized() +{ + Complete(); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.cpp new file mode 100644 index 0000000..753570f --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.cpp @@ -0,0 +1,81 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Async/GIS_AsyncAction_WaitItemFragmentDataChanged.h" +#include "Engine/Engine.h" +#include "UObject/Object.h" +#include "GIS_ItemFragment.h" +#include "GIS_ItemInstance.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitItemFragmentDataChanged) + +UGIS_AsyncAction_WaitItemFragmentDataChanged* UGIS_AsyncAction_WaitItemFragmentDataChanged::WaitItemFragmentStateChanged(UObject* WorldContext, UGIS_ItemInstance* ItemInstance, + TSoftClassPtr FragmentClass) +{ + if (!IsValid(WorldContext)) + { + GIS_LOG(Warning, "invalid world context!") + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::LogAndReturnNull); + if (!IsValid(World)) + { + GIS_LOG(Warning, "can't get world from context:%s", *GetNameSafe(WorldContext)); + return nullptr; + } + + if (!IsValid(ItemInstance)) + { + GIS_LOG(Warning, "invalid item instance"); + return nullptr; + } + + TSubclassOf Class = FragmentClass.LoadSynchronous(); + if (Class == nullptr) + { + GIS_LOG(Warning, "invalid fragment class"); + return nullptr; + } + + UGIS_AsyncAction_WaitItemFragmentDataChanged* NewAction = NewObject(GetTransientPackage(), StaticClass()); + NewAction->ItemInstance = ItemInstance; + NewAction->FragmentClass = Class; + NewAction->RegisterWithGameInstance(World->GetGameInstance()); + return NewAction; +} + +void UGIS_AsyncAction_WaitItemFragmentDataChanged::Activate() +{ + UGIS_ItemInstance* Item = ItemInstance.IsValid() ? ItemInstance.Get() : nullptr; + if (IsValid(Item)) + { + Item->OnFragmentStateAddedEvent.AddDynamic(this, &ThisClass::OnFragmentStateChanged); + Item->OnFragmentStateUpdatedEvent.AddDynamic(this, &ThisClass::OnFragmentStateChanged); + } +} + +void UGIS_AsyncAction_WaitItemFragmentDataChanged::Cancel() +{ + Super::Cancel(); + UGIS_ItemInstance* Item = ItemInstance.IsValid() ? ItemInstance.Get() : nullptr; + if (IsValid(Item)) + { + Item->OnFragmentStateAddedEvent.RemoveAll(this); + Item->OnFragmentStateUpdatedEvent.RemoveAll(this); + ItemInstance.Reset(); + FragmentClass = nullptr; + } +} + +void UGIS_AsyncAction_WaitItemFragmentDataChanged::OnFragmentStateChanged(const UGIS_ItemFragment* Fragment, const FInstancedStruct& State) +{ + if (ShouldBroadcastDelegates()) + { + if (Fragment != nullptr && Fragment->GetClass() == FragmentClass) + { + OnStateChanged.Broadcast(Fragment, State); + } + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagFloat.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagFloat.cpp new file mode 100644 index 0000000..48c6e4c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagFloat.cpp @@ -0,0 +1,187 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/GIS_GameplayTagFloat.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_GameplayTagFloat) + + +FString FGIS_GameplayTagFloat::GetDebugString() const +{ + return FString::Printf(TEXT("%sx%f"), *Tag.ToString(), Value); +} + +void FGIS_GameplayTagFloatContainer::AddItem(FGameplayTag Tag, float Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to AddItem"), ELogVerbosity::Warning); + return; + } + + if (Value > 0) + { + for (FGIS_GameplayTagFloat& Item : Items) + { + // handle adding to existing value. + if (Item.Tag == Tag) + { + const float OldValue = Item.Value; + const float NewValue = Item.Value + Value; + Item.Value = NewValue; + TagToValueMap[Tag] = NewValue; + MarkItemDirty(Item); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, OldValue, NewValue); + } + return; + } + } + + // handle adding new item. + FGIS_GameplayTagFloat& NewItem = Items.Emplace_GetRef(Tag, Value); + TagToValueMap.Add(Tag, Value); + MarkItemDirty(NewItem); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, 0, Value); + } + } +} + +void FGIS_GameplayTagFloatContainer::SetItem(FGameplayTag Tag, float Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to SetItem"), ELogVerbosity::Warning); + return; + } + for (FGIS_GameplayTagFloat& Item : Items) + { + if (Item.Tag == Tag) + { + const float OldValue = Item.Value; + Item.Value = Value; + TagToValueMap[Tag] = Value; + MarkItemDirty(Item); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, OldValue, Value); + } + return; + } + } + + FGIS_GameplayTagFloat& NewItem = Items.Emplace_GetRef(Tag, Value); + MarkItemDirty(NewItem); + TagToValueMap.Add(Tag, Value); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, 0, Value); + } +} + +void FGIS_GameplayTagFloatContainer::SetItems(const TArray& NewItems) +{ + Items = NewItems; + TagToValueMap.Empty(); + for (const FGIS_GameplayTagFloat& NewItem : NewItems) + { + TagToValueMap.Add(NewItem.Tag, NewItem.Value); + } + MarkArrayDirty(); +} + +void FGIS_GameplayTagFloatContainer::EmptyItems() +{ + Items.Empty(); + TagToValueMap.Empty(); + MarkArrayDirty(); +} + +void FGIS_GameplayTagFloatContainer::RemoveItem(FGameplayTag Tag, float Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning); + return; + } + + //@TODO: Should we error if you try to remove a Item that doesn't exist or has a smaller count? + if (Value > 0) + { + for (auto It = Items.CreateIterator(); It; ++It) + { + FGIS_GameplayTagFloat& Item = *It; + if (Item.Tag == Tag) + { + if (Item.Value <= Value) + { + const float OldValue = Item.Value; + It.RemoveCurrent(); + TagToValueMap.Remove(Tag); + MarkArrayDirty(); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, OldValue, 0); + } + } + else + { + const float OldValue = Item.Value; + const float NewValue = Item.Value - Value; + Item.Value = NewValue; + TagToValueMap[Tag] = NewValue; + MarkItemDirty(Item); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Tag, OldValue, NewValue); + } + } + return; + } + } + } +} + +void FGIS_GameplayTagFloatContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + FGIS_GameplayTagFloat& Item = Items[Index]; + TagToValueMap.Remove(Item.Tag); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Item.Tag, Item.PrevValue, 0); + } + Item.PrevValue = 0; + } +} + +void FGIS_GameplayTagFloatContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGIS_GameplayTagFloat& Item = Items[Index]; + TagToValueMap.Add(Item.Tag, Item.Value); + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Item.Tag, 0, Item.Value); + } + Item.PrevValue = Item.Value; + } +} + +void FGIS_GameplayTagFloatContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + FGIS_GameplayTagFloat& Item = Items[Index]; + TagToValueMap[Item.Tag] = Item.Value; + if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagFloatUpdate(Item.Tag, Item.PrevValue, Item.Value); + } + Item.PrevValue = Item.Value; + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagInteger.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagInteger.cpp new file mode 100644 index 0000000..9df7f7f --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Attributes/GIS_GameplayTagInteger.cpp @@ -0,0 +1,167 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Attributes/GIS_GameplayTagInteger.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_GameplayTagInteger) + +FString FGIS_GameplayTagInteger::GetDebugString() const +{ + return FString::Printf(TEXT("%sx%d"), *Tag.ToString(), Value); +} + +void FGIS_GameplayTagIntegerContainer::AddItem(FGameplayTag Tag, int32 Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to AddItem"), ELogVerbosity::Warning); + return; + } + + if (Value > 0) + { + for (FGIS_GameplayTagInteger& Item : Items) + { + if (Item.Tag == Tag) + { + const int32 OldValue = Item.Value; + const int32 NewValue = Item.Value + Value; + Item.Value = NewValue; + TagToValueMap[Tag] = NewValue; + MarkItemDirty(Item); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, OldValue, NewValue); + } + return; + } + } + + FGIS_GameplayTagInteger& NewItem = Items.Emplace_GetRef(Tag, Value); + TagToValueMap.Add(Tag, Value); + MarkItemDirty(NewItem); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, 0, Value); + } + } +} + +void FGIS_GameplayTagIntegerContainer::SetItem(FGameplayTag Tag, int32 Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to SetItem"), ELogVerbosity::Warning); + return; + } + for (FGIS_GameplayTagInteger& Item : Items) + { + if (Item.Tag == Tag) + { + int32 OldValue = Item.Value; + Item.Value = Value; + TagToValueMap[Tag] = Value; + MarkItemDirty(Item); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, OldValue, Value); + } + return; + } + } + + FGIS_GameplayTagInteger& NewItem = Items.Emplace_GetRef(Tag, Value); + MarkItemDirty(NewItem); + TagToValueMap.Add(Tag, Value); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, 0, Value); + } +} + +void FGIS_GameplayTagIntegerContainer::RemoveItem(FGameplayTag Tag, int32 Value) +{ + if (!Tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning); + return; + } + + //@TODO: Should we error if you try to remove a Item that doesn't exist or has a smaller count? + if (Value > 0) + { + for (auto It = Items.CreateIterator(); It; ++It) + { + FGIS_GameplayTagInteger& Item = *It; + if (Item.Tag == Tag) + { + if (Item.Value <= Value) + { + const int32 OldValue = Item.Value; + It.RemoveCurrent(); + TagToValueMap.Remove(Tag); + MarkArrayDirty(); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, OldValue, 0); + } + } + else + { + const int32 OldValue = Item.Value; + const int32 NewValue = Item.Value - Value; + Item.Value = NewValue; + TagToValueMap[Tag] = NewValue; + MarkItemDirty(Item); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Tag, OldValue, NewValue); + } + } + return; + } + } + } +} + +void FGIS_GameplayTagIntegerContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + FGIS_GameplayTagInteger& Item = Items[Index]; + + TagToValueMap.Remove(Item.Tag); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Item.Tag, Item.PrevValue, 0); + } + Item.PrevValue = 0; + } +} + +void FGIS_GameplayTagIntegerContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGIS_GameplayTagInteger& Item = Items[Index]; + TagToValueMap.Add(Item.Tag, Item.Value); + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Item.Tag, 0, Item.Value); + } + Item.PrevValue = Item.Value; + } +} + +void FGIS_GameplayTagIntegerContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + FGIS_GameplayTagInteger& Item = Items[Index]; + TagToValueMap[Item.Tag] = Item.Value; + if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast(ContainerOwner)) + { + Interface->OnTagIntegerUpdate(Item.Tag, Item.PrevValue, Item.Value); + } + Item.PrevValue = Item.Value; + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_CollectionContainer.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_CollectionContainer.cpp new file mode 100644 index 0000000..781d653 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_CollectionContainer.cpp @@ -0,0 +1,76 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_CollectionContainer.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CollectionContainer) + + +bool FGIS_CollectionEntry::IsValidEntry() const +{ + return Id.IsValid() && IsValid(Instance) && IsValid(Definition); +} + +FGIS_CollectionContainer::FGIS_CollectionContainer(UGIS_InventorySystemComponent* InInventory) +{ + OwningComponent = InInventory; +} + +void FGIS_CollectionContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + const FGIS_CollectionEntry& Entry = Entries[Index]; + + // already in the list. + if (Entry.IsValidEntry() && OwningComponent->CollectionIdToInstanceMap.Contains(Entry.Instance->GetCollectionId())) + { + OwningComponent->OnCollectionRemoved(Entry); + } + else if (OwningComponent->PendingCollections.Contains(Entry.Id)) + { + GIS_OWNED_CLOG(OwningComponent, Warning, "Discard pending collection(%s).", *GetNameSafe(OwningComponent->PendingCollections[Entry.Id].Definition)) + OwningComponent->PendingCollections.Remove(Entry.Id); + } + } +} + +void FGIS_CollectionContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + const FGIS_CollectionEntry& Entry = Entries[Index]; + + if (OwningComponent->GetOwner() && Entry.IsValidEntry()) + { + OwningComponent->OnCollectionAdded(Entry); + } + else + { + OwningComponent->PendingCollections.Add(Entry.Id, Entry); + } + } +} + +void FGIS_CollectionContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + const FGIS_CollectionEntry& Entry = Entries[Index]; + + if (Entry.IsValidEntry() && OwningComponent->CollectionIdToInstanceMap.Contains(Entry.Instance->GetCollectionId())) //Already Added. + { + OwningComponent->OnCollectionUpdated(Entry); + } + else if (OwningComponent->PendingCollections.Contains(Entry.Id)) //In pending list. + { + OwningComponent->PendingCollections.Emplace(Entry.Id, Entry); // Updated to pending. + } + else + { + OwningComponent->PendingCollections.Add(Entry.Id, Entry); //Add to pending list. + } + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemCollection.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemCollection.cpp new file mode 100644 index 0000000..1fa711e --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemCollection.cpp @@ -0,0 +1,861 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemCollection.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_InventoryMeesages.h" +#include "GIS_InventorySubsystem.h" +#include "GIS_InventoryTags.h" +#include "Items/GIS_ItemInstance.h" +#include "Items/GIS_ItemDefinition.h" +#include "Engine/ActorChannel.h" +#include "Net/UnrealNetwork.h" +#include "Engine/Engine.h" +#include "Engine/NetDriver.h" +#include "Engine/BlueprintGeneratedClass.h" +#include "GIS_ItemRestriction.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemCollection) + +bool UGIS_ItemCollectionDefinition::IsSupportedForNetworking() const +{ + return true; +} + +TSubclassOf UGIS_ItemCollectionDefinition::GetCollectionInstanceClass() const +{ + return UGIS_ItemCollection::StaticClass(); +} + +UGIS_ItemCollection::GIS_CollectionNotifyLocker::GIS_CollectionNotifyLocker(UGIS_ItemCollection& InItemCollection): ItemCollection(InItemCollection) +{ + ItemCollection.NotifyLocker++; +} + +UGIS_ItemCollection::GIS_CollectionNotifyLocker::~GIS_CollectionNotifyLocker() +{ + ItemCollection.NotifyLocker--; +} + +UGIS_ItemCollection::UGIS_ItemCollection(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer), Container(this) +{ +} + +void UGIS_ItemCollection::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, Container); + + //fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4 + UBlueprintGeneratedClass* bpClass = Cast(this->GetClass()); + if (bpClass != nullptr) + { + bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps); + } +} + +bool UGIS_ItemCollection::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) +{ + check(!HasAnyFlags(RF_ClassDefaultObject)); + check(GetOuter() != nullptr); + + AActor* Owner = CastChecked(GetOuter()); + + bool bProcessed = false; + + FWorldContext* const Context = GEngine->GetWorldContextFromWorld(GetWorld()); + if (Context != nullptr) + { + for (FNamedNetDriver& Driver : Context->ActiveNetDrivers) + { + if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateFunction(Owner, Function)) + { + Driver.NetDriver->ProcessRemoteFunction(Owner, Function, Parms, OutParms, Stack, this); + bProcessed = true; + } + } + } + + return bProcessed; +} + +int32 UGIS_ItemCollection::GetFunctionCallspace(UFunction* Function, FFrame* Stack) +{ + if (HasAnyFlags(RF_ClassDefaultObject) || !IsSupportedForNetworking()) + { + // This handles absorbing authority/cosmetic + return GEngine->GetGlobalFunctionCallspace(Function, this, Stack); + } + check(GetOuter() != nullptr); + return GetOuter()->GetFunctionCallspace(Function, Stack); +} + +bool UGIS_ItemCollection::IsInitialized() const +{ + return bInitialized; +} + +FString UGIS_ItemCollection::GetCollectionName() const +{ + if (Definition) + { + if (Definition->CollectionTag.IsValid()) + { + TArray TagNames; + UGameplayTagsManager::Get().SplitGameplayTagFName(Definition->CollectionTag, TagNames); + if (!TagNames.IsEmpty()) + { + return FString::Format(TEXT("{0} Collection"), {TagNames.Last().ToString()}); + } + } + return FString::Format(TEXT("{0}"), {GetNameSafe(this)}); + } + return TEXT("Invalid Collection!!!"); +} + +FString UGIS_ItemCollection::GetDebugString() const +{ + return FString::Format(TEXT("{0}({1})"), {GetCollectionName(), GetNameSafe(OwningInventory->GetOwner())}); +} + +bool UGIS_ItemCollection::HasItem(const UGIS_ItemInstance* Item, int32 Amount, bool SimilarItem) const +{ + if (Item == nullptr) { return false; } + + return GetItemAmount(Item, SimilarItem) >= Amount; +} + +FGIS_ItemInfo UGIS_ItemCollection::AddItem(const FGIS_ItemInfo& ItemInfo) +{ + //The actually added item info; 实际添加的道具信息 + FGIS_ItemInfo ItemInfoAdded = FGIS_ItemInfo(ItemInfo.Item, 0, this); + + FGIS_ItemInfo CanAddItemInfo; + if (CanAddItem(ItemInfo, CanAddItemInfo)) + { + //非唯一道具或数量为一,直接加. + if (!CanAddItemInfo.Item->IsUnique() || CanAddItemInfo.Amount <= 1) + { + //要添加的信息 + FGIS_ItemInfo ItemInfoToAdd(CanAddItemInfo.Item, CanAddItemInfo.Amount, ItemInfo); + ItemInfoAdded = AddInternal(ItemInfoToAdd); + } + else //unique item can stack. + { + //先加1个 + FGIS_ItemInfo OriginalResult = AddItem(FGIS_ItemInfo(CanAddItemInfo.Item, 1, ItemInfo)); + // 遍历加入数量为1的道具, 每个都有不同的GUID + for (int32 i = 1; i < CanAddItemInfo.Amount; i++) + { + UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), CanAddItemInfo.Item); + check(DuplicatedItem && DuplicatedItem->IsItemValid()) + AddItem(FGIS_ItemInfo(DuplicatedItem, 1, ItemInfo)); + } + ItemInfoAdded = FGIS_ItemInfo(CanAddItemInfo.Amount, OriginalResult); + } + } + + // overflow + if (ItemInfoAdded.Amount < ItemInfo.Amount) + { + HandleItemOverflow(ItemInfo, ItemInfoAdded); + } + + return ItemInfoAdded; +} + +int32 UGIS_ItemCollection::AddItems(const TArray& ItemInfos) +{ + int32 TotalAdded = 0; + for (int32 i = 0; i < ItemInfos.Num(); i++) + { + TotalAdded += AddItem(ItemInfos[i]).Amount; + } + return TotalAdded; +} + +FGIS_ItemInfo UGIS_ItemCollection::AddItem(UGIS_ItemInstance* Item, int32 Amount) +{ + return AddItem(FGIS_ItemInfo(Item, Amount)); +} + +bool UGIS_ItemCollection::CanAddItem(const FGIS_ItemInfo& Input, FGIS_ItemInfo& Output) +{ + if (!bInitialized || Input.Item == nullptr || !Input.Item->IsItemValid() || Input.Amount < 1) + { + return false; + } + + FGIS_ItemInfo ModifiedInput = Input; + + /* + * Documentation: For none-unique item(means stackable), if you want to add 2 apples with id x, there are following case: + * 1.There are already 10 apples with id x in your inventory,this will turn your "add 2 apples of id x" request into "add 2 apples of id y", resulting 12 apples with id y in your inventory. + * 2.There's no any apples in your inventory, resulting in 2 apples with id x in your inventory. + * 文档: 道具不唯一. 举例: 你要加2个苹果(ID是X)到库存,且苹果定义上它不是唯一的(即可叠加)。 + * 如果1:库存里如果已经有10个苹果(ID是Y)。就把 “你要加2个ID为X的苹果” 变成“你要加2个ID为Y的苹果”. + * 如果2:库存里如果没有任何苹果。那么就不变,其结果就是往库存里加2个ID是X的苹果. + */ + if (!ModifiedInput.Item->IsUnique()) + { + FGIS_ItemInfo SimilarItemInfo; + if (GetItemInfo(ModifiedInput.Item, SimilarItemInfo)) + { + //修改要加入的道具信息为相似道具、但保留Amount. + ModifiedInput = FGIS_ItemInfo(Input.Amount, SimilarItemInfo); + } + } + + if (ModifiedInput.Item->GetOwningCollection() != nullptr && ModifiedInput.Item->GetOwningCollection() != this) + { + UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), ModifiedInput.Item); + check(DuplicatedItem && DuplicatedItem->IsItemValid()) + GIS_LOG(Verbose, "The Item(%s) was duplicated, and the duplicated one was added because the item is already part of another collection on actor %s.", *Input.GetDebugString(), + *GetNameSafe(Input.Item->GetOuter())) + ModifiedInput = FGIS_ItemInfo(DuplicatedItem, Input.Amount, Input); + } + + if (ModifiedInput.Item->GetOuter() != nullptr && ModifiedInput.Item->GetOuter() != OwningInventory->GetOwner()) + { + UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), ModifiedInput.Item, false); + check(DuplicatedItem && DuplicatedItem->IsItemValid()) + GIS_LOG(Verbose, "The Item(%s) was duplicated, and the duplicated one was added because the item was created by another actor %s.", *Input.GetDebugString(), + *GetNameSafe(Input.Item->GetOuter())) + ModifiedInput = FGIS_ItemInfo(DuplicatedItem, Input.Amount, Input); + } + + for (int32 i = 0; i < Definition->Restrictions.Num(); i++) + { + auto Restriction = Definition->Restrictions[i]; + if (Restriction && !Restriction->CanAddItem(ModifiedInput, this)) + { + GIS_CLOG(Warning, "can't add item(%s) due to restriction:%s", *Input.GetDebugString(), *Restriction->GetClass()->GetName()) + return false; + } + } + + Output = ModifiedInput; + return true; +} + +bool UGIS_ItemCollection::CanItemStack(const FGIS_ItemInfo& ItemInfo, const FGIS_ItemStack& ItemStack) const +{ + return !ItemInfo.Item->IsUnique() && ItemStack.Item->StackableEquivalentTo(ItemInfo.Item); +} + +bool UGIS_ItemCollection::RemoveItemCondition(const FGIS_ItemInfo& ItemInfo, FGIS_ItemInfo& OutItemInfo) +{ + if (!ItemInfo.Item || !ItemInfo.Item->IsItemValid() || ItemInfo.Amount < 1) + { + return false; + } + OutItemInfo = ItemInfo; + + if (!ItemInfo.Item->IsUnique()) + { + FGIS_ItemInfo SimilarItemInfo; + if (GetItemInfo(ItemInfo.Item, SimilarItemInfo)) + { + OutItemInfo = FGIS_ItemInfo(SimilarItemInfo.Item, ItemInfo.Amount); + } + } + + // make sure is this collection. + if (OutItemInfo.ItemCollection != this) + { + OutItemInfo.ItemCollection = this; + } + + for (int32 i = 0; i < Definition->Restrictions.Num(); i++) + { + // Check against src item info in restriction. + if (!Definition->Restrictions[i]->CanRemoveItem(OutItemInfo)) + { + return false; + } + } + + return true; +} + +FGIS_ItemInfo UGIS_ItemCollection::RemoveItem(const FGIS_ItemInfo& ItemInfo) +{ + FGIS_ItemInfo ItemInfoToRemove; + if (!RemoveItemCondition(ItemInfo, ItemInfoToRemove)) + { + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + return RemoveInternal(ItemInfoToRemove); +} + +void UGIS_ItemCollection::RemoveAll() +{ + for (int32 i = Container.Stacks.Num() - 1; i >= 0; i--) + { + FGIS_ItemStack& ItemStack = Container.Stacks[i]; + RemoveItem(FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount)); + } +} + +bool UGIS_ItemCollection::GetItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const +{ + if (const FGIS_ItemStack* Stack = Container.Stacks.FindByKey(InStackId)) + { + OutItemInfo = *Stack; + return true; + } + return false; +} + +bool UGIS_ItemCollection::FindItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const +{ + return GetItemInfoByStackId(InStackId, OutItemInfo); +} + +bool UGIS_ItemCollection::GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const +{ + if (Item == nullptr) + { + return false; + } + + bool bFoundSimilar = false; + FGIS_ItemInfo SimilarItemInfo; + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + const FGIS_ItemStack& ItemStack = Container.Stacks[i]; + //Is not unique but has same definition. + if (!Item->IsUnique() && ItemStack.Item->GetDefinition() == Item->GetDefinition()) + { + SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection); + bFoundSimilar = true; + } + + if (ItemStack.Item->GetItemId() != Item->GetItemId()) + { + continue; + } + + //Found unique item with same id. + OutItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection); + return true; + } + + if (bFoundSimilar) + { + OutItemInfo = SimilarItemInfo; + } + return bFoundSimilar; +} + +bool UGIS_ItemCollection::GetItemInfoByDefinition(const TSoftObjectPtr& ItemDefinition, FGIS_ItemInfo& OutItemInfo) +{ + if (ItemDefinition.IsNull()) + { + return false; + } + + UGIS_ItemDefinition* LoadedItemDefinition = ItemDefinition.LoadSynchronous(); + + if (LoadedItemDefinition == nullptr) + { + return false; + } + + bool bFoundSimilar = false; + FGIS_ItemInfo SimilarItemInfo; + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + FGIS_ItemStack& ItemStack = Container.Stacks[i]; + if (ItemStack.Item->GetDefinition() == LoadedItemDefinition) + { + SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection); + bFoundSimilar = true; + } + } + + if (bFoundSimilar && SimilarItemInfo.IsValid()) + { + OutItemInfo = SimilarItemInfo; + return true; + } + return false; +} + +bool UGIS_ItemCollection::GetItemInfosByDefinition(const TSoftObjectPtr& ItemDefinition, TArray& OutItemInfos) +{ + if (ItemDefinition.IsNull()) + { + return false; + } + + UGIS_ItemDefinition* LoadedItemDefinition = ItemDefinition.LoadSynchronous(); + + if (LoadedItemDefinition == nullptr) + { + return false; + } + + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + FGIS_ItemStack& ItemStack = Container.Stacks[i]; + if (ItemStack.Item->GetDefinition() == LoadedItemDefinition) + { + FGIS_ItemInfo SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection); + OutItemInfos.Add(SimilarItemInfo); + } + } + return OutItemInfos.Num() > 0; +} + +int32 UGIS_ItemCollection::GetItemAmount(const UGIS_ItemInstance* Item, bool SimilarItem) const +{ + if (Item == nullptr) { return 0; } + + int32 Count = 0; + + for (int i = 0; i < Container.Stacks.Num(); i++) + { + if (Container.Stacks[i].Item && Container.Stacks[i].Item->SimilarTo(Item)) + { + Count += Container.Stacks[i].Amount; + } + } + + return Count; +} + +int32 UGIS_ItemCollection::GetItemAmount(TSoftObjectPtr ItemDefinition, bool CountStacks) const +{ + if (ItemDefinition.IsNull()) + { + return 0; + } + UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous(); + if (LoadedDefinition == nullptr) + { + return 0; + } + int32 Count = 0; + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + if (Container.Stacks[i].Item == nullptr) + { + continue; + } + if (LoadedDefinition != Container.Stacks[i].Item->GetDefinition()) + { + continue; + } + Count += CountStacks ? 1 : Container.Stacks[i].Amount; + } + + return Count; +} + +TArray UGIS_ItemCollection::GetAllItemInfos() const +{ + TArray Infos; + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + const FGIS_ItemStack& ItemStack = Container.Stacks[i]; + Infos.Add(FGIS_ItemInfo(ItemStack)); + } + return Infos; +} + +TArray UGIS_ItemCollection::GetAllItems() const +{ + TArray Rets; + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + if (Container.Stacks[i].IsValidStack()) + { + Rets.AddUnique(Container.Stacks[i].Item); + } + } + return Rets; +} + +const TArray& UGIS_ItemCollection::GetAllItemStacks() const +{ + return Container.Stacks; +} + +int32 UGIS_ItemCollection::GetItemStacksNum() const +{ + return Container.Stacks.Num(); +} + +void UGIS_ItemCollection::AddItemStack(const FGIS_ItemStack& Stack) +{ + check(Stack.IsValidStack()) + int32 Idx = Container.Stacks.AddDefaulted(); + Container.Stacks[Idx] = Stack; + FGIS_ItemStack& AddedStack = Container.Stacks[Idx]; + + OnPreItemStackAdded(Stack, Idx); + OnItemStackAdded(AddedStack); + + Container.MarkItemDirty(AddedStack); +} + +void UGIS_ItemCollection::RemoveItemStackAtIndex(int32 Idx, bool bRemoveFromCollection) +{ + check(Container.Stacks.IsValidIndex(Idx)) + const FGIS_ItemStack RemovedStack = Container.Stacks[Idx]; + OnItemStackRemoved(RemovedStack); + Container.Stacks.RemoveAt(Idx); + if (bRemoveFromCollection) + { + RemovedStack.Item->UnassignCollection(this); + if (OwningInventory->IsReplicatedSubObjectRegistered(RemovedStack.Item)) + { + OwningInventory->RemoveReplicatedSubObject(RemovedStack.Item); + } + } + Container.MarkArrayDirty(); +} + +void UGIS_ItemCollection::UpdateItemStackAmountAtIndex(int32 Idx, int32 NewAmount) +{ + check(Container.Stacks.IsValidIndex(Idx)) + FGIS_ItemStack& StackToUpdate = Container.Stacks[Idx]; + StackToUpdate.Amount = NewAmount; + OnItemStackUpdated(StackToUpdate); + Container.MarkItemDirty(StackToUpdate); +} + +void UGIS_ItemCollection::OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx) +{ + if (IsValid(Stack.Item)) + { + if (Stack.Item->GetOwningCollection() != this) + { + Stack.Item->AssignCollection(this); + } + + if (!OwningInventory->IsReplicatedSubObjectRegistered(Stack.Item)) + { + OwningInventory->AddReplicatedSubObject(Stack.Item); + } + } +} + +void UGIS_ItemCollection::OnItemStackAdded(const FGIS_ItemStack& Stack) +{ + check(Stack.IsValidStack()) + if (OwningInventory) + { + StackToIdxMap.Add(Stack.Id, Stack.Index); + FGIS_InventoryStackUpdateMessage Message; + Message.Inventory = OwningInventory; + Message.ChangeType = EGIS_ItemStackChangeType::WasAdded; + Message.CollectionId = CollectionId; + Message.Instance = Stack.Item; + Message.StackId = Stack.Id; + Message.NewCount = Stack.Amount; + Message.Delta = Stack.Amount; + OwningInventory->OnInventoryStackUpdate.Broadcast(Message); + + GIS_CLOG(Verbose, "added item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount) + } +} + +void UGIS_ItemCollection::OnItemStackRemoved(const FGIS_ItemStack& Stack) +{ + if (OwningInventory) + { + StackToIdxMap.Remove(Stack.Id); + FGIS_InventoryStackUpdateMessage Message; + Message.Inventory = OwningInventory; + Message.ChangeType = EGIS_ItemStackChangeType::WasRemoved; + Message.CollectionId = CollectionId; + Message.Instance = Stack.Item; + Message.StackId = Stack.Id; + Message.NewCount = 0; + Message.Delta = -Stack.LastObservedAmount; + OwningInventory->OnInventoryStackUpdate.Broadcast(Message); + + GIS_CLOG(Verbose, "removed item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount) + } +} + +void UGIS_ItemCollection::OnItemStackUpdated(const FGIS_ItemStack& Stack) +{ + if (OwningInventory) + { + if (StackToIdxMap.Contains(Stack.Id)) + { + StackToIdxMap[Stack.Id] = Stack.Index; + } + FGIS_InventoryStackUpdateMessage Message; + Message.Inventory = OwningInventory; + Message.ChangeType = EGIS_ItemStackChangeType::Changed; + Message.CollectionId = CollectionId; + Message.Instance = Stack.Item; + Message.StackId = Stack.Id; + Message.NewCount = Stack.Amount; + Message.Delta = Stack.Amount - Stack.LastObservedAmount; + OwningInventory->OnInventoryStackUpdate.Broadcast(Message); + GIS_CLOG(Verbose, "updated item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount) + } +} + +void UGIS_ItemCollection::ProcessPendingItemStacks() +{ + if (bInitialized) + { + TArray Added; + for (const TPair& Pending : PendingItemStacks) + { + if (Pending.Value.IsValidStack()) + { + Added.AddUnique(Pending.Key); + } + } + for (int32 i = 0; i < Added.Num(); i++) + { + FGuid AddedStackId = Added[i]; + const FGIS_ItemStack& AddedStack = PendingItemStacks[AddedStackId]; + OnItemStackAdded(AddedStack); + GIS_CLOG(Verbose, "added item stack:item(%s),amount(%d) from pending list.", *AddedStack.Item->GetDefinition()->GetName(), AddedStack.Amount) + PendingItemStacks.Remove(AddedStackId); + } + } +} + +void UGIS_ItemCollection::SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition) +{ + check(OwningInventory != nullptr); + check(NewDefinition != nullptr); + if (!bInitialized) + { + Definition = NewDefinition; + CollectionTag = Definition->CollectionTag; + bInitialized = true; + } +} + +void UGIS_ItemCollection::SetCollectionTag(FGameplayTag NewTag) +{ + CollectionTag = NewTag; +} + +void UGIS_ItemCollection::SetCollectionId(FGuid NewId) +{ + if (!CollectionId.IsValid()) + { + CollectionId = NewId; + } +} + +FGIS_ItemInfo UGIS_ItemCollection::AddInternal(const FGIS_ItemInfo& ItemInfo) +{ + bool bFound = false; + FGIS_ItemStack AddedItemStack; + + if (!ItemInfo.Item->IsUnique()) + { + // First, trying to add to existing stack. + int32 TargetStackIdx = Container.IndexOfById(ItemInfo.StackId); + if (TargetStackIdx != INDEX_NONE) + { + check(Container.Stacks.IsValidIndex(TargetStackIdx)) + const FGIS_ItemStack& ExistingStack = Container.Stacks[TargetStackIdx]; + if (ExistingStack.Collection == this && ExistingStack.Item == ItemInfo.Item) + { + UpdateItemStackAmountAtIndex(TargetStackIdx, ItemInfo.Amount + ExistingStack.Amount); + AddedItemStack = ExistingStack; + bFound = true; + } + } + + if (!bFound) + { + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + if (CanItemStack(ItemInfo, Container.Stacks[i]) == false) + { + continue; + } + + UpdateItemStackAmountAtIndex(i, ItemInfo.Amount + Container.Stacks[i].Amount); + AddedItemStack = Container.Stacks[i]; + bFound = true; + break; + } + } + } + + //新增. + if (!bFound) + { + AddedItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, ItemInfo.Amount, this, ItemInfo.Index); + AddItemStack(AddedItemStack); + } + FGIS_ItemInfo AddedItemInfo = FGIS_ItemInfo(ItemInfo.Item, ItemInfo.Amount, this, AddedItemStack.Id); + return AddedItemInfo; +} + +void UGIS_ItemCollection::HandleItemOverflow(const FGIS_ItemInfo& OriginalItemInfo, const FGIS_ItemInfo& ItemInfoAdded) +{ + FGIS_ItemInfo RejectedItemInfo = FGIS_ItemInfo(OriginalItemInfo.Amount - ItemInfoAdded.Amount, OriginalItemInfo); + + FGIS_ItemInfo ReturnedItemInfo; + if (Definition->OverflowOptions.bReturnOverflow) + { + if (OriginalItemInfo.ItemCollection != nullptr && OriginalItemInfo.ItemCollection != this) + { + ReturnedItemInfo = OriginalItemInfo.ItemCollection->AddItem(RejectedItemInfo); + } + } + + if (Definition->OverflowOptions.bSendRejectedMessage) + { + FGIS_InventoryAddItemInfoRejectedMessage Message; + Message.Inventory = OwningInventory; + Message.Collection = this; + Message.OriginalItemInfo = OriginalItemInfo; + Message.ItemInfoAdded = ItemInfoAdded; + Message.RejectedItemInfo = RejectedItemInfo; + Message.ReturnedItemInfo = ReturnedItemInfo; + OwningInventory->OnInventoryAddItemInfo_Rejected.Broadcast(Message); + } + GIS_CLOG(Warning, "try add %s, added %d, rejected %d, returned:%d", *OriginalItemInfo.GetDebugString(), ItemInfoAdded.Amount, RejectedItemInfo.Amount, ReturnedItemInfo.Amount); +} + +FGIS_ItemInfo UGIS_ItemCollection::RemoveInternal(const FGIS_ItemInfo& ItemInfo) +{ + int32 Removed = 0; + + FGIS_ItemStack ItemStackToRemove; + + { + int32 Idx = Container.IndexOfByIds(ItemInfo.StackId, ItemInfo.Item->GetItemId()); + + if (Idx != INDEX_NONE) + { + ItemStackToRemove = SimpleInternalItemRemove(ItemInfo, Removed, Idx); + } + } + + //如果已经移除的数量未达到指定移除的数量,就继续移除。 + if (Removed < ItemInfo.Amount) + { + TArray TempStacks = Container.Stacks; + for (int32 i = TempStacks.Num() - 1; i >= 0; i--) + { + if (TempStacks[i].Item == nullptr || TempStacks[i].Item->GetItemId() != ItemInfo.Item->GetItemId()) + { + continue; + } + ItemStackToRemove = SimpleInternalItemRemove(ItemInfo, Removed, i); + if (Removed >= ItemInfo.Amount) + { + break; + } + } + } + + const FGIS_ItemInfo RemovedItemInfo = FGIS_ItemInfo(ItemInfo.Item, Removed, this, ItemStackToRemove.Id); + + if (Removed == 0) + { + return RemovedItemInfo; + } + + return RemovedItemInfo; +} + +FGIS_ItemStack UGIS_ItemCollection::SimpleInternalItemRemove(const FGIS_ItemInfo& ItemInfo, int32& AlreadyRemoved, int32 StackIndex) +{ + check(Container.Stacks.IsValidIndex(StackIndex)); + const FGIS_ItemStack FoundStack = Container.Stacks[StackIndex]; + int32 RemainingToRemove = ItemInfo.Amount - AlreadyRemoved; //我要减10个,已经减了4个,还要减6个 + int32 NewAmount = FoundStack.Amount - RemainingToRemove; + if (NewAmount <= 0) //如果栈里还有3个,不够减,那么已经减去的数量就成了7个,然后栈被移除。 + { + AlreadyRemoved += FoundStack.Amount; + RemoveItemStackAtIndex(StackIndex); + } + else //如果栈里还有7个,够减,那么已经减去的数量为10,栈还剩下7-6=1个。 + { + AlreadyRemoved += RemainingToRemove; + UpdateItemStackAmountAtIndex(StackIndex, NewAmount); + } + return FoundStack; +} + +FGIS_ItemInfo UGIS_ItemCollection::GiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection) +{ + if (OwningInventory->GetOwnerRole() != ROLE_Authority) + { + GIS_CLOG(Warning, "Has no authority") + return FGIS_ItemInfo::None; + } + if (!ItemInfo.IsValid()) + { + GIS_CLOG(Warning, "invalid ItemInfo to give.") + return FGIS_ItemInfo::None; + } + + if (ItemInfo.Item->GetOwningInventory() == ItemCollection->GetOwningInventory()) + { + GIS_CLOG(Warning, "Item:%s already belongs to this inventory.", *ItemInfo.GetDebugString()); + return FGIS_ItemInfo::None; + } + + FGIS_ItemInfo RemovedItemInfo = RemoveItem(ItemInfo); + + FGIS_ItemInfo ItemInfoToAdd = FGIS_ItemInfo(ItemInfo.Item, RemovedItemInfo.Amount, this); + FGIS_ItemInfo GivenItemInfo = ItemCollection->AddItem(ItemInfoToAdd); + if (GivenItemInfo.Amount != RemovedItemInfo.Amount) + { + // Failed to add so add it back to the previous collection. + //ItemInfoToAdd.Amount = ItemInfoToAdd.Amount - GivenItemInfo.Amount; + //if (ItemInfoToAdd.IsValid()) + //{ + // AddItem(FGIS_ItemInfo(ItemInfoToAdd)); + //} + } + return GivenItemInfo; +} + +void UGIS_ItemCollection::ServerGiveItem_Implementation(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection) +{ + GiveItem(ItemInfo, ItemCollection); +} + +void UGIS_ItemCollection::GiveAllItems(UGIS_ItemCollection* OtherItemCollection) +{ + for (int i = Container.Stacks.Num() - 1; i >= 0; i--) + { + auto& itemStack = Container.Stacks[i]; + GiveItem(FGIS_ItemInfo(itemStack), OtherItemCollection); + } +} + +void UGIS_ItemCollection::ServerGiveAllItems_Implementation(UGIS_ItemCollection* OtherItemCollection) +{ + GiveAllItems(OtherItemCollection); +} + +int32 UGIS_ItemCollection::GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const +{ + if (ItemInfo.Item->IsUnique()) + { + //预计还有5个,而要添加10个,那么就只能5个。 + //预计还有10个,而要添加6个,那么就能放6个。 + return FMath::Min(ItemInfo.Amount, AvailableAdditionalStacks); + } + + //满了,且没有已经存在的Stack可以叠上去。 + if (AvailableAdditionalStacks == 0 && !HasItem(ItemInfo.Item, 1)) + { + return 0; + } + return ItemInfo.Amount; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemMultiStackCollection.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemMultiStackCollection.cpp new file mode 100644 index 0000000..1daf67d --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemMultiStackCollection.cpp @@ -0,0 +1,329 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemMultiStackCollection.h" + +#include "GIS_InventorySystemComponent.h" +#include "GIS_InventoryTags.h" +#include "GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemMultiStackCollection) + +UGIS_ItemMultiStackCollectionDefinition::UGIS_ItemMultiStackCollectionDefinition() +{ + StackSizeLimitAttribute = GIS_AttributeTags::StackSizeLimit; +} + +TSubclassOf UGIS_ItemMultiStackCollectionDefinition::GetCollectionInstanceClass() const +{ + return UGIS_ItemMultiStackCollection::StaticClass(); +} + +UGIS_ItemMultiStackCollection::UGIS_ItemMultiStackCollection(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +{ +} + +bool UGIS_ItemMultiStackCollection::GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const +{ + if (Item == nullptr) + { + return false; + } + + bool bFoundSimilar = false; + FGIS_ItemInfo SimilarItemInfo; + + for (int32 i = Container.Stacks.Num() - 1; i >= 0; i--) + { + const FGIS_ItemStack& ItemStack = Container.Stacks[i]; + + if (!ItemStack.IsValidStack()) + { + continue; + } + + if (!Item->IsUnique() && ItemStack.Item->GetDefinition() == Item->GetDefinition()) + { + SimilarItemInfo = FGIS_ItemInfo(ItemStack); + bFoundSimilar = true; + } + if (Container.Stacks[i].Item->GetItemId() != Item->GetItemId()) + { + continue; + } + + OutItemInfo = FGIS_ItemInfo(Container.Stacks[i]); + return true; + } + + if (bFoundSimilar) + { + OutItemInfo = SimilarItemInfo; + } + return bFoundSimilar; +} + +int32 UGIS_ItemMultiStackCollection::GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const +{ + int32 AmountToAdd = ItemInfo.Amount; + + int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item); + + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + const FGIS_ItemStack& itemStack = Container.Stacks[i]; + if (CanItemStack(ItemInfo, itemStack) == false) { continue; } + + if (itemStack.Amount == MaxStackSize) { continue; } + + int32 TotalToSet = itemStack.Amount + AmountToAdd; + int32 SizeDifference = TotalToSet - MaxStackSize; + + if (SizeDifference <= 0) + { + AmountToAdd = 0; + break; + } + + AmountToAdd = SizeDifference; + } + + int32 StacksToAdd = AmountToAdd / MaxStackSize; + int32 RemainderStack = AmountToAdd % MaxStackSize; + + + if (AvailableAdditionalStacks > StacksToAdd) + { + return ItemInfo.Amount; + } + if (AvailableAdditionalStacks == StacksToAdd) + { + return ItemInfo.Amount - RemainderStack; + } + + return ItemInfo.Amount - RemainderStack - MaxStackSize * (StacksToAdd - AvailableAdditionalStacks); +} + +FGIS_ItemInfo UGIS_ItemMultiStackCollection::AddInternal(const FGIS_ItemInfo& ItemInfo) +{ + // total amounts of item need to add. + int32 AmountToAdd = ItemInfo.Amount; + int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item); + + // temp struct to record added stack. + FGIS_ItemStack AddedItemStack; + + { + //try adding item amount on top of the existing stack. 尝试在指定栈上新增数量。 + int32 TargetStackIdx = Container.IndexOfById(ItemInfo.StackId); + if (TargetStackIdx != INDEX_NONE) + { + check(Container.Stacks.IsValidIndex(TargetStackIdx)) + const FGIS_ItemStack& TargetStack = Container.Stacks[TargetStackIdx]; + + //Make sure the target stack is valid and can be stacked with new item. 确保指定栈有效且可与请求的道具信息堆叠。 + if (TargetStack.IsValidStack() && CanItemStack(ItemInfo, TargetStack)) + { + AddedItemStack = TargetStack; + IncreaseStackAmount(TargetStackIdx, MaxStackSize, AmountToAdd); + } + } + } + + //尝试在已经存在的兼容栈上新增数量。 + for (int32 i = 0; i < Container.Stacks.Num(); i++) + { + const FGIS_ItemStack& ItemStack = Container.Stacks[i]; + if (CanItemStack(ItemInfo, ItemStack) == false) + { + continue; + } + AddedItemStack = ItemStack; + + int32 AmountAdded = IncreaseStackAmount(i, MaxStackSize, AmountToAdd); + } + + /** + * 30/10=3,30%10=0 need 3 stacks. + * 25/10=2,25%10=1 need 3 stacks. + * 77/21=3,77%21=1 need 4 stacks. (4*21 = 84) > 77 + */ + int32 StacksToAdd = AmountToAdd / MaxStackSize; + int32 RemainderStack = AmountToAdd % MaxStackSize; + + for (int32 i = 0; i < StacksToAdd; i++) + { + FGIS_ItemStack NewItemStack; + NewItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, MaxStackSize, this); + AddedItemStack = NewItemStack; + AddItemStack(NewItemStack); + } + + if (RemainderStack != 0) + { + FGIS_ItemStack NewItemStack; + NewItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, RemainderStack, this); + AddedItemStack = NewItemStack; + AddItemStack(NewItemStack); + } + + return FGIS_ItemInfo(ItemInfo.Item, AddedItemStack.Amount, this); +} + +int32 UGIS_ItemMultiStackCollection::GetMaxStackSize(UGIS_ItemInstance* Item) const +{ + const UGIS_ItemMultiStackCollectionDefinition* MyDefinition = CastChecked(Definition); + + if (!Item->GetDefinition()->HasIntegerAttribute(MyDefinition->StackSizeLimitAttribute)) + { + return MyDefinition->DefaultStackSizeLimit; + } + return Item->GetDefinition()->GetIntegerAttribute(MyDefinition->StackSizeLimitAttribute); + + // if (!Item->HasIntegerAttribute(MyDefinition->StackSizeLimitAttribute)) + // { + // return MyDefinition->DefaultStackSizeLimit; + // } + // return Item->GetIntegerAttribute(MyDefinition->StackSizeLimitAttribute); +} + +/** + * 案例: 每个栈最多放10个,现在有35个苹果,占用4个栈,这两个栈的ID分别是A,B,C,D。它们的分布是[(A:10),(B:10)(C:10)(D:5)] + * 假设传入的ItemInfo是栈A里删除11个苹果:那么先从栈A删除10个,这时候栈A空了,还需要在栈B里移除1个。最后的栈分布是[(B:9),(C:10),(D:5)] + * 假设传入的ItemInfo是栈A里删除34个苹果:那么先从栈ABC都删除10个,ABC清空,然后剩下栈D里删除4个,最后的栈分布是[(D:1)] + */ +FGIS_ItemInfo UGIS_ItemMultiStackCollection::RemoveInternal(const FGIS_ItemInfo& ItemInfo) +{ + int32 AlreadyRemoved = 0; + int32 AmountToRemove = ItemInfo.Amount; + //上一次移除的StackIndex + int32 PreviousStackIndexWithSameItem = INDEX_NONE; + int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item); + FGIS_ItemStack RemovedItemStack; + + { + // Try remove from existing stack first. + int32 StackIdx = Container.IndexOfById(ItemInfo.StackId); + if (StackIdx != INDEX_NONE) + { + check(Container.Stacks.IsValidIndex(StackIdx)) + RemovedItemStack = Container.Stacks[StackIdx]; + PreviousStackIndexWithSameItem = RemoveItemFromStack(StackIdx, PreviousStackIndexWithSameItem, MaxStackSize, AmountToRemove, AlreadyRemoved); + } + } + + // 继续从其他栈中移除这个道具 + TArray TempStacks = Container.Stacks; + for (int i = TempStacks.Num() - 1; i >= 0; i--) + { + if (AmountToRemove <= 0) { break; } + + if (TempStacks[i].Item == nullptr || TempStacks[i].Item->GetItemId() != ItemInfo.Item->GetItemId()) { continue; } + //忽略前面移除的Stack. + if (RemovedItemStack == TempStacks[i]) { continue; } + + RemovedItemStack = TempStacks[i]; + + PreviousStackIndexWithSameItem = RemoveItemFromStack(i, PreviousStackIndexWithSameItem, MaxStackSize, AmountToRemove, AlreadyRemoved); + } + + //No any stacks contain this item instance; 无任何包含此道具实例的道具栈,因此可以从该集合完全移除。 + if (PreviousStackIndexWithSameItem == INDEX_NONE) + { + ItemInfo.Item->UnassignCollection(this); + if (OwningInventory->IsReplicatedSubObjectRegistered(ItemInfo.Item)) + { + OwningInventory->RemoveReplicatedSubObject(ItemInfo.Item); + } + } + + if (AlreadyRemoved == 0) + { + return FGIS_ItemInfo(ItemInfo.Item, AlreadyRemoved, this); + } + return FGIS_ItemInfo(ItemInfo.Item, AlreadyRemoved, this); +} + +int32 UGIS_ItemMultiStackCollection::RemoveItemFromStack(int32 Index, int32 PrevStackIndexWithSameItem, int32 MaxStackSize, int32& AmountToRemove, int32& AlreadyRemoved) +{ + check(Container.Stacks.IsValidIndex(Index)); + + int32 AmountInStack = Container.Stacks[Index].Amount; + + if (PrevStackIndexWithSameItem != INDEX_NONE) + { + check(Container.Stacks.IsValidIndex(PrevStackIndexWithSameItem)) + + int32 MergedAmount = AmountInStack + Container.Stacks[PrevStackIndexWithSameItem].Amount; + + if (MergedAmount > MaxStackSize) //假设每个栈最大100,当前的栈有70,之前的栈有60,即一共130,就超了30. + { + UpdateItemStackAmountAtIndex(PrevStackIndexWithSameItem, MaxStackSize); //让之前的栈满,即60+40=100; + AmountInStack = MergedAmount - MaxStackSize; //再让当前栈变成 130-100=30(即少了70-40=30) + } + else //the total size of 2 stacks doesn't reach max stack size. 假设每个栈最大100,当前的栈有70,之前的栈有10,即一共80,没有超过100. + { + // merge current stack's amount into prev stack. //让之前栈为10+70=80. + UpdateItemStackAmountAtIndex(PrevStackIndexWithSameItem, MergedAmount); + // and empty this one. 当前栈归0. + AmountInStack = 0; + } + } + + if (AmountToRemove == 0) { return PrevStackIndexWithSameItem; } + + int32 NewAmount = AmountInStack - AmountToRemove; + if (NewAmount <= 0) + { + AmountToRemove = -NewAmount; + AlreadyRemoved += AmountInStack; + + //Item can be stored within multiple stacks, we don't need to remove it from this collection now. Item在此集合可以属于多个栈,不在这里移除。 + RemoveItemStackAtIndex(Index, false); + } + else + { + AlreadyRemoved += AmountToRemove; + AmountToRemove = 0; + UpdateItemStackAmountAtIndex(Index, NewAmount); + PrevStackIndexWithSameItem = Index; + } + return PrevStackIndexWithSameItem; +} + +/** + * 代入: Stack(item,70);MaxStackSize(100),AmountToAdd(50) 结果:Stack(item,100), AmountToAdd(20), 返回实际添加30 + * 代入: Stack(item,40);MaxStackSize(100),AmountToAdd(50) 结果:Stack(item,90), AmountToAdd(0), 返回实际添加50 + */ +int32 UGIS_ItemMultiStackCollection::IncreaseStackAmount(int32 StackIdx, int32 MaxStackSize, int32& AmountToAdd) +{ + check(Container.Stacks.IsValidIndex(StackIdx)) + + const FGIS_ItemStack& ItemStack = Container.Stacks[StackIdx]; + if (ItemStack.Amount == MaxStackSize) + { + return 0; + } + int32 OriginAmountToAdd = AmountToAdd; + int32 NewAmount = ItemStack.Amount + AmountToAdd; + + int32 OverflowedAmount = NewAmount - MaxStackSize; // <=0 no overflow >0 means overflow. + + // This stack can hold the new amount. 这个栈装得下。 + if (OverflowedAmount <= 0) + { + AmountToAdd = 0; //all amounts have been added. 无需再添加。 + } + else //This stack overflow. 这个栈溢出了。 + { + NewAmount = MaxStackSize; + //Still need to add overflowed amount. 超过最大尺寸后的剩余要增加的数量 + AmountToAdd = OverflowedAmount; + } + + UpdateItemStackAmountAtIndex(StackIdx, NewAmount); + //实际增加的数量 + return OriginAmountToAdd - AmountToAdd; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction.cpp new file mode 100644 index 0000000..c3975a0 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction.cpp @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemRestriction.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction) + +bool UGIS_ItemRestriction::CanAddItem(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + return CanAddItemInternal(ItemInfo, ReceivingCollection); +} + +bool UGIS_ItemRestriction::CanRemoveItem(FGIS_ItemInfo& ItemInfo) const +{ + return CanRemoveItemInternal(ItemInfo); +} + +bool UGIS_ItemRestriction::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + return true; +} + +bool UGIS_ItemRestriction::CanRemoveItemInternal_Implementation(FGIS_ItemInfo& ItemInfo) const +{ + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StackSizeLimit.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StackSizeLimit.cpp new file mode 100644 index 0000000..e536329 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StackSizeLimit.cpp @@ -0,0 +1,57 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemRestriction_StackSizeLimit.h" + +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_StackSizeLimit) + +UGIS_ItemRestriction_StackSizeLimit::UGIS_ItemRestriction_StackSizeLimit() +{ + DefaultStackSizeLimit = 99; +} + +bool UGIS_ItemRestriction_StackSizeLimit::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + int32 MaxStackSize = GetStackSizeLimit(ItemInfo.Item); + + FGIS_ItemInfo ExistingItemInfoResult; + if (!ReceivingCollection->GetItemInfo(ItemInfo.Item, ExistingItemInfoResult)) + { + ItemInfo.Amount = FMath::Min(ItemInfo.Amount, MaxStackSize); + return true; + } + + int32 ItemAmountsThatFit = FMath::Min(MaxStackSize - ExistingItemInfoResult.Amount, ItemInfo.Amount); + ItemAmountsThatFit = FMath::Max(0, ItemAmountsThatFit); + + if (ItemAmountsThatFit == 0) + { + return false; + } + + ItemInfo.Amount = ItemAmountsThatFit; + return true; +} + + +int32 UGIS_ItemRestriction_StackSizeLimit::GetStackSizeLimit(const UGIS_ItemInstance* Item) const +{ + if (Item == nullptr) + { + return 0; + } + if (StackSizeLimitAttributeTag.IsValid() && Item->GetDefinition()->HasIntegerAttribute(StackSizeLimitAttributeTag)) + { + return Item->GetDefinition()->GetIntegerAttribute(StackSizeLimitAttributeTag); + } + // if (StackSizeLimitAttributeTag.IsValid() && Item->HasIntegerAttribute(StackSizeLimitAttributeTag)) + // { + // return Item->GetIntegerAttribute(StackSizeLimitAttributeTag); + // } + + return DefaultStackSizeLimit; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StacksNumLimit.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StacksNumLimit.cpp new file mode 100644 index 0000000..8281d42 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_StacksNumLimit.cpp @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemRestriction_StacksNumLimit.h" + +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_StacksNumLimit) + +bool UGIS_ItemRestriction_StacksNumLimit::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + int32 ExistingNum = ReceivingCollection->GetItemStacksNum(); + + int32 AvailableAdditionalStacks = MaxStacksNum - ExistingNum; + + int32 ItemAmountsThatFit = ReceivingCollection->GetItemAmountFittingInLimitedAdditionalStacks(ItemInfo, AvailableAdditionalStacks); + + if (ItemAmountsThatFit == 0) + { + return false; + } + + ItemInfo = FGIS_ItemInfo(ItemAmountsThatFit, ItemInfo); + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_TagRequirements.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_TagRequirements.cpp new file mode 100644 index 0000000..43403cf --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_TagRequirements.cpp @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemRestriction_TagRequirements.h" + +#include "Items/GIS_ItemInstance.h" +#include "GIS_ItemCollection.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_TagRequirements) + +bool UGIS_ItemRestriction_TagRequirements::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + if (TagQuery.IsEmpty()) + { + return true; + } + return TagQuery.Matches(ItemInfo.Item->GetItemTags()); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_UniqueOnly.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_UniqueOnly.cpp new file mode 100644 index 0000000..fed4655 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemRestriction_UniqueOnly.cpp @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemRestriction_UniqueOnly.h" + +#include "Items/GIS_ItemInstance.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_UniqueOnly) + +bool UGIS_ItemRestriction_UniqueOnly::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const +{ + // this restriction will not modify the item info will be added, so just set it to OutItemInfo + return ItemInfo.Item->IsUnique(); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemSlotCollection.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemSlotCollection.cpp new file mode 100644 index 0000000..ad5778a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Collections/GIS_ItemSlotCollection.cpp @@ -0,0 +1,583 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemSlotCollection.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySystemComponent.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_LogChannels.h" +#include "Misc/DataValidation.h" +#include "UObject/ObjectSaveContext.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemSlotCollection) + +TSubclassOf UGIS_ItemSlotCollectionDefinition::GetCollectionInstanceClass() const +{ + return UGIS_ItemSlotCollection::StaticClass(); +} + +bool UGIS_ItemSlotCollectionDefinition::IsValidSlotIndex(int32 SlotIndex) const +{ + return SlotDefinitions.IsValidIndex(SlotIndex); +} + +int32 UGIS_ItemSlotCollectionDefinition::GetIndexOfSlot(const FGameplayTag& SlotName) const +{ + return TagToIndexMap.Contains(SlotName) ? TagToIndexMap[SlotName] : INDEX_NONE; +} + +FGameplayTag UGIS_ItemSlotCollectionDefinition::GetSlotOfIndex(int32 SlotIndex) const +{ + return SlotDefinitions.IsValidIndex(SlotIndex) ? SlotDefinitions[SlotIndex].Tag : FGameplayTag::EmptyTag; +} + +const TArray& UGIS_ItemSlotCollectionDefinition::GetSlotDefinitions() const +{ + return SlotDefinitions; +} + +bool UGIS_ItemSlotCollectionDefinition::GetSlotDefinition(int32 SlotIndex, FGIS_ItemSlotDefinition& OutDefinition) const +{ + if (SlotDefinitions.IsValidIndex(SlotIndex)) + { + OutDefinition = SlotDefinitions[SlotIndex]; + return true; + } + return false; +} + +bool UGIS_ItemSlotCollectionDefinition::GetSlotDefinition(const FGameplayTag& SlotName, FGIS_ItemSlotDefinition& OutDefinition) const +{ + if (TagToIndexMap.Contains(SlotName)) + { + check(SlotDefinitions.IsValidIndex(TagToIndexMap[SlotName])); + OutDefinition = SlotDefinitions[TagToIndexMap[SlotName]]; + return true; + } + return false; +} + +int32 UGIS_ItemSlotCollectionDefinition::GetSlotIndexWithinGroup(FGameplayTag GroupTag, FGameplayTag SlotTag) const +{ + if (SlotGroupMap.Contains(GroupTag)) + { + if (SlotGroupMap[GroupTag].SlotToIndexMap.Contains(SlotTag)) + { + return SlotGroupMap[GroupTag].SlotToIndexMap[SlotTag]; + } + } + return INDEX_NONE; +} + +UGIS_ItemSlotCollection::UGIS_ItemSlotCollection(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void UGIS_ItemSlotCollection::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + // DOREPLIFETIME(ThisClass, ItemBySlots) +} + +const UGIS_ItemSlotCollectionDefinition* UGIS_ItemSlotCollection::GetMyDefinition() const +{ + return MyDefinition; +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::AddItem(const FGIS_ItemInfo& ItemInfo) +{ + if (!ItemInfo.IsValid()) + { + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + return AddItem(ItemInfo, GetTargetSlotIndex(ItemInfo.Item)); +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::AddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex) +{ + FGIS_ItemInfo ItemInfoAdded = AddItemInternal(ItemInfo, SlotIndex); + + if (ItemInfoAdded.Amount < ItemInfo.Amount) + { + HandleItemOverflow(ItemInfo, ItemInfoAdded); + } + return ItemInfoAdded; +} + +void UGIS_ItemSlotCollection::ServerAddItem_Implementation(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex) +{ + AddItem(ItemInfo, SlotIndex); +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::AddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName) +{ + int32 Index = MyDefinition->GetIndexOfSlot(SlotName); + if (Index <= INDEX_NONE) + { + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + return AddItem(ItemInfo, Index); +} + +void UGIS_ItemSlotCollection::ServerAddItemBySlotName_Implementation(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName) +{ + AddItemBySlotName(ItemInfo, SlotName); +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::RemoveItem(const FGIS_ItemInfo& ItemInfo) +{ + int32 SlotIndex = GetItemSlotIndex(ItemInfo.Item); + if (SlotIndex == INDEX_NONE) + { + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + return RemoveItem(SlotIndex, ItemInfo.Amount); +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::RemoveItem(int32 SlotIndex, int32 Amount) +{ + if (!SlotToStackMap.Contains(SlotIndex)) + { + return FGIS_ItemInfo(nullptr, 0, this); + } + + const FGIS_ItemStack* ItemToRemove = Container.FindById(SlotToStackMap[SlotIndex]); + + if (ItemToRemove == nullptr) + { + return FGIS_ItemInfo(nullptr, 0, this); + } + + int32 AmountToRemove = Amount > 0 ? Amount : ItemToRemove->Amount; + FGIS_ItemInfo Removed = RemoveInternal(FGIS_ItemInfo(ItemToRemove->Item, AmountToRemove, this, ItemToRemove->Id)); + + return Removed; +} + +bool UGIS_ItemSlotCollection::IsItemFitWithSlot(const UGIS_ItemInstance* Item, int32 SlotIndex) const +{ + if (!IsValid(Item)) + { + GIS_CLOG(Error, "invalid item") + return false; + } + const TArray& SlotDefinitions = MyDefinition->SlotDefinitions; + if (SlotDefinitions.IsEmpty() || !SlotDefinitions.IsValidIndex(SlotIndex)) + { + GIS_CLOG(Error, "has empty slots!") + return false; + } + + const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[SlotIndex]; + + // doesn't match definition. + if (!SlotDefinition.MatchItem(Item)) + { + return false; + } + + // slot is empty. + if (!SlotToStackMap.Contains(SlotIndex)) + { + return true; + } + + // slot is not empty, check if it can stack. + if (!Item->IsUnique()) + { + if (const FGIS_ItemStack* ExistingStack = Container.FindById(SlotToStackMap[SlotIndex])) + { + if (ExistingStack->Item->StackableEquivalentTo(Item)) + { + return true; + } + } + } + + // can't stack, check new item can replace existing item. + return MyDefinition->bNewItemPriority; +} + +int32 UGIS_ItemSlotCollection::GetTargetSlotIndex(const UGIS_ItemInstance* Item) const +{ + const TArray& SlotDefinitions = MyDefinition->SlotDefinitions; + if (SlotDefinitions.IsEmpty()) + { + GIS_CLOG(Error, "has empty slots!") + return INDEX_NONE; + } + + int32 FoundUsedSlot = INDEX_NONE; + int32 FoundEmptySlot = INDEX_NONE; + + for (int32 i = 0; i < SlotDefinitions.Num(); i++) + { + const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[i]; + if (!SlotDefinition.MatchItem(Item)) + { + continue; + } + + FoundUsedSlot = i; + + // i对应的槽为空 + if (!SlotToStackMap.Contains(i)) + { + if (FoundEmptySlot != INDEX_NONE) + { + continue; + } + FoundEmptySlot = i; + continue; + } + + //非唯一且可堆叠. + if (!Item->IsUnique()) + { + if (const FGIS_ItemStack* ExistingStack = Container.FindById(SlotToStackMap[i])) + { + if (ExistingStack->Item->StackableEquivalentTo(Item)) + { + return i; + } + } + } + } + + if (FoundEmptySlot != INDEX_NONE) + { + return FoundEmptySlot; + } + return FoundUsedSlot; +} + +bool UGIS_ItemSlotCollection::GetItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const +{ + int32 Index = MyDefinition->GetIndexOfSlot(SlotTag); + + if (Index != INDEX_NONE) + { + OutItemInfo = GetItemInfoAtSlot(Index); + + return OutItemInfo.IsValid(); + } + return false; +} + +bool UGIS_ItemSlotCollection::FindItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const +{ + return GetItemInfoAtSlot(SlotTag, OutItemInfo); +} + +bool UGIS_ItemSlotCollection::GetItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const +{ + int32 Index = MyDefinition->GetIndexOfSlot(SlotTag); + + if (Index != INDEX_NONE) + { + OutItemStack = GetItemStackAtSlot(Index); + return OutItemStack.IsValidStack(); + } + return false; +} + +bool UGIS_ItemSlotCollection::FindItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const +{ + return GetItemStackAtSlot(SlotTag, OutItemStack); +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::GetItemInfoAtSlot(int32 SlotIndex) const +{ + if (SlotIndex < 0) { return FGIS_ItemInfo::None; } + + FGIS_ItemStack ItemStack = GetItemStackAtSlot(SlotIndex); + if (ItemStack.IsValidStack()) + { + return FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection); + } + return FGIS_ItemInfo::None; +} + +FGIS_ItemStack UGIS_ItemSlotCollection::GetItemStackAtSlot(int32 SlotIndex) const +{ + if (SlotToStackMap.Contains(SlotIndex)) + { + if (const FGIS_ItemStack* Stack = Container.FindById(SlotToStackMap[SlotIndex])) + { + return *Stack; + } + } + return FGIS_ItemStack(); +} + +FGameplayTag UGIS_ItemSlotCollection::GetItemSlotName(const UGIS_ItemInstance* Item) const +{ + int32 SlotIndex = GetItemSlotIndex(Item); + return MyDefinition->GetSlotOfIndex(SlotIndex); +} + +int32 UGIS_ItemSlotCollection::GetItemSlotIndex(const UGIS_ItemInstance* Item) const +{ + const TArray& SlotDefinitions = MyDefinition->SlotDefinitions; + + int32 StackableEquivalentItemIndex = INDEX_NONE; + for (int i = 0; i < SlotDefinitions.Num(); i++) + { + const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[i]; + if (!SlotDefinition.MatchItem(Item)) + { + continue; + } + + if (!SlotToStackMap.Contains(i) || !SlotToStackMap[i].IsValid()) + { + continue; + } + + if (const FGIS_ItemStack* Stack = Container.FindById(SlotToStackMap[i])) + { + if (Stack->Item == Item) + { + return i; + } + + if (Stack->Item->StackableEquivalentTo(Item)) + { + StackableEquivalentItemIndex = i; + } + } + } + + return StackableEquivalentItemIndex; +} + +FGIS_ItemInfo UGIS_ItemSlotCollection::AddItemInternal(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex) +{ + FGIS_ItemInfo CanAddItemInfo; + const bool CanAddResult = CanAddItem(ItemInfo, CanAddItemInfo); + if (!CanAddResult) + { + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + if (SlotIndex == INDEX_NONE) + { + GIS_CLOG(Warning, "invalid valid target slot(%d) to put item:%s", SlotIndex, *ItemInfo.GetDebugString()) + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + FGIS_ItemSlotDefinition SlotDefinition; + if (!MyDefinition->GetSlotDefinition(SlotIndex, SlotDefinition)) + { + GIS_CLOG(Verbose, "No slot definition found for slot index:%d", SlotIndex) + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + if (!MyDefinition->bNewItemPriority && CanAddItemInfo.Item->IsUnique() && SlotToStackMap.Contains(SlotIndex) && SlotToStackMap[SlotIndex].IsValid()) + { + GIS_CLOG(Verbose, "Can't add item info because the target slot:%s was occupied. ItemInfo:%s.", *SlotDefinition.Tag.ToString(), *ItemInfo.GetDebugString()) + return FGIS_ItemInfo(ItemInfo.Item, 0, this); + } + + // similar item amount for this item. + int32 CurrentAmount = GetItemAmount(CanAddItemInfo.Item.Get()); + + FGIS_ItemInfo SetItemInfo; + if (SetItemAmount(FGIS_ItemInfo(CanAddItemInfo.Amount + CurrentAmount, CanAddItemInfo), SlotIndex, true, SetItemInfo)) + { + return SetItemInfo; + } + return FGIS_ItemInfo(ItemInfo.Item, 0, this); +} + +int32 UGIS_ItemSlotCollection::StackIdToSlotIndex(FGuid InStackId) const +{ + if (!InStackId.IsValid()) + { + return INDEX_NONE; + } + if (StackToIdxMap.Contains(InStackId)) + { + return StackToIdxMap[InStackId]; + } + return INDEX_NONE; +} + +int32 UGIS_ItemSlotCollection::SlotIndexToStackIndex(int32 InSlotIndex) const +{ + if (SlotToStackMap.Contains(InSlotIndex)) + { + return Container.IndexOfById(SlotToStackMap[InSlotIndex]); + } + return INDEX_NONE; +} + +FGuid UGIS_ItemSlotCollection::SlotIndexToStackId(int32 InSlotIndex) const +{ + return SlotToStackMap.Contains(InSlotIndex) && SlotToStackMap[InSlotIndex].IsValid() ? SlotToStackMap[InSlotIndex] : FGuid(); +} + +void UGIS_ItemSlotCollection::OnRep_ItemsBySlot() +{ +} + +void UGIS_ItemSlotCollection::SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition) +{ + Super::SetDefinition(NewDefinition); + check(OwningInventory != nullptr) + check(Definition != nullptr) + if (bInitialized) + { + MyDefinition = CastChecked(Definition); + if (OwningInventory->GetOwnerRole() >= ROLE_Authority) + { + if (MyDefinition->SlotDefinitions.Num() == 0) + { + GIS_CLOG(Error, "has empty slots") + } + } + } +} + +void UGIS_ItemSlotCollection::OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx) +{ + Super::OnPreItemStackAdded(Stack, Idx); +} + +void UGIS_ItemSlotCollection::OnItemStackAdded(const FGIS_ItemStack& Stack) +{ + check(Stack.IsValidStack()) + if (OwningInventory) + { + SlotToStackMap.Add(Stack.Index, Stack.Id); + } + Super::OnItemStackAdded(Stack); +} + +void UGIS_ItemSlotCollection::OnItemStackRemoved(const FGIS_ItemStack& Stack) +{ + if (OwningInventory) + { + SlotToStackMap.Remove(Stack.Index); + } + Super::OnItemStackRemoved(Stack); +} + +bool UGIS_ItemSlotCollection::SetItemAmount(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex, bool RemovePreviousItem, FGIS_ItemInfo& ItemInfoAdded) +{ + if (!OwningInventory->GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "has no authority!"); + return false; + } + + int32 Amount = ItemInfo.Amount; + + int32 StackIdx = SlotIndexToStackIndex(SlotIndex); + + // Found valid stack + if (StackIdx != INDEX_NONE) + { + const FGIS_ItemStack& CurrentStack = Container.Stacks[StackIdx]; + + if (ItemInfo.Item->StackableEquivalentTo(CurrentStack.Item)) + { + // reduce existing amount to get the amount needed to add. + Amount -= CurrentStack.Amount; + } + else if (RemovePreviousItem) + { + FGIS_ItemInfo RemovedItem = RemoveItem(FGIS_ItemInfo(CurrentStack)); + if (RemovedItem.Amount > 0) + { + FGIS_ItemInfo CanAddItemInfo; + if (MyDefinition->bTryGivePrevItemToNewItemCollection && ItemInfo.ItemCollection != nullptr && ItemInfo.ItemCollection->CanAddItem(RemovedItem, CanAddItemInfo)) + { + GIS_CLOG(Verbose, "An existing item has been replaced by a new one, and the old one has been added to the collection where the new item coming from. prev:%s new:%s", + *RemovedItem.GetDebugString(), *ItemInfo.GetDebugString()) + ItemInfo.ItemCollection->AddItem(CanAddItemInfo); + } + else + { + GIS_CLOG(Verbose, "An item has been replaced by a new one, and the old one has gone forever! prev: %s new:%s", + *RemovedItem.GetDebugString(), *ItemInfo.GetDebugString()) + } + } + } + else + { + return false; + } + } + + // Not found, add new item. + ItemInfoAdded = AddInternal(FGIS_ItemInfo(Amount, SlotIndex, ItemInfo)); + + return true; +} + +#if WITH_EDITOR +void UGIS_ItemSlotCollectionDefinition::PreSave(FObjectPreSaveContext SaveContext) +{ + // remove repeated slot by name. + { + TArray SlotNames; + TArray Slots; + for (int32 i = 0; i < SlotDefinitions.Num(); ++i) + { + if (!SlotNames.Contains(SlotDefinitions[i].Tag)) + { + Slots.Add(SlotDefinitions[i]); + SlotNames.Add(SlotDefinitions[i].Tag); + } + } + SlotDefinitions = Slots; + } + + IndexToTagMap.Empty(); + TagToIndexMap.Empty(); + + for (int32 i = 0; i < SlotDefinitions.Num(); ++i) + { + if (!SlotDefinitions[i].Tag.IsValid()) + { + continue; + } + if (!IndexToTagMap.Contains(i)) + { + IndexToTagMap.Add(i, SlotDefinitions[i].Tag); + } + if (!TagToIndexMap.Contains(SlotDefinitions[i].Tag)) + { + TagToIndexMap.Add(SlotDefinitions[i].Tag, i); + } + } + + TArray Groups; + SlotGroupMap.Empty(); + for (int32 i = 0; i < SlotGroups.Num(); i++) + { + FGIS_ItemSlotGroup Group; + int32 Idx = 0; + for (int32 j = 0; j < SlotDefinitions.Num(); j++) + { + if (SlotDefinitions[j].Tag.MatchesTag(SlotGroups[i])) + { + Group.IndexToSlotMap.Add(Idx, SlotDefinitions[j].Tag); + Group.SlotToIndexMap.Add(SlotDefinitions[j].Tag, Idx); + Idx++; + } + } + if (!Group.IndexToSlotMap.IsEmpty()) + { + SlotGroupMap.Add(SlotGroups[i], Group); + } + } + + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment.cpp new file mode 100644 index 0000000..c0bf011 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment.cpp @@ -0,0 +1,48 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Fragments/GIS_ItemFragment.h" + +#include "Items/GIS_ItemStack.h" +#include "Misc/DataValidation.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment) + + +bool UGIS_ItemFragment::IsMixinDataSerializable() const +{ + return IsStateSerializable(); +} + +TObjectPtr UGIS_ItemFragment::GetCompatibleMixinDataType() const +{ + return GetCompatibleStateType(); +} + +bool UGIS_ItemFragment::MakeDefaultMixinData(FInstancedStruct& DefaultState) const +{ + return MakeDefaultState(DefaultState); +} + + +bool UGIS_ItemFragment::MakeDefaultState_Implementation(FInstancedStruct& DefaultState) const +{ + return false; +} + +const UScriptStruct* UGIS_ItemFragment::GetCompatibleStateType_Implementation() const +{ + return nullptr; +} + +bool UGIS_ItemFragment::IsStateSerializable_Implementation() const +{ + return false; +} + +#if WITH_EDITOR +bool UGIS_ItemFragment::FragmentDataValidation_Implementation(FText& OutMessage) const +{ + return true; +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_DynamicAttributes.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_DynamicAttributes.cpp new file mode 100644 index 0000000..8c087e3 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_DynamicAttributes.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemFragment_DynamicAttributes.h" + +#include "Items/GIS_ItemInstance.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_DynamicAttributes) + +void UGIS_ItemFragment_DynamicAttributes::OnInstanceCreated(UGIS_ItemInstance* Instance) const +{ + for (int32 i = 0; i < InitialFloatAttributes.Num(); i++) + { + if (InitialFloatAttributes[i].Tag.IsValid()) + { + Instance->SetFloatAttribute(InitialFloatAttributes[i].Tag, InitialFloatAttributes[i].Value); + } + } + for (int32 i = 0; i < InitialIntegerAttributes.Num(); i++) + { + if (InitialIntegerAttributes[i].Tag.IsValid()) + { + Instance->SetIntegerAttribute(InitialIntegerAttributes[i].Tag, InitialIntegerAttributes[i].Value); + } + } + // for (int32 i=0;iSetBoolAttribute(InitialBoolAttributes[i].Tag,InitialBoolAttributes[i].Value); + // } +} + +float UGIS_ItemFragment_DynamicAttributes::GetFloatAttributeDefault(FGameplayTag AttributeTag) const +{ + return FloatAttributeMap.Contains(AttributeTag) ? FloatAttributeMap[AttributeTag] : 0; +} + +int32 UGIS_ItemFragment_DynamicAttributes::GetIntegerAttributeDefault(FGameplayTag AttributeTag) const +{ + return IntegerAttributeMap.Contains(AttributeTag) ? IntegerAttributeMap[AttributeTag] : 0; +} + +#if WITH_EDITOR +void UGIS_ItemFragment_DynamicAttributes::PreSave(FObjectPreSaveContext SaveContext) +{ + FloatAttributeMap.Empty(); + for (const FGIS_GameplayTagFloat& Attribute : InitialFloatAttributes) + { + if (FloatAttributeMap.Contains(Attribute.Tag)) + { + FloatAttributeMap.Add(Attribute.Tag, Attribute.Value); + } + } + + IntegerAttributeMap.Empty(); + for (const FGIS_GameplayTagInteger& Attribute : InitialIntegerAttributes) + { + if (IntegerAttributeMap.Contains(Attribute.Tag)) + { + IntegerAttributeMap.Add(Attribute.Tag, Attribute.Value); + } + } + + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Equippable.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Equippable.cpp new file mode 100644 index 0000000..929ff61 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Equippable.cpp @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemFragment_Equippable.h" + +#include "GIS_EquipmentInstance.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_Equippable) + + +UGIS_ItemFragment_Equippable::UGIS_ItemFragment_Equippable() +{ + InstanceType = UGIS_EquipmentInstance::StaticClass(); +} + +#if WITH_EDITOR +void UGIS_ItemFragment_Equippable::PreSave(FObjectPreSaveContext SaveContext) +{ + if (InstanceType.IsNull()) + { + bActorBased = false; + } + else + { + UClass* InstanceClass = InstanceType.LoadSynchronous(); + if (InstanceClass != nullptr) + { + bActorBased = InstanceClass->IsChildOf(AActor::StaticClass()); + } + } + Super::PreSave(SaveContext); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Shoppable.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Shoppable.cpp new file mode 100644 index 0000000..0a3d8e6 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Fragments/GIS_ItemFragment_Shoppable.cpp @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemFragment_Shoppable.h" +#include "GIS_InventoryTags.h" +#include "Items/GIS_ItemInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_Shoppable) + +void UGIS_ItemFragment_Shoppable::OnInstanceCreated(UGIS_ItemInstance* Instance) const +{ +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_CoreStructLibray.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_CoreStructLibray.cpp new file mode 100644 index 0000000..1f2d2ff --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_CoreStructLibray.cpp @@ -0,0 +1,42 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_CoreStructLibray.h" + +#include "Items/GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CoreStructLibray) + + +FGIS_ItemDefinitionAmount::FGIS_ItemDefinitionAmount() +{ + Definition = nullptr; + Amount = 1; +} + +FGIS_ItemDefinitionAmount::FGIS_ItemDefinitionAmount(TSoftObjectPtr InDefinition, int32 InAmount) +{ + Definition = InDefinition; + Amount = InAmount; +} + +bool FGIS_ItemSlotDefinition::MatchItem(const UGIS_ItemInstance* Item) const +{ + if (!IsValid(Item)) + { + return false; + } + + if (TagQuery.IsEmpty()) + { + return false; + } + + if (TagQuery.Matches(Item->GetDefinition()->ItemTags)) + { + return true; + } + + return false; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinContainer.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinContainer.cpp new file mode 100644 index 0000000..02207db --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinContainer.cpp @@ -0,0 +1,385 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_MixinContainer.h" +#include "Engine/World.h" +#include "AssetRegistry/AssetData.h" +#include "GIS_ItemFragment.h" +#include "GIS_LogChannels.h" +#include "GIS_MixinOwnerInterface.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Items/GIS_ItemInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinContainer) + +uint32 GetTypeHash(const FGIS_Mixin& Entry) +{ + return HashCombine(GetTypeHash(Entry.Target), Entry.Timestamp); +} + +int32 FGIS_MixinContainer::IndexOfTarget(const UObject* Target) const +{ + if (!IsValid(Target)) + { + return INDEX_NONE; + } + return AcceleratedMap.Contains(Target->GetClass()) ? AcceleratedMap[Target->GetClass()] : INDEX_NONE; +} + +int32 FGIS_MixinContainer::IndexOfTargetByClass(const TSubclassOf& TargetClass) const +{ + check(IsValid(TargetClass)); + + const int32* Idx = AcceleratedMap.Find(TargetClass); + if (Idx != nullptr) + { + return *Idx; + } + + return INDEX_NONE; +} + +// bool FGIS_MixinContainer::GetDataByTargetClass(const TSubclassOf& TargetClass, FInstancedStruct& OutData) const +// { +// if (AcceleratedMap.Contains(TargetClass) && Mixins.IsValidIndex(AcceleratedMap[TargetClass])) +// { +// OutData = Mixins[AcceleratedMap[TargetClass]].Data; +// return true; +// } +// return false; +// } + +bool FGIS_MixinRecord::operator==(const FGIS_MixinRecord& Other) const +{ + return TargetPath == Other.TargetPath && Data.GetScriptStruct() == Other.Data.GetScriptStruct(); +} + +bool FGIS_MixinRecord::IsValid() const +{ + return !TargetPath.IsEmpty() && Data.IsValid(); +} + +bool FGIS_MixinContainer::GetDataByTarget(const UObject* Target, FInstancedStruct& OutData) const +{ + if (AcceleratedMap.Contains(Target) && Mixins.IsValidIndex(AcceleratedMap[Target])) + { + OutData = Mixins[AcceleratedMap[Target]].Data; + return true; + } + return false; +} + +int32 FGIS_MixinContainer::SetDataForTarget(const TObjectPtr& Target, const FInstancedStruct& Data) +{ + if (Target == nullptr || !Data.IsValid()) + { + return INDEX_NONE; + } + + if (!IsObjectLoadedFromDisk(Target)) + { + return INDEX_NONE; + } + + if (AcceleratedMap.Contains(Target)) + { + return UpdateDataAt(AcceleratedMap[Target], Data); + } + + // is not valid class -> data pair. + if (!CheckCompatibility(Target, Data)) + { + return INDEX_NONE; + } + + int32 Idx = Mixins.AddDefaulted(); + FGIS_Mixin& NewMixin = Mixins[Idx]; + NewMixin.Target = Target; + NewMixin.Data = Data; + NewMixin.Timestamp = OwningObject->GetWorld()->GetTimeSeconds(); + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataAdded(NewMixin.Target, NewMixin.Data); + } + MarkItemDirty(NewMixin); + CacheMixins(); + + return Idx; +} + +bool FGIS_MixinContainer::IsObjectLoadedFromDisk(const UObject* Object) const +{ + if (!IsValid(Object)) + { + return false; + } + + // 获取资源路径 + FSoftObjectPath AssetPath(Object); + if (AssetPath.IsNull()) + { + return false; + } + + // 检查资产注册表 + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + FAssetData AssetData = AssetRegistry.GetAssetByObjectPath(AssetPath); + return AssetData.IsValid(); +} + +int32 FGIS_MixinContainer::UpdateDataByTargetClass(const TSubclassOf& TargetClass, const FInstancedStruct& Data) +{ + if (AcceleratedMap.Contains(TargetClass)) + { + return UpdateDataAt(AcceleratedMap[TargetClass], Data); + } + return INDEX_NONE; +} + +int32 FGIS_MixinContainer::UpdateDataAt(const int32 Idx, const FInstancedStruct& Data) +{ + if (Idx == INDEX_NONE || !Mixins.IsValidIndex(Idx)) + { + return INDEX_NONE; + } + + FGIS_Mixin& Entry = Mixins[Idx]; + + if (!CheckCompatibility(Entry.Target, Data)) + { + return INDEX_NONE; + } + + Entry.Data = Data; + Entry.Timestamp = OwningObject->GetWorld()->GetTimeSeconds(); + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataUpdated(Entry.Target, Entry.Data); + } + MarkItemDirty(Entry); + CacheMixins(); + + return Idx; +} + +void FGIS_MixinContainer::RemoveDataByTargetClass(const TSubclassOf& TargetClass) +{ + const int32 Idx = AcceleratedMap.Contains(TargetClass) ? AcceleratedMap[TargetClass] : INDEX_NONE; + + if (Idx != INDEX_NONE) + { + const FGIS_Mixin& Entry = Mixins[Idx]; + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataRemoved(Entry.Target, Entry.Data); + } + Mixins.RemoveAt(Idx); + MarkArrayDirty(); + } + + CacheMixins(); +} + +bool FGIS_MixinContainer::CheckCompatibility(const UObject* Target, const FInstancedStruct& Data) const +{ + if (!IsValid(Target) || !Data.IsValid()) + { + return false; + } + if (const IGIS_MixinTargetInterface* MixinTarget = Cast(Target)) + { + return MixinTarget->GetCompatibleMixinDataType() == Data.GetScriptStruct(); + } + return false; +} + +TArray FGIS_MixinContainer::GetAllData() const +{ + TArray AllData; + AllData.Reserve(Mixins.Num()); + + for (const FGIS_Mixin& Mixin : Mixins) + { + AllData.Add(Mixin.Data); + } + + return AllData; +} + +TArray FGIS_MixinContainer::GetSerializableMixins() const +{ + return Mixins.FilterByPredicate([this](const FGIS_Mixin& Mixin) + { + if (const IGIS_MixinTargetInterface* MixinTarget = Cast(Mixin.Target)) + { + return MixinTarget->IsMixinDataSerializable() && Mixin.Data.IsValid() && MixinTarget->GetCompatibleMixinDataType() == Mixin.Data.GetScriptStruct() && IsObjectLoadedFromDisk(Mixin.Target); + } + return false; + }); +} + +TArray FGIS_MixinContainer::GetSerializableMixinRecords() const +{ + TArray Records; + TArray FilteredMixins = GetSerializableMixins(); + + for (const FGIS_Mixin& FilteredMixin : FilteredMixins) + { + FGIS_MixinRecord Record; + const FSoftObjectPath AssetPath = FSoftObjectPath(FilteredMixin.Target); + Record.TargetPath = AssetPath.ToString(); + Record.Data = FilteredMixin.Data; + Records.Add(Record); + } + + return Records; +} + +void FGIS_MixinContainer::RestoreFromRecords(const TArray& Records) +{ + TArray ConvertedMixins = ConvertRecordsToMixins(Records); + for (const FGIS_Mixin& ConvertedMixin : ConvertedMixins) + { + SetDataForTarget(ConvertedMixin.Target, ConvertedMixin.Data); + } +} + +TArray FGIS_MixinContainer::ConvertRecordsToMixins(const TArray& Records) +{ + TArray Ret; + for (const FGIS_MixinRecord& Record : Records) + { + if (!Record.IsValid()) + { + continue; + } + const FSoftObjectPath TargetPath = FSoftObjectPath(Record.TargetPath); + const TSoftObjectPtr TargetObjectSoftPtr = TSoftObjectPtr(TargetPath); + const TObjectPtr TargetObject = !TargetObjectSoftPtr.IsNull() ? TargetObjectSoftPtr.LoadSynchronous() : nullptr; + if (!IsValid(TargetObject)) + { + continue; + } + const IGIS_MixinTargetInterface* TargetInterface = Cast(TargetObject); + if (TargetInterface == nullptr) + { + continue; + } + if (!TargetInterface->IsMixinDataSerializable()) + { + GIS_LOG(Warning, "Skip restoring mixin's data, as target(%s,class:%s) existed in record no longer considered serializable!", + *GetNameSafe(TargetObject), *GetNameSafe(TargetObject->GetClass())); + continue; + } + + if (TargetInterface->GetCompatibleMixinDataType() != Record.Data.GetScriptStruct()) + { + GIS_LOG(Warning, + "Skip restoring mixin's data, as target(%s,class:%s)'s data type(%s) in record no longer compatible with the new type(%s).", + *GetNameSafe(TargetObject), *GetNameSafe(TargetObject->GetClass()), + *GetNameSafe(Record.Data.GetScriptStruct()), *GetNameSafe(TargetInterface->GetCompatibleMixinDataType())); + continue; + } + FGIS_Mixin Mixin; + Mixin.Target = TargetObject; + Mixin.Data = Record.Data; + Ret.Add(Mixin); + } + return Ret; +} + +TArray FGIS_MixinContainer::GetAllSerializableData() const +{ + TArray AllData; + AllData.Reserve(Mixins.Num()); + + for (const FGIS_Mixin& Mixin : Mixins) + { + if (const IGIS_MixinTargetInterface* MixinTarget = Cast(Mixin.Target)) + { + if (MixinTarget->IsMixinDataSerializable()) + { + AllData.Add(Mixin.Data); + } + } + } + + return AllData; +} + +void FGIS_MixinContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (const int32 Index : AddedIndices) + { + FGIS_Mixin& Mixin = Mixins[Index]; + if (Mixin.Timestamp != Mixin.LastReplicatedTimestamp) + { + Mixin.LastReplicatedTimestamp = Mixin.Timestamp; + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataAdded(Mixin.Target, Mixin.Data); + } + } + } + + CacheMixins(); +} + +void FGIS_MixinContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (const int32 Index : ChangedIndices) + { + FGIS_Mixin& Mixin = Mixins[Index]; + if (Mixin.Timestamp != Mixin.LastReplicatedTimestamp) + { + Mixin.LastReplicatedTimestamp = Mixin.Timestamp; + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataUpdated(Mixin.Target, Mixin.Data); + } + } + } + + CacheMixins(); +} + +void FGIS_MixinContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (const int32 Index : RemovedIndices) + { + const FGIS_Mixin& Mixin = Mixins[Index]; + if (IGIS_MixinOwnerInterface* MixinOwner = Cast(OwningObject)) + { + MixinOwner->OnMixinDataRemoved(Mixin.Target, Mixin.Data); + } + } + + CacheMixins(); +} + +void FGIS_MixinContainer::CacheMixins() +{ + const uint32 MixinsHash = GetTypeHash(Mixins); + if (MixinsHash == LastCachedHash) + { + // Same hash, no need to cache things again. + return; + } + + const int32 Size = Mixins.Num(); + AcceleratedMap.Empty(Size); + + for (int Idx = 0; Idx < Size; ++Idx) + { + const UObject* Target = Mixins[Idx].Target; + AcceleratedMap.Add(Target, Idx); + } + + LastCachedHash = GetTypeHash(Mixins); +} + +bool FGIS_MixinContainer::NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams) +{ + return FastArrayDeltaSerialize(Mixins, DeltaParams, *this); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinOwnerInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinOwnerInterface.cpp new file mode 100644 index 0000000..97484dc --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinOwnerInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_MixinOwnerInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinOwnerInterface) +// Add default functionality here for any IGIS_MixinOwnerInterface functions that are not pure virtual. diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinTargetInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinTargetInterface.cpp new file mode 100644 index 0000000..bbfd03b --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/GIS_MixinTargetInterface.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_MixinTargetInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinTargetInterface) +// Add default functionality here for any IGIS_MixinTargetInterface functions that are not pure virtual. diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinition.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinition.cpp new file mode 100644 index 0000000..ba629c9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinition.cpp @@ -0,0 +1,139 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Items/GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" +#include "Engine/Texture2D.h" +#include "Items/GIS_ItemDefinitionSchema.h" +#include "Fragments/GIS_ItemFragment.h" +#include "Misc/DataValidation.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDefinition) + +UGIS_ItemDefinition::UGIS_ItemDefinition(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + bUnique = false; +} + +const UGIS_ItemFragment* UGIS_ItemDefinition::GetFragment(TSubclassOf FragmentClass) const +{ + if (FragmentClass != nullptr) + { + for (UGIS_ItemFragment* Fragment : Fragments) + { + if (Fragment && Fragment->IsA(FragmentClass)) + { + return Fragment; + } + } + } + + return nullptr; +} + +bool UGIS_ItemDefinition::HasFloatAttribute(FGameplayTag AttributeTag) const +{ + return FloatAttributeMap.Contains(AttributeTag); +} + +bool UGIS_ItemDefinition::HasIntegerAttribute(FGameplayTag AttributeTag) const +{ + return IntegerAttributeMap.Contains(AttributeTag); +} + +float UGIS_ItemDefinition::GetFloatAttribute(FGameplayTag AttributeTag) const +{ + return FloatAttributeMap.Contains(AttributeTag) ? FloatAttributeMap[AttributeTag] : 0; +} + +int32 UGIS_ItemDefinition::GetIntegerAttribute(FGameplayTag AttributeTag) const +{ + return IntegerAttributeMap.Contains(AttributeTag) ? IntegerAttributeMap[AttributeTag] : 0; +} + +const UGIS_ItemFragment* UGIS_ItemDefinition::FindFragmentOfItemDefinition(TSoftObjectPtr ItemDefinition, TSubclassOf FragmentClass) +{ + if (!ItemDefinition.IsNull()) + { + const UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous(); + if (LoadedDefinition != nullptr) + { + return LoadedDefinition->GetFragment(FragmentClass); + } + } + return nullptr; +} + +#if WITH_EDITOR +void UGIS_ItemDefinition::PreSave(FObjectPreSaveContext SaveContext) +{ + FloatAttributeMap.Empty(); + for (const FGIS_GameplayTagFloat& Attribute : StaticFloatAttributes) + { + FloatAttributeMap.Add(Attribute.Tag, Attribute.Value); + } + + IntegerAttributeMap.Empty(); + for (const FGIS_GameplayTagInteger& Attribute : StaticIntegerAttributes) + { + IntegerAttributeMap.Add(Attribute.Tag, Attribute.Value); + } + + FText SchemaError; + UGIS_ItemDefinitionSchema::TryPreSaveItemDefinition(this, SchemaError); + if (!SchemaError.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("ItemDefinition PreSave validation warning for %s: %s"), *GetPathName(), *SchemaError.ToString()); + } + + Super::PreSave(SaveContext); +} + +EDataValidationResult UGIS_ItemDefinition::IsDataValid(class FDataValidationContext& Context) const +{ + if (ItemTags.IsEmpty()) + { + Context.AddWarning(FText::FromString(FString::Format(TEXT("Item tags should not be empty for %s, or it can't be queried by external system."), {GetPathName()}))); + } + + // Check for each fragment's data validation. + for (int32 i = 0; i < Fragments.Num(); i++) + { + const UGIS_ItemFragment* Fragment = Fragments[i]; + if (Fragment != nullptr) + { + FText Message; + if (!Fragment->FragmentDataValidation(Message)) + { + Context.AddWarning(FText::FromString(FString::Format(TEXT("DataValidation failed for fragment at index:{0},message:{1}"), {i, Message.ToString()}))); + return EDataValidationResult::Invalid; + } + } + } + + // Check for duplicate fragment types + TSet> FragmentClassSet; + for (const UGIS_ItemFragment* Fragment : Fragments) + { + if (Fragment != nullptr) + { + TSubclassOf FragmentClass = Fragment->GetClass(); + if (FragmentClassSet.Contains(FragmentClass)) + { + Context.AddError(FText::FromString(FString::Format(TEXT("Duplicate fragment type {0} found in Fragments array for %s."), {FragmentClass->GetName(), GetPathName()}))); + return EDataValidationResult::Invalid; + } + FragmentClassSet.Add(FragmentClass); + } + } + + FText SchemaError; + if (!UGIS_ItemDefinitionSchema::TryValidateItemDefinition(this, SchemaError)) + { + Context.AddError(SchemaError); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinitionSchema.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinitionSchema.cpp new file mode 100644 index 0000000..6f9f0b3 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemDefinitionSchema.cpp @@ -0,0 +1,576 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Items/GIS_ItemDefinitionSchema.h" +#include "GIS_InventorySystemSettings.h" +#include "GIS_LogChannels.h" +#include "Items/GIS_ItemDefinition.h" +#include "Fragments/GIS_ItemFragment.h" +#include "Misc/DataValidation.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDefinitionSchema) + +bool UGIS_ItemDefinitionSchema::TryValidateItemDefinition(const UGIS_ItemDefinition* Definition, FText& OutError) +{ + if (Definition == nullptr) + { + OutError = FText::FromString(TEXT("Item definition is null.")); + return false; + } + + if (const UGIS_InventorySystemSettings* Settings = UGIS_InventorySystemSettings::Get()) + { + FString AssetPath = Definition->GetPathName(); + const UGIS_ItemDefinitionSchema* Schema = Settings->GetItemDefinitionSchemaForAsset(AssetPath); + if (Schema) + { + return Schema->TryValidate(Definition, OutError); + } + // OutError = FText::FromString(FString::Format(TEXT("No valid schema found for item definition at path: {0}."), {AssetPath})); + } + return true; +} + +void UGIS_ItemDefinitionSchema::TryPreSaveItemDefinition(UGIS_ItemDefinition* Definition, FText& OutError) +{ + if (Definition == nullptr) + { + OutError = FText::FromString(TEXT("Item definition is null.")); + return; + } + + if (const UGIS_InventorySystemSettings* Settings = UGIS_InventorySystemSettings::Get()) + { + FString AssetPath = Definition->GetPathName(); + const UGIS_ItemDefinitionSchema* Schema = Settings->GetItemDefinitionSchemaForAsset(AssetPath); + if (Schema) + { + Schema->TryPreSave(Definition, OutError); + } + // OutError = FText::FromString(FString::Format(TEXT("No valid schema found for item definition at path: {0}."), {AssetPath})); + } +} + +bool UGIS_ItemDefinitionSchema::TryValidate(const UGIS_ItemDefinition* Definition, FText& OutError) const +{ + if (Definition == nullptr) + { + OutError = FText::FromString(TEXT("Item definition is null.")); + return false; + } + + // Validate parent tag: all ItemTags must be children of RequiredParentTag + if (RequiredParentTag.IsValid()) + { + for (const FGameplayTag& Tag : Definition->ItemTags.GetGameplayTagArray()) + { + if (!Tag.MatchesTag(RequiredParentTag)) + { + OutError = FText::FromString(FString::Format(TEXT("Tag {0} is not a child of required parent tag {1}."), {Tag.ToString(), RequiredParentTag.ToString()})); + return false; + } + } + } + + FGIS_ItemDefinitionValidationEntry FoundEntry; + bool bFoundEntry = false; + int32 ValidationEntryIndex = -1; + + // Find matching validation entry + for (int32 Index = 0; Index < ValidationEntries.Num(); ++Index) + { + const FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[Index]; + if (!Entry.ItemTagQuery.IsEmpty() && Definition->ItemTags.MatchesQuery(Entry.ItemTagQuery)) + { + FoundEntry = Entry; + bFoundEntry = true; + ValidationEntryIndex = Index; + break; + } + } + + if (!bFoundEntry) + { + return true; // No matching entry + } + + TMap, int32> FragmentClassMap; + TArray> FragmentClasses; + + // Build fragment class map and check for duplicate fragment types + for (const UGIS_ItemFragment* Fragment : Definition->Fragments) + { + if (Fragment != nullptr) + { + TSubclassOf FragmentClass = Fragment->GetClass(); + int32& Count = FragmentClassMap.FindOrAdd(FragmentClass); + Count++; + if (Count > 1) + { + OutError = FText::FromString(FString::Format(TEXT("Duplicate fragment type {0} found in Fragments array for schema {1}."), {FragmentClass->GetName(), GetPathName()})); + return false; + } + } + } + + FragmentClassMap.GetKeys(FragmentClasses); + + // Validate common required fragments + for (const TSoftClassPtr& CommonFragment : CommonRequiredFragments) + { + if (UClass* LoadedClass = CommonFragment.LoadSynchronous()) + { + if (!FragmentClasses.Contains(LoadedClass)) + { + OutError = FText::FromString( + FString::Format(TEXT("Missing common required fragment of type {0} for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()})); + return false; + } + } + } + + // Validate forbidden fragments, excluding common required fragments and required fragments + TSet> EffectiveRequiredFragments; + for (const TSoftClassPtr& RequiredFragment : FoundEntry.RequiredFragments) + { + if (!CommonRequiredFragments.Contains(RequiredFragment)) + { + EffectiveRequiredFragments.Add(RequiredFragment); + } + } + for (const TSoftClassPtr& ForbiddenFragment : FoundEntry.ForbiddenFragments) + { + if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous()) + { + if (CommonRequiredFragments.Contains(ForbiddenFragment)) + { + OutError = FText::FromString(FString::Format( + TEXT("Forbidden fragment {0} in ValidationEntry {1} is a common required fragment in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()})); + return false; + } + if (EffectiveRequiredFragments.Contains(ForbiddenFragment)) + { + OutError = FText::FromString( + FString::Format(TEXT("Forbidden fragment {0} in ValidationEntry {1} is a required fragment in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()})); + return false; + } + if (FragmentClasses.Contains(LoadedClass)) + { + OutError = FText::FromString(FString::Format( + TEXT("Forbidden fragment of type {0} found in definition for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()})); + return false; + } + } + } + + // Validate required fragments, excluding those already in CommonRequiredFragments + for (const TSoftClassPtr& RequiredFragment : EffectiveRequiredFragments) + { + if (UClass* LoadedClass = RequiredFragment.LoadSynchronous()) + { + if (!FragmentClasses.Contains(LoadedClass)) + { + OutError = FText::FromString( + FString::Format(TEXT("Missing required fragment of type {0} for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()})); + return false; + } + } + } + + // Validate required float attributes + for (const FGIS_GameplayTagFloat& RequiredFloat : FoundEntry.RequiredFloatAttributes) + { + bool bFound = false; + for (const FGIS_GameplayTagFloat& FloatAttr : Definition->StaticFloatAttributes) + { + if (FloatAttr.Tag == RequiredFloat.Tag) + { + bFound = true; + break; + } + } + if (!bFound) + { + OutError = FText::FromString( + FString::Format(TEXT("Missing required float attribute {0} for ValidationEntry {1} in schema {2}."), {RequiredFloat.Tag.ToString(), ValidationEntryIndex, GetPathName()})); + return false; + } + } + + // Validate required integer attributes + for (const FGIS_GameplayTagInteger& RequiredInteger : FoundEntry.RequiredIntegerAttributes) + { + bool bFound = false; + for (const FGIS_GameplayTagInteger& IntegerAttr : Definition->StaticIntegerAttributes) + { + if (IntegerAttr.Tag == RequiredInteger.Tag) + { + bFound = true; + break; + } + } + if (!bFound) + { + OutError = FText::FromString( + FString::Format(TEXT("Missing required integer attribute {0} for ValidationEntry {1} in schema {2}."), {RequiredInteger.Tag.ToString(), ValidationEntryIndex, GetPathName()})); + return false; + } + } + + // Validate bUnique + if (FoundEntry.bEnforceUnique && Definition->bUnique != FoundEntry.RequiredUniqueValue) + { + OutError = FText::FromString(FString::Format( + TEXT("bUnique must be {0} for ValidationEntry {1} in schema {2}."), {FoundEntry.RequiredUniqueValue ? TEXT("true") : TEXT("false"), ValidationEntryIndex, GetPathName()})); + return false; + } + + return true; +} + +void UGIS_ItemDefinitionSchema::TryPreSave(UGIS_ItemDefinition* Definition, FText& OutError) const +{ + if (Definition == nullptr) + { + OutError = FText::FromString(TEXT("Item definition is null.")); + return; + } + + FGIS_ItemDefinitionValidationEntry FoundEntry; + bool bFoundEntry = false; + + // Find matching validation entry + for (const FGIS_ItemDefinitionValidationEntry& Entry : ValidationEntries) + { + if (!Entry.ItemTagQuery.IsEmpty() && Definition->ItemTags.MatchesQuery(Entry.ItemTagQuery)) + { + FoundEntry = Entry; + bFoundEntry = true; + break; + } + } + + // Collect required fragments + TSet> RequiredFragmentSet; + for (const TSoftClassPtr& CommonFragment : CommonRequiredFragments) + { + RequiredFragmentSet.Add(CommonFragment); + } + if (bFoundEntry) + { + for (const TSoftClassPtr& RequiredFragment : FoundEntry.RequiredFragments) + { + if (!CommonRequiredFragments.Contains(RequiredFragment)) + { + RequiredFragmentSet.Add(RequiredFragment); + } + } + } + + // Collect forbidden fragments + TSet> ForbiddenFragmentSet; + if (bFoundEntry) + { + for (const TSoftClassPtr& ForbiddenFragment : FoundEntry.ForbiddenFragments) + { + if (!CommonRequiredFragments.Contains(ForbiddenFragment) && !RequiredFragmentSet.Contains(ForbiddenFragment)) + { + ForbiddenFragmentSet.Add(ForbiddenFragment); + } + } + } + + // Build map of existing fragments, keeping only the first instance of each type + TMap, TObjectPtr> ExistingFragmentMap; + for (TObjectPtr Fragment : Definition->Fragments) + { + if (Fragment != nullptr) + { + TSubclassOf FragmentClass = Fragment->GetClass(); + if (!ExistingFragmentMap.Contains(FragmentClass)) + { + ExistingFragmentMap.Add(FragmentClass, Fragment); + } + else + { + UE_LOG(LogGIS, Warning, TEXT("Removed duplicate fragment of type %s from ItemDefinition(%s) by schema(%s)."), *FragmentClass->GetName(), *GetNameSafe(Definition), *GetPathName()); + } + } + } + + // Remove forbidden fragments + for (const TSoftClassPtr& ForbiddenFragment : ForbiddenFragmentSet) + { + if (UClass* ForbiddenClass = ForbiddenFragment.LoadSynchronous()) + { + if (ExistingFragmentMap.Remove(ForbiddenClass)) + { + UE_LOG(LogGIS, Warning, TEXT("Removed forbidden fragment of type %s from ItemDefinition(%s) by schema(%s)."), *ForbiddenClass->GetName(), *GetNameSafe(Definition), *GetPathName()); + } + } + } + + // Add missing required fragments + for (const TSoftClassPtr& RequiredFragment : RequiredFragmentSet) + { + if (UClass* FragmentClass = RequiredFragment.LoadSynchronous()) + { + if (!ExistingFragmentMap.Contains(FragmentClass)) + { + UGIS_ItemFragment* NewFragment = NewObject(Definition, FragmentClass); + if (NewFragment) + { + ExistingFragmentMap.Add(FragmentClass, NewFragment); + UE_LOG(LogGIS, Warning, TEXT("Added missing required fragment of type %s to ItemDefinition(%s) by schema(%s)."), *FragmentClass->GetName(), *GetNameSafe(Definition), + *GetPathName()); + } + } + } + } + + // Sort fragments according to FragmentOrder + TArray> NewFragments; + TArray> NonRequiredFragments; + + // Cache loaded fragment classes to avoid repeated LoadSynchronous calls + TMap, UClass*> FragmentClassCache; + for (const TSoftClassPtr& OrderedFragment : FragmentOrder) + { + UClass* FragmentClass = FragmentClassCache.FindOrAdd(OrderedFragment, OrderedFragment.LoadSynchronous()); + if (FragmentClass) + { + if (TObjectPtr* ExistingFragment = ExistingFragmentMap.Find(FragmentClass)) + { + NewFragments.Add(*ExistingFragment); + ExistingFragmentMap.Remove(FragmentClass); + } + } + } + + // Append remaining non-required fragments + ExistingFragmentMap.GenerateValueArray(NonRequiredFragments); + for (const TObjectPtr& Fragment : NonRequiredFragments) + { + if (Fragment && !FragmentOrder.Contains(Fragment->GetClass())) + { + UE_LOG(LogGIS, Warning, TEXT("Fragment type %s is not included in FragmentOrder, appended to end of fragments with ItemDefinition(%s) by schema(%s)."), *Fragment->GetClass()->GetName(), + *GetNameSafe(Definition), *GetPathName()); + } + } + NewFragments.Append(NonRequiredFragments); + + // Update the definition's fragment array + Definition->Fragments = MoveTemp(NewFragments); + + // Auto-fix required attributes and bUnique if a matching entry was found + if (bFoundEntry) + { + // Auto-fix float attributes + for (const FGIS_GameplayTagFloat& RequiredFloat : FoundEntry.RequiredFloatAttributes) + { + bool bFound = false; + for (FGIS_GameplayTagFloat& FloatAttr : Definition->StaticFloatAttributes) + { + if (FloatAttr.Tag == RequiredFloat.Tag) + { + bFound = true; + break; + } + } + if (!bFound) + { + Definition->StaticFloatAttributes.Add(RequiredFloat); + UE_LOG(LogGIS, Warning, TEXT("Added missing required float attribute %s to ItemDefinition(%s) by schema(%s)."), *RequiredFloat.Tag.ToString(), *GetNameSafe(Definition), + *GetPathName()); + } + } + + // Auto-fix integer attributes + for (const FGIS_GameplayTagInteger& RequiredInteger : FoundEntry.RequiredIntegerAttributes) + { + bool bFound = false; + for (FGIS_GameplayTagInteger& IntegerAttr : Definition->StaticIntegerAttributes) + { + if (IntegerAttr.Tag == RequiredInteger.Tag) + { + bFound = true; + break; + } + } + if (!bFound) + { + Definition->StaticIntegerAttributes.Add(RequiredInteger); + UE_LOG(LogGIS, Warning, TEXT("Added missing required integer attribute %s to ItemDefinition(%s) by schema(%s)."), *RequiredInteger.Tag.ToString(), *GetNameSafe(Definition), + *GetPathName()); + } + } + + // Auto-fix bUnique + if (FoundEntry.bEnforceUnique) + { + if (Definition->bUnique != FoundEntry.RequiredUniqueValue) + { + Definition->bUnique = FoundEntry.RequiredUniqueValue; + UE_LOG(LogGIS, Warning, TEXT("Set bUnique to %s for ItemDefinition(%s) by schema(%s)."), FoundEntry.RequiredUniqueValue ? TEXT("true") : TEXT("false"), *GetNameSafe(Definition), + *GetPathName()); + } + } + } + + // Mark the definition as modified + Definition->Modify(); +} + +#if WITH_EDITOR +void UGIS_ItemDefinitionSchema::PreSave(FObjectPreSaveContext SaveContext) +{ + // Remove RequiredFragments and ForbiddenFragments that overlap with CommonRequiredFragments + for (int32 EntryIndex = 0; EntryIndex < ValidationEntries.Num(); ++EntryIndex) + { + FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[EntryIndex]; + int32 InitialRequiredCount = Entry.RequiredFragments.Num(); + int32 InitialForbiddenCount = Entry.ForbiddenFragments.Num(); + + Entry.RequiredFragments.RemoveAll([&](const TSoftClassPtr& Fragment) + { + if (CommonRequiredFragments.Contains(Fragment)) + { + if (UClass* LoadedClass = Fragment.LoadSynchronous()) + { + UE_LOG(LogGIS, Warning, TEXT("Removed fragment %s from ValidationEntry %d RequiredFragments as it is already in CommonRequiredFragments for schema(%s)."), *LoadedClass->GetName(), + EntryIndex, *GetPathName()); + } + return true; + } + return false; + }); + + Entry.ForbiddenFragments.RemoveAll([&](const TSoftClassPtr& Fragment) + { + if (CommonRequiredFragments.Contains(Fragment)) + { + if (UClass* LoadedClass = Fragment.LoadSynchronous()) + { + UE_LOG(LogGIS, Warning, TEXT("Removed fragment %s from ValidationEntry %d ForbiddenFragments as it is in CommonRequiredFragments for schema(%s)."), *LoadedClass->GetName(), + EntryIndex, *GetPathName()); + } + return true; + } + return false; + }); + + if (Entry.RequiredFragments.Num() < InitialRequiredCount) + { + UE_LOG(LogGIS, Warning, TEXT("Removed %d fragment(s) from ValidationEntry %d RequiredFragments for schema(%s)."), InitialRequiredCount - Entry.RequiredFragments.Num(), EntryIndex, + *GetPathName()); + } + if (Entry.ForbiddenFragments.Num() < InitialForbiddenCount) + { + UE_LOG(LogGIS, Warning, TEXT("Removed %d fragment(s) from ValidationEntry %d ForbiddenFragments for schema(%s)."), InitialForbiddenCount - Entry.ForbiddenFragments.Num(), EntryIndex, + *GetPathName()); + } + } + + // Mark the schema as modified if changes were made + Modify(); + + Super::PreSave(SaveContext); +} + +EDataValidationResult UGIS_ItemDefinitionSchema::IsDataValid(class FDataValidationContext& Context) const +{ + // Validate that RequiredFragments do not overlap with CommonRequiredFragments + for (int32 EntryIndex = 0; EntryIndex < ValidationEntries.Num(); ++EntryIndex) + { + const FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[EntryIndex]; + for (const TSoftClassPtr& RequiredFragment : Entry.RequiredFragments) + { + if (CommonRequiredFragments.Contains(RequiredFragment)) + { + if (UClass* LoadedClass = RequiredFragment.LoadSynchronous()) + { + Context.AddWarning(FText::FromString( + FString::Format( + TEXT("ValidationEntry {0} contains fragment {1} in RequiredFragments, which is already in CommonRequiredFragments for schema {2}."), + {EntryIndex, LoadedClass->GetName(), GetPathName()}))); + } + } + } + + // Validate that ForbiddenFragments do not overlap with CommonRequiredFragments or RequiredFragments + TSet> EffectiveRequiredFragments; + for (const TSoftClassPtr& RequiredFragment : Entry.RequiredFragments) + { + if (!CommonRequiredFragments.Contains(RequiredFragment)) + { + EffectiveRequiredFragments.Add(RequiredFragment); + } + } + for (const TSoftClassPtr& ForbiddenFragment : Entry.ForbiddenFragments) + { + if (CommonRequiredFragments.Contains(ForbiddenFragment)) + { + if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous()) + { + Context.AddError(FText::FromString( + FString::Format( + TEXT("ValidationEntry {0} contains fragment {1} in ForbiddenFragments, which is in CommonRequiredFragments for schema {2}."), + {EntryIndex, LoadedClass->GetName(), GetPathName()}))); + return EDataValidationResult::Invalid; + } + } + if (EffectiveRequiredFragments.Contains(ForbiddenFragment)) + { + if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous()) + { + Context.AddError(FText::FromString( + FString::Format( + TEXT("ValidationEntry {0} contains fragment {1} in ForbiddenFragments, which is in RequiredFragments for schema {2}."), + {EntryIndex, LoadedClass->GetName(), GetPathName()}))); + return EDataValidationResult::Invalid; + } + } + } + } + + // Validate FragmentOrder contains no duplicates + TSet> FragmentOrderSet; + for (const TSoftClassPtr& Fragment : FragmentOrder) + { + if (FragmentOrderSet.Contains(Fragment)) + { + if (UClass* LoadedClass = Fragment.LoadSynchronous()) + { + Context.AddError(FText::FromString(FString::Format(TEXT("FragmentOrder contains duplicate fragment {0} for schema {1}."), {LoadedClass->GetName(), GetPathName()}))); + return EDataValidationResult::Invalid; + } + } + FragmentOrderSet.Add(Fragment); + } + + // Suggest including all CommonRequiredFragments and RequiredFragments in FragmentOrder + TSet> AllRequiredFragments = TSet>(CommonRequiredFragments); + for (const FGIS_ItemDefinitionValidationEntry& Entry : ValidationEntries) + { + for (const TSoftClassPtr& RequiredFragment : Entry.RequiredFragments) + { + if (!CommonRequiredFragments.Contains(RequiredFragment)) + { + AllRequiredFragments.Add(RequiredFragment); + } + } + } + for (const TSoftClassPtr& RequiredFragment : AllRequiredFragments) + { + if (!FragmentOrder.Contains(RequiredFragment)) + { + if (UClass* LoadedClass = RequiredFragment.LoadSynchronous()) + { + Context.AddWarning(FText::FromString(FString::Format(TEXT("Required fragment {0} is not included in FragmentOrder for schema {1}."), {LoadedClass->GetName(), GetPathName()}))); + } + } + } + + return Context.GetNumErrors() > 0 ? EDataValidationResult::Invalid : EDataValidationResult::Valid; +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInfo.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInfo.cpp new file mode 100644 index 0000000..c106340 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInfo.cpp @@ -0,0 +1,120 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Items/GIS_ItemInfo.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInfo) + +FGIS_ItemInfo FGIS_ItemInfo::None = FGIS_ItemInfo(); + +FGIS_ItemInfo::FGIS_ItemInfo() +{ + Item = nullptr; + Amount = 0; + ItemCollection = nullptr; + StackId = FGIS_ItemStack::InvalidId; + CollectionTag = FGameplayTag::EmptyTag; + CollectionId = FGIS_ItemStack::InvalidId; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection) +{ + Item = InItem; + Amount = InAmount; + ItemCollection = InCollection; + StackId = FGIS_ItemStack::InvalidId; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, FGuid InStackId) +{ + Item = InItem; + Amount = InAmount; + ItemCollection = InCollection; + CollectionId = InCollection->GetCollectionId(); + StackId = InStackId; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount) +{ + Item = InItem; + Amount = InAmount; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGuid InCollectionId) +{ + Item = InItem; + Amount = InAmount; + CollectionId = InCollectionId; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGameplayTag InCollectionTag) +{ + Item = InItem; + Amount = InAmount; + CollectionTag = InCollectionTag; +} + +FGIS_ItemInfo::FGIS_ItemInfo(int32 InAmount, const FGIS_ItemInfo& OtherInfo) +{ + Amount = InAmount; + Item = OtherInfo.Item; + ItemCollection = OtherInfo.ItemCollection; + StackId = OtherInfo.StackId; + CollectionId = OtherInfo.CollectionId; + CollectionTag = OtherInfo.CollectionTag; +} + +FGIS_ItemInfo::FGIS_ItemInfo(int32 InAmount, int32 InIndex, const FGIS_ItemInfo& OtherInfo) +{ + Amount = InAmount; + Index = InIndex; + Item = OtherInfo.Item; + ItemCollection = OtherInfo.ItemCollection; + StackId = OtherInfo.StackId; + CollectionId = OtherInfo.CollectionId; + CollectionTag = OtherInfo.CollectionTag; +} + +FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, const FGIS_ItemInfo& OtherInfo) +{ + Item = InItem; + Amount = InAmount; + ItemCollection = OtherInfo.ItemCollection; + StackId = OtherInfo.StackId; + CollectionId = OtherInfo.CollectionId; +} + +FGIS_ItemInfo::FGIS_ItemInfo(const FGIS_ItemStack& ItemStack) +{ + Item = ItemStack.Item; + ItemCollection = ItemStack.Collection; + Amount = ItemStack.Amount; + StackId = ItemStack.Id; +} + +FString FGIS_ItemInfo::GetDebugString() const +{ + if (!IsValid()) + { + return TEXT("Invalid item info"); + } + + return FString::Format(TEXT("{0}({1})"), {Item->GetDefinition()->GetName(), Amount}); +} + +bool FGIS_ItemInfo::operator==(const FGIS_ItemInfo& Other) const +{ + return Other.Amount == Amount && Other.Item == Item && Other.ItemCollection == ItemCollection; +} + +bool FGIS_ItemInfo::IsValid() const +{ + return Item != nullptr && Amount > 0; +} + +UGIS_InventorySystemComponent* FGIS_ItemInfo::GetInventory() const +{ + return ItemCollection != nullptr ? ItemCollection->GetOwningInventory() : nullptr; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInstance.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInstance.cpp new file mode 100644 index 0000000..6c5cb80 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInstance.cpp @@ -0,0 +1,422 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Items/GIS_ItemInstance.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" +#include "GIS_ItemFragment.h" +#include "Net/UnrealNetwork.h" +#include "Engine/BlueprintGeneratedClass.h" + +#if UE_WITH_IRIS +#include "Iris/ReplicationSystem/ReplicationFragmentUtil.h" +#endif // UE_WITH_IRIS + +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInstance) + + +UGIS_ItemInstance::UGIS_ItemInstance(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), IntegerAttributes(this), FloatAttributes(this), FragmentStates(this) +{ + OwningCollection = nullptr; +} + +void UGIS_ItemInstance::GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const +{ + TagContainer = GetItemTags(); +} + +void UGIS_ItemInstance::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + + // FastArray don't need push model. + DOREPLIFETIME(ThisClass, IntegerAttributes); + DOREPLIFETIME(ThisClass, FloatAttributes); + DOREPLIFETIME(ThisClass, FragmentStates); + + FDoRepLifetimeParams SharedParams; + SharedParams.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ItemId, SharedParams); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, Definition, SharedParams); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, OwningCollection, SharedParams); + + SharedParams.Condition = COND_InitialOrOwner; + + //fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4 + UBlueprintGeneratedClass* bpClass = Cast(this->GetClass()); + if (bpClass != nullptr) + { + bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps); + } +} + +FGuid UGIS_ItemInstance::GetItemId() const +{ + return ItemId; +} + +void UGIS_ItemInstance::SetItemId(FGuid NewId) +{ + ItemId = NewId; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ItemId, this); +} + +bool UGIS_ItemInstance::IsUnique() const +{ + return Definition->bUnique; +} + +FText UGIS_ItemInstance::GetItemName() const +{ + return Definition->DisplayName; +} + +const UGIS_ItemDefinition* UGIS_ItemInstance::GetDefinition() const +{ + return Definition; +} + +void UGIS_ItemInstance::SetDefinition(const UGIS_ItemDefinition* NewDefinition) +{ + Definition = NewDefinition; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, Definition, this); +} + +FGameplayTagContainer UGIS_ItemInstance::GetItemTags() const +{ + return Definition->ItemTags; +} + +const UGIS_ItemFragment* UGIS_ItemInstance::GetFragment(TSubclassOf FragmentClass) const +{ + if (Definition != nullptr && FragmentClass != nullptr) + { + return Definition->GetFragment(FragmentClass); + } + return nullptr; +} + +const UGIS_ItemFragment* UGIS_ItemInstance::FindFragment(TSubclassOf FragmentClass, bool& bValid) const +{ + bValid = false; + if (const UGIS_ItemFragment* Fragment = GetFragment(FragmentClass)) + { + bValid = true; + return Fragment; + } + return nullptr; +} + +bool UGIS_ItemInstance::HasAnyAttribute(FGameplayTag AttributeTag) const +{ + return HasFloatAttribute(AttributeTag) || HasIntegerAttribute(AttributeTag); // || HasBoolAttribute(AttributeTag); +} + +bool UGIS_ItemInstance::HasFloatAttribute(FGameplayTag AttributeTag) const +{ + if (FloatAttributes.ContainsTag(AttributeTag)) + { + return true; + } + return false; +} + +FText UGIS_ItemInstance::GetItemDescription() const +{ + return Definition->Description; +} + +float UGIS_ItemInstance::GetFloatAttribute(FGameplayTag AttributeTag) const +{ + if (FloatAttributes.ContainsTag(AttributeTag)) + { + return FloatAttributes.GetValue(AttributeTag); + } + return 0; +} + +int32 UGIS_ItemInstance::GetIntegerAttribute(FGameplayTag AttributeTag) const +{ + if (IntegerAttributes.ContainsTag(AttributeTag)) + { + return IntegerAttributes.GetValue(AttributeTag); + } + return 0; +} + +void UGIS_ItemInstance::SetFloatAttribute(FGameplayTag AttributeTag, float NewValue) +{ + FloatAttributes.SetItem(AttributeTag, NewValue); +} + +void UGIS_ItemInstance::AddFloatAttribute(FGameplayTag AttributeTag, float Value) +{ + FloatAttributes.AddItem(AttributeTag, Value); +} + +void UGIS_ItemInstance::RemoveFloatAttribute(FGameplayTag AttributeTag, float Value) +{ + FloatAttributes.RemoveItem(AttributeTag, Value); +} + +bool UGIS_ItemInstance::HasIntegerAttribute(FGameplayTag AttributeTag) const +{ + if (IntegerAttributes.ContainsTag(AttributeTag)) + { + return true; + } + return false; +} + +void UGIS_ItemInstance::SetIntegerAttribute(FGameplayTag AttributeTag, int32 NewValue) +{ + IntegerAttributes.SetItem(AttributeTag, NewValue); +} + +void UGIS_ItemInstance::AddIntegerAttribute(FGameplayTag AttributeTag, int32 NewValue) +{ + IntegerAttributes.AddItem(AttributeTag, NewValue); +} + +void UGIS_ItemInstance::RemoveIntegerAttribute(FGameplayTag AttributeTag, int32 Value) +{ + IntegerAttributes.RemoveItem(AttributeTag, Value); +} + +UGIS_ItemCollection* UGIS_ItemInstance::GetOwningCollection() const +{ + return OwningCollection; +} + +UGIS_InventorySystemComponent* UGIS_ItemInstance::GetOwningInventory() const +{ + return IsValid(OwningCollection) ? OwningCollection->GetOwningInventory() : nullptr; +} + +bool UGIS_ItemInstance::FindFragmentStateByClass(TSubclassOf FragmentClass, FInstancedStruct& OutState) const +{ + return FragmentStates.GetDataByTarget(GetFragment(FragmentClass), OutState); +} + +void UGIS_ItemInstance::SetFragmentStateByClass(TSubclassOf FragmentClass, const FInstancedStruct& NewState) +{ + if (const UGIS_ItemFragment* Fragment = Definition->GetFragment(FragmentClass)) + { + FragmentStates.SetDataForTarget(Fragment, NewState); + } +} + +void UGIS_ItemInstance::AssignCollection(UGIS_ItemCollection* NewItemCollection) +{ + if (NewItemCollection == nullptr || OwningCollection == NewItemCollection) + { + return; + } + + if (OwningCollection != nullptr && OwningCollection->GetOwningInventory() != nullptr && OwningCollection != NewItemCollection) + { + GIS_CLOG(Error, "is unable to be added to a new item collection:%s when it is already a member of an existing item collection.", *OwningCollection->GetDebugString()); + return; + } + OwningCollection = NewItemCollection; + GIS_CLOG(Verbose, "item(%s) assigned new collection(%s)", *GetNameSafe(this), *OwningCollection->GetDebugString()); + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this); +} + +void UGIS_ItemInstance::UnassignCollection(UGIS_ItemCollection* ItemCollection) +{ + if (OwningCollection != nullptr && ItemCollection != nullptr && OwningCollection != ItemCollection) + { + GIS_CLOG(Warning, "belong to %s, but trying to remove from %s.", *OwningCollection->GetDebugString(), *ItemCollection->GetDebugString()); + return; + } + GIS_CLOG(Verbose, "item(%s) removed from collection(%s)", *GetNameSafe(this), *OwningCollection->GetDebugString()); + OwningCollection = nullptr; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this); +} + +// void UGIS_ItemInstance::ResetCollection() +// { +// OwningCollection = nullptr; +// MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this); +// } + +bool UGIS_ItemInstance::IsItemValid() const +{ + return ItemId.IsValid() && Definition != nullptr; +} + +bool UGIS_ItemInstance::StackableEquivalentTo(const UGIS_ItemInstance* OtherItem) const +{ + return AreStackableEquivalent(this, OtherItem); +} + +bool UGIS_ItemInstance::SimilarTo(const UGIS_ItemInstance* OtherItem) const +{ + return AreSimilar(this, OtherItem); +} + +bool UGIS_ItemInstance::AreStackableEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs) +{ + if (Lhs == nullptr || Rhs == nullptr) + { + return false; + } + // 有必要比较同一个指针吗? + if (Lhs == Rhs) + { + return true; + } + if (Lhs->GetClass() != Rhs->GetClass()) + { + return false; + } + if (Lhs->GetDefinition() != nullptr && Rhs->GetDefinition() != nullptr) + { + if (Lhs->GetDefinition() != Rhs->GetDefinition()) + { + return false; + } + } + if (Lhs->IsUnique()) + { + return false; + } + return true; + //return AreValueEquivalent(Lhs, Rhs); +} + +// bool UGIS_ItemInstance::AreValueEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs) +// { +// if (Lhs == nullptr || Rhs == nullptr) +// { +// return false; +// } +// if (Lhs == Rhs) +// { +// return true; +// } +// if (Lhs->GetClass() != Rhs->GetClass()) +// { +// return false; +// } +// // if (Lhs->GetItemTags() != Rhs->GetItemTags()) +// // return false; +// if (Lhs->GetDefinition() != Rhs->GetDefinition()) +// { +// return false; +// } +// // if (!Lhs->GetDefinitionTag().IsValid() || !Rhs->GetDefinitionTag().IsValid()) +// // { +// // return false; +// // } +// return true; +// } + +bool UGIS_ItemInstance::AreSimilar(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs) +{ + if (Lhs == nullptr || Rhs == nullptr) + { + return false; + } + if (Lhs == Rhs) + { + return true; + } + if (Lhs->GetClass() != Rhs->GetClass()) + { + return false; + } + if (Lhs->GetDefinition() != Rhs->GetDefinition()) + { + return false; + } + if (Lhs->IsUnique()) + { + return false; + } + if (Lhs->GetItemId() == Rhs->GetItemId()) + { + return true; + } + return true; +} + +void UGIS_ItemInstance::OnItemDuplicated(const UGIS_ItemInstance* SrcItem) +{ + // FloatAttributes.ContainerOwner = this; + // IntegerAttributes.ContainerOwner = this; +} + +const FGIS_MixinContainer& UGIS_ItemInstance::GetFragmentStates() const +{ + return FragmentStates; +} + +void UGIS_ItemInstance::OnMixinDataAdded(const TObjectPtr& Target, const FInstancedStruct& Data) +{ + const UGIS_ItemFragment* Fragment = CastChecked(Target); + OnFragmentStateAdded(Fragment, Data); + OnFragmentStateAddedEvent.Broadcast(Fragment, Data); +} + +void UGIS_ItemInstance::OnMixinDataUpdated(const TObjectPtr& Target, const FInstancedStruct& Data) +{ + const UGIS_ItemFragment* Fragment = CastChecked(Target); + OnFragmentStateUpdated(Fragment, Data); + OnFragmentStateUpdatedEvent.Broadcast(Fragment, Data); +} + +void UGIS_ItemInstance::OnMixinDataRemoved(const TObjectPtr& Target, const FInstancedStruct& Data) +{ + const UGIS_ItemFragment* Fragment = CastChecked(Target); + OnFragmentStateRemoved(Fragment, Data); + OnFragmentStateRemovedEvent.Broadcast(Fragment, Data); +} + +void UGIS_ItemInstance::OnFragmentStateAdded(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data) +{ +} + +void UGIS_ItemInstance::OnFragmentStateUpdated(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data) +{ +} + +void UGIS_ItemInstance::OnFragmentStateRemoved(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data) +{ +} + +void UGIS_ItemInstance::OnTagFloatUpdate(const FGameplayTag& Tag, float OldValue, float NewValue) +{ + OnFloatAttributeChanged(Tag, OldValue, NewValue); + OnFloatAttributeChangedEvent.Broadcast(Tag, OldValue, NewValue); +} + +void UGIS_ItemInstance::OnTagIntegerUpdate(const FGameplayTag& Tag, int32 OldValue, int32 NewValue) +{ + OnIntegerAttributeChanged(Tag, OldValue, NewValue); + OnIntegerAttributeChangedEvent.Broadcast(Tag, OldValue, NewValue); +} + +void UGIS_ItemInstance::OnFloatAttributeChanged_Implementation(const FGameplayTag& Tag, float OldValue, float NewValue) +{ +} + +void UGIS_ItemInstance::OnIntegerAttributeChanged_Implementation(const FGameplayTag& Tag, int32 OldValue, int32 NewValue) +{ +} + +#if UE_WITH_IRIS +void UGIS_ItemInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) +{ + using namespace UE::Net; + UObject::RegisterReplicationFragments(Context, RegistrationFlags); + // Build descriptors and allocate PropertyReplicationFragments for this object + FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInterface.cpp new file mode 100644 index 0000000..d1fc372 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemInterface.cpp @@ -0,0 +1,7 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// +// #include "Items/GIS_ItemInterface.h" +// +// #include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInterface) +// diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemStack.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemStack.cpp new file mode 100644 index 0000000..536fb60 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Core/Items/GIS_ItemStack.cpp @@ -0,0 +1,195 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Items/GIS_ItemStack.h" +#include "Components/ActorComponent.h" +#include "GIS_InventoryMeesages.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" +#include "GIS_InventorySystemComponent.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_LogChannels.h" + +#include "GIS_ItemSlotCollection.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemStack) + +FGuid FGIS_ItemStack::InvalidId = FGuid(0, 0, 0, 0); + +FGIS_ItemStack::FGIS_ItemStack() +{ + Item = nullptr; + Id = InvalidId; + Amount = 0; + Collection = nullptr; + LastObservedAmount = -1; +} + +FString FGIS_ItemStack::GetDebugString() +{ + return FString::Format(TEXT("Item({0}),Amount({1}),ID({2})"), {Item && Item->GetDefinition() ? GetNameSafe(Item->GetDefinition()) : TEXT("None"), Amount, Id.ToString()}); +} + +void FGIS_ItemStack::Initialize(FGuid InStackId, UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, int32 InIndex) +{ + Id = InStackId; + Item = InItem; + Amount = InAmount; + Collection = InCollection; + Index = InIndex; + LastObservedAmount = InAmount; +} + +bool FGIS_ItemStack::IsValidStack() const +{ + return Id.IsValid() && IsValid(Item) && IsValid(Collection) && Amount > 0; +} + +void FGIS_ItemStack::Reset() +{ + Id.Invalidate(); + Item = nullptr; + Amount = 0; + Collection = nullptr; + LastObservedAmount = INDEX_NONE; +} + +bool FGIS_ItemStack::operator==(const FGIS_ItemStack& Other) const +{ + return Id == Other.Id && Item->GetItemId() == Other.Item->GetItemId(); +} + +bool FGIS_ItemStack::operator!=(const FGIS_ItemStack& Other) const +{ + return !(operator==(Other)); +} + +bool FGIS_ItemStack::operator==(const FGuid& OtherId) const +{ + return Id == OtherId; +} + +bool FGIS_ItemStack::operator!=(const FGuid& OtherId) const +{ + return Id != OtherId; +} + +void FGIS_ItemStackContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + FGIS_ItemStack& Stack = Stacks[Index]; + + if (OwningCollection->StackToIdxMap.Contains(Stack.Id)) + { + OwningCollection->OnItemStackRemoved(Stack); + } + else if (OwningCollection->PendingItemStacks.Contains(Stack.Id)) + { + GIS_OWNED_CLOG(OwningCollection, Warning, "Discard pending item stack(%s).", *OwningCollection->PendingItemStacks[Stack.Id].GetDebugString()) + OwningCollection->PendingItemStacks.Remove(Stack.Id); + } + + Stack.LastObservedAmount = 0; + } +} + +void FGIS_ItemStackContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGIS_ItemStack& Stack = Stacks[Index]; + + if (OwningCollection && OwningCollection->IsInitialized() && Stack.IsValidStack()) + { + OwningCollection->OnItemStackAdded(Stack); + } + else if (OwningCollection->PendingItemStacks.Contains(Stack.Id)) + { + OwningCollection->PendingItemStacks[Stack.Id] = Stack; + } + else + { + OwningCollection->PendingItemStacks.Add(Stack.Id, Stack); + } + Stack.LastObservedAmount = Stack.Amount; + } +} + +void FGIS_ItemStackContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + FGIS_ItemStack& Stack = Stacks[Index]; + check(Stack.LastObservedAmount != INDEX_NONE); + + if (OwningCollection->StackToIdxMap.Contains(Stack.Id)) //Already Added. + { + OwningCollection->OnItemStackUpdated(Stack); + } + else if (OwningCollection->PendingItemStacks.Contains(Stack.Id)) //In pending list. + { + OwningCollection->PendingItemStacks.Emplace(Stack.Id, Stack); // Updated to pending. + } + else + { + OwningCollection->PendingItemStacks.Add(Stack.Id, Stack); //Add to pending list. + } + Stack.LastObservedAmount = Stack.Amount; + } +} + +const FGIS_ItemStack* FGIS_ItemStackContainer::FindById(const FGuid& StackId) const +{ + if (!StackId.IsValid()) + { + return nullptr; + } + return Stacks.FindByPredicate([StackId](const FGIS_ItemStack& Stack) + { + return Stack.Id == StackId; + }); +} + +const FGIS_ItemStack* FGIS_ItemStackContainer::FindByItemId(const FGuid& ItemId) const +{ + if (!ItemId.IsValid()) + { + return nullptr; + } + return Stacks.FindByPredicate([ItemId](const FGIS_ItemStack& Stack) + { + return Stack.Item->GetItemId() == ItemId; + }); +} + +int32 FGIS_ItemStackContainer::IndexOfById(const FGuid& StackId) const +{ + if (!StackId.IsValid()) + { + return INDEX_NONE; + } + return Stacks.IndexOfByPredicate([StackId](const FGIS_ItemStack& Stack) + { + return Stack.Id == StackId; + }); +} + +int32 FGIS_ItemStackContainer::IndexOfByItemId(const FGuid& ItemId) const +{ + return Stacks.IndexOfByPredicate([ItemId](const FGIS_ItemStack& Stack) + { + return Stack.Item->GetItemId() == ItemId; + }); +} + +int32 FGIS_ItemStackContainer::IndexOfByIds(const FGuid& StackId, const FGuid& ItemId) const +{ + if (!StackId.IsValid() || !ItemId.IsValid()) + { + return INDEX_NONE; + } + return Stacks.IndexOfByPredicate([StackId,ItemId](const FGIS_ItemStack& Stack) + { + return Stack.Id == StackId && Stack.Item->GetItemId() == ItemId; + }); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingStructLibrary.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingStructLibrary.cpp new file mode 100644 index 0000000..753b8d9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingStructLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_CraftingStructLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CraftingStructLibrary) diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingSystemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingSystemComponent.cpp new file mode 100644 index 0000000..977143e --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_CraftingSystemComponent.cpp @@ -0,0 +1,252 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_CraftingSystemComponent.h" +#include "Engine/World.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_CurrencySystemComponent.h" +#include "GIS_InventoryFunctionLibrary.h" +#include "GIS_InventorySubsystem.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" +#include "GIS_ItemFragment_CraftingRecipe.h" +#include "Items/GIS_ItemInstance.h" +#include "Kismet/KismetMathLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CraftingSystemComponent) + +// Sets default values for this component's properties +UGIS_CraftingSystemComponent::UGIS_CraftingSystemComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + NumOfSelectedItemsCache = 0; + // ... +} + +bool UGIS_CraftingSystemComponent::Craft(const UGIS_ItemDefinition* Recipe, UGIS_InventorySystemComponent* CostInventory, int32 Quantity) +{ + const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(Recipe); + if (RecipeFragment == nullptr) + { + return false; + } + + return CraftInternal(Recipe, CostInventory, Quantity); +} + +bool UGIS_CraftingSystemComponent::CanCraft(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity) +{ + return CanCraftInternal(RecipeDefinition, Inventory, Quantity); +} + +bool UGIS_CraftingSystemComponent::RemoveItemIngredients(UGIS_InventorySystemComponent* Inventory, const TArray& ItemIngredients) +{ + for (int32 i = 0; i < ItemIngredients.Num(); i++) + { + FGIS_ItemInfo ItemInfoToRemove; + ItemInfoToRemove.Item = ItemIngredients[i].Item; + ItemInfoToRemove.Amount = ItemIngredients[i].Amount; + if (Inventory->GetDefaultCollection()->RemoveItem(ItemInfoToRemove).Amount == ItemInfoToRemove.Amount) { continue; } + return false; + } + return false; +} + +bool UGIS_CraftingSystemComponent::IsValidRecipe_Implementation(const UGIS_ItemDefinition* RecipeDefinition) const +{ + if (const UGIS_ItemFragment_CraftingRecipe* Fragment = GetRecipeFragment(RecipeDefinition)) + { + return !Fragment->InputItems.IsEmpty() && !Fragment->OutputItems.IsEmpty(); + } + return false; +} + +bool UGIS_CraftingSystemComponent::CanCraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, + int32 Quantity) +{ + if (!IsValidRecipe(RecipeDefinition)) + { + return false; + } + + const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition); + check(RecipeFragment != nullptr); + + if (!SelectItemForIngredients(Inventory, RecipeFragment->InputItems, Quantity)) + { + return false; + } + + TArray Selected; + int32 SelectedCount = 0; + // Has enough items? 有足够的道具? + if (!CheckIfEnoughItemIngredients(RecipeFragment->InputItems, Quantity, SelectedItemsCache, Selected, SelectedCount)) + { + return false; + } + + UGIS_CurrencySystemComponent* CurrencySystem = Inventory->GetCurrencySystem(); + if (CurrencySystem == nullptr) + { + return false; + } + + // Has enough currencies? 有足够货币? + if (!CurrencySystem->HasCurrencies(UGIS_InventoryFunctionLibrary::MultiplyCurrencies(RecipeFragment->InputCurrencies, Quantity))) + { + return false; + } + + return true; +} + +bool UGIS_CraftingSystemComponent::CraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity) +{ + const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition); + if (RecipeFragment == nullptr) + { + return false; + } + + if (!SelectItemForIngredients(Inventory, RecipeFragment->InputItems, Quantity)) + { + return false; + } + + if (!CanCraftInternal(RecipeDefinition, Inventory, Quantity)) + { + return false; + } + + UGIS_CurrencySystemComponent* CurrencySystem = Inventory->GetCurrencySystem(); + + bool bCurrencyRemoveSuccess = CurrencySystem->RemoveCurrencies(UGIS_InventoryFunctionLibrary::MultiplyCurrencies(RecipeFragment->InputCurrencies, Quantity)); + + bool bItemRemoveSuccess = RemoveItemIngredients(Inventory, SelectedItemsCache); + + if (bCurrencyRemoveSuccess && bItemRemoveSuccess) + { + ProduceCraftingOutput(RecipeDefinition, Inventory, Quantity); + return true; + } + return false; +} + +void UGIS_CraftingSystemComponent::ProduceCraftingOutput(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity) +{ + const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition); + if (RecipeFragment == nullptr) + { + return; + } + + TArray ItemAmounts = UGIS_InventoryFunctionLibrary::MultiplyItemAmounts(RecipeFragment->OutputItems, Quantity); + + for (int32 i = 0; i < ItemAmounts.Num(); i++) + { + FGIS_ItemInfo ItemInfoToAdd; + ItemInfoToAdd.Item = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(Inventory->GetOwner(), ItemAmounts[i].Definition); + ItemInfoToAdd.Amount = ItemAmounts[i].Amount; + Inventory->AddItem(ItemInfoToAdd); + } +} + +const UGIS_ItemFragment_CraftingRecipe* UGIS_CraftingSystemComponent::GetRecipeFragment(const UGIS_ItemDefinition* RecipeDefinition) const +{ + return RecipeDefinition ? RecipeDefinition->FindFragment() : nullptr; +} + +bool UGIS_CraftingSystemComponent::SelectItemForIngredients(const UGIS_InventorySystemComponent* Inventory, const TArray& ItemIngredients, int32 Quantity) +{ + SelectedItemsCache.Empty(); + NumOfSelectedItemsCache = 0; + for (int32 i = 0; i < ItemIngredients.Num(); i++) + { + auto RequiredItem = ItemIngredients[i]; + int32 NeededAmount = RequiredItem.Amount * Quantity; + + TArray FilteredItemInfos; + if (Inventory->GetItemInfosByDefinition(RequiredItem.Definition, FilteredItemInfos)) + { + for (int32 j = 0; j < FilteredItemInfos.Num(); j++) + { + const FGIS_ItemInfo& itemInfo = FilteredItemInfos[j]; + + int32 HaveAmount = itemInfo.Amount; + + bool FoundSelectedItem = false; + for (int k = 0; k < NumOfSelectedItemsCache; k++) + { + const FGIS_ItemInfo& SelectedItemInfo = SelectedItemsCache[k]; + if (itemInfo.Item != SelectedItemInfo.Item) { continue; } + if (itemInfo.StackId != SelectedItemInfo.StackId) { continue; } + + FoundSelectedItem = true; + + HaveAmount = FMath::Max(0, HaveAmount - SelectedItemInfo.Amount); + int32 additionalIgnoreAmount = FMath::Clamp(HaveAmount, 0, NeededAmount); + //更新已经选择道具信息。 + SelectedItemsCache[k] = FGIS_ItemInfo(SelectedItemInfo.Amount + additionalIgnoreAmount, SelectedItemInfo); + } + + int32 SelectedAmount = FMath::Clamp(HaveAmount, 0, NeededAmount); + + //记录已选择道具信息。 + if (FoundSelectedItem == false) + { + SelectedItemsCache.SetNum(NumOfSelectedItemsCache + 1); + SelectedItemsCache[NumOfSelectedItemsCache] = FGIS_ItemInfo(SelectedAmount, itemInfo); + NumOfSelectedItemsCache++; + } + + NeededAmount -= SelectedAmount; + if (NeededAmount <= 0) { break; } + } + } + if (NeededAmount <= 0) { continue; } + return false; + } + return true; +} + +bool UGIS_CraftingSystemComponent::CheckIfEnoughItemIngredients(const TArray& ItemIngredients, int32 Quantity, const TArray& SelectedItems, + TArray& ItemsToIgnore, int32& NumOfItemsToIgnore) +{ + for (int32 i = 0; i < ItemIngredients.Num(); i++) + { + const FGIS_ItemDefinitionAmount& ingredientAmount = ItemIngredients[i]; + + int32 neededAmount = Quantity * ingredientAmount.Amount; + + for (int32 j = 0; j < SelectedItems.Num(); j++) + { + const FGIS_ItemInfo& itemInfo = SelectedItems[j]; + if (ingredientAmount.Definition != itemInfo.Item->GetDefinition()) { continue; } + + int32 haveAmount = itemInfo.Amount; + bool foundMatch = false; + + + int32 selectedAmount = FMath::Clamp(haveAmount, 0, neededAmount); + + if (foundMatch == false) + { + ItemsToIgnore.SetNum(NumOfItemsToIgnore + 1); + ItemsToIgnore[NumOfItemsToIgnore] = FGIS_ItemInfo(selectedAmount, itemInfo); + NumOfItemsToIgnore++; + } + + neededAmount -= selectedAmount; + if (neededAmount <= 0) { break; } + } + + if (neededAmount > 0) + { + return false; + } + } + + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_ItemFragment_CraftingRecipe.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_ItemFragment_CraftingRecipe.cpp new file mode 100644 index 0000000..5f00281 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Crafting/GIS_ItemFragment_CraftingRecipe.cpp @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_ItemFragment_CraftingRecipe.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_CraftingRecipe) + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGIS_ItemFragment_CraftingRecipe::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + for (FGIS_ItemDefinitionAmount& DefaultItem : OutputItems) + { + DefaultItem.EditorFriendlyName = FString::Format(TEXT("{0}x {1}"), {DefaultItem.Amount, DefaultItem.Definition.IsNull() ? TEXT("Invalid Item") : DefaultItem.Definition.GetAssetName()}); + } +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_CurrencyDropper.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_CurrencyDropper.cpp new file mode 100644 index 0000000..2786e1a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_CurrencyDropper.cpp @@ -0,0 +1,44 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Drops/GIS_CurrencyDropper.h" + +#include "GIS_CurrencySystemComponent.h" +#include "GIS_LogChannels.h" +#include "Misc/DataValidation.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyDropper) + +void UGIS_CurrencyDropper::BeginPlay() +{ + MyCurrency = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner()); + if (MyCurrency == nullptr) + { + GIS_CLOG(Warning, "Mising currency system component!"); + } + Super::BeginPlay(); +} + +void UGIS_CurrencyDropper::Drop() +{ + if (AActor* PickupActor = CreatePickupActorInstance()) + { + if (UGIS_CurrencySystemComponent* CurrencySys = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(PickupActor)) + { + CurrencySys->SetCurrencies(MyCurrency->GetAllCurrencies()); + } + } +} + + +#if WITH_EDITOR +EDataValidationResult UGIS_CurrencyDropper::IsDataValid(FDataValidationContext& Context) const +{ + if (PickupActorClass.IsNull()) + { + Context.AddError(FText::FromString(FString::Format(TEXT("%s has no pickup actor class.!"), {*GetName()}))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_DropperComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_DropperComponent.cpp new file mode 100644 index 0000000..6530060 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_DropperComponent.cpp @@ -0,0 +1,78 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Drops/GIS_DropperComponent.h" +#include "Engine/World.h" +#include "GIS_LogChannels.h" +#include "Components/CapsuleComponent.h" +#include "GameFramework/Character.h" +#include "Kismet/KismetMathLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_DropperComponent) + + +UGIS_DropperComponent::UGIS_DropperComponent() +{ + PrimaryComponentTick.bStartWithTickEnabled = false; + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); +} + +void UGIS_DropperComponent::Drop() +{ +} + + +AActor* UGIS_DropperComponent::CreatePickupActorInstance_Implementation() +{ + UWorld* World = GetWorld(); + check(World); + if (PickupActorClass.IsNull()) + { + GIS_CLOG(Error, "missing PickupActorClass!"); + return nullptr; + } + UClass* PickupClass = PickupActorClass.LoadSynchronous(); + if (PickupClass == nullptr) + { + GIS_CLOG(Error, "failed to load PickupActorClass!"); + return nullptr; + } + + FVector Origin = CalcDropOrigin(); + AActor* Pickup = World->SpawnActor(PickupClass, FTransform(Origin + CalcDropOffset())); + if (Pickup == nullptr) + { + GIS_CLOG(Error, "failed to spawn pickup actor from PickupActorClass(%s)!", *PickupClass->GetName()); + return nullptr; + } + return Pickup; +} + +FVector UGIS_DropperComponent::CalcDropOrigin_Implementation() const +{ + if (IsValid(DropTransform)) + { + return DropTransform->GetActorLocation(); + } + + FVector OriginLocation = GetOwner()->GetActorLocation(); + + if (const ACharacter* Character = Cast(GetOwner())) + { + OriginLocation.Z -= Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); + } + else if (const UCapsuleComponent* CapsuleComponent = Cast(GetOwner()->GetRootComponent())) + { + OriginLocation.Z -= CapsuleComponent->GetScaledCapsuleHalfHeight(); + } + return OriginLocation; +} + +FVector UGIS_DropperComponent::CalcDropOffset_Implementation() const +{ + const float RandomX = UKismetMathLibrary::RandomFloatInRange(-DropRadius, DropRadius); + const float RandomY = UKismetMathLibrary::RandomFloatInRange(-DropRadius, DropRadius); + + return FVector(RandomX, RandomY, 0); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_ItemDropperComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_ItemDropperComponent.cpp new file mode 100644 index 0000000..8cc8203 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_ItemDropperComponent.cpp @@ -0,0 +1,108 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Drops/GIS_ItemDropperComponent.h" +#include "GIS_InventoryTags.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_LogChannels.h" +#include "Pickups/GIS_InventoryPickupComponent.h" +#include "Pickups/GIS_ItemPickupComponent.h" +#include "Pickups/GIS_WorldItemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDropperComponent) + +void UGIS_ItemDropperComponent::Drop() +{ + TArray ItemsToDrop = GetItemsToDrop(); + DropItemsInternal(ItemsToDrop); +} + +void UGIS_ItemDropperComponent::BeginPlay() +{ + if (!CollectionTag.IsValid()) + { + CollectionTag = GIS_CollectionTags::Main; + } + + Super::BeginPlay(); +} + +TArray UGIS_ItemDropperComponent::GetItemsToDrop() const +{ + return GetItemsToDropInternal(); +} + +TArray UGIS_ItemDropperComponent::GetItemsToDropInternal() const +{ + TArray Items; + + UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); + if (Inventory == nullptr) + { + GIS_CLOG(Error, "requires inventory system component to drop items.") + return Items; + } + UGIS_ItemCollection* Collection = Inventory->GetCollectionByTag(CollectionTag); + if (Collection == nullptr) + { + GIS_CLOG(Error, " inventory missing collection with tag:%s'", *CollectionTag.ToString()) + return Items; + } + + Items = Collection->GetAllItemInfos(); + return Items; +} + +void UGIS_ItemDropperComponent::DropItemsInternal(const TArray& ItemInfos) +{ + if (bDropAsInventory) + { + DropInventoryPickup(ItemInfos); + } + else + { + for (int32 i = 0; i < ItemInfos.Num(); i++) + { + DropItemPickup(ItemInfos[i]); + } + } +} + +void UGIS_ItemDropperComponent::DropInventoryPickup(const TArray& ItemInfos) +{ + if (AActor* PickupActor = CreatePickupActorInstance()) + { + UGIS_InventorySystemComponent* Inventory = PickupActor->FindComponentByClass(); + UGIS_InventoryPickupComponent* Pickup = PickupActor->FindComponentByClass(); + if (Inventory == nullptr || Pickup == nullptr) + { + GIS_CLOG(Error, "Spawned pickup(%s) missing either inventory component or inventory pickup component.", *PickupActor->GetName()); + return; + } + + UGIS_ItemCollection* Collection = Inventory->GetDefaultCollection(); + if (Collection == nullptr) + { + GIS_CLOG(Error, "Spawned pickup(%s)'s inventory doesn't have default collection.", *PickupActor->GetName()); + return; + } + Collection->RemoveAll(); + Collection->AddItems(ItemInfos); + } +} + +void UGIS_ItemDropperComponent::DropItemPickup(const FGIS_ItemInfo& ItemInfo) +{ + if (AActor* Pickup = CreatePickupActorInstance()) + { + UGIS_ItemPickupComponent* ItemPickup = Pickup->FindComponentByClass(); + UGIS_WorldItemComponent* WorldItem = Pickup->FindComponentByClass(); + if (ItemPickup == nullptr || WorldItem == nullptr) + { + GIS_CLOG(Error, "Spawned pickup(%s) missing either ItemPickup component or WorldItem component.", *Pickup->GetName()); + } + WorldItem->SetItemInfo(ItemInfo.Item, ItemInfo.Amount); + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_RandomItemDropperComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_RandomItemDropperComponent.cpp new file mode 100644 index 0000000..d802c95 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Drops/GIS_RandomItemDropperComponent.cpp @@ -0,0 +1,94 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Drops/GIS_RandomItemDropperComponent.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_RandomItemDropperComponent) + +TArray UGIS_RandomItemDropperComponent::GetItemsToDropInternal() const +{ + TArray Results; + + UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); + if (Inventory == nullptr) + { + GIS_CLOG(Error, "requires inventory system component to drop items.") + return Results; + } + UGIS_ItemCollection* Collection = Inventory->GetCollectionByTag(CollectionTag); + if (Collection == nullptr) + { + GIS_CLOG(Error, " inventory missing collection with tag:%s'", *CollectionTag.ToString()) + return Results; + } + + const TArray& ItemStacks = Collection->GetAllItemStacks(); + + TArray ItemInfos; + ItemInfos.Reserve(ItemStacks.Num()); + int32 ProbabilitySum = 0; + + for (int32 i = 0; i < ItemStacks.Num(); ++i) + { + //加权 + ProbabilitySum += ItemStacks[i].Amount; + ItemInfos[i] = FGIS_ItemInfo(ProbabilitySum, ItemStacks[i]); + } + + int32 RandomAmount = FMath::RandRange(MinAmount, MaxAmount + 1); + Results.Empty(); + + for (int i = 0; i < RandomAmount; i++) + { + auto& selectedItemInfo = GetRandomItemInfo(ItemInfos, ProbabilitySum); + bool foundMatch = false; + //去重,多个栈可能指向同一个道具实例,若发现重复 + for (int j = 0; j < Results.Num(); j++) + { + if (Results[j].Item == selectedItemInfo.Item) + { + Results[j] = FGIS_ItemInfo(Results[j].Amount + 1, Results[j]); + foundMatch = true; + break; + } + } + + if (!foundMatch) { Results.Add(FGIS_ItemInfo(1, selectedItemInfo)); } + } + + return Results; +} + +const FGIS_ItemInfo& UGIS_RandomItemDropperComponent::GetRandomItemInfo(const TArray& ItemInfos, int32 ProbabilitySum) const +{ + int32 RandomProbabilityIdx = FMath::RandRange(0, ProbabilitySum); + + int32 min = 0; + int32 max = ItemInfos.Num() - 1; + int32 mid = 0; + + while (min <= max) + { + mid = (min + max) / 2; + if (ItemInfos[mid].Amount == RandomProbabilityIdx) + { + ++mid; + break; + } + + if (RandomProbabilityIdx < ItemInfos[mid].Amount + && (mid == 0 || RandomProbabilityIdx > ItemInfos[mid - 1].Amount)) { break; } + + if (RandomProbabilityIdx < ItemInfos[mid].Amount) { max = mid - 1; } + else + { + min = mid + 1; + } + } + + return ItemInfos[mid]; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentActorInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentActorInterface.cpp new file mode 100644 index 0000000..808b044 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentActorInterface.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_EquipmentActorInterface.h" +#include "GIS_EquipmentInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentActorInterface) + +// Add default functionality here for any IGIS_EquipmentActorInterface functions that are not pure virtual. +void IGIS_EquipmentActorInterface::ReceiveSourceEquipment_Implementation(UGIS_EquipmentInstance* NewEquipmentInstance, int32 Idx) +{ +} + +UGIS_EquipmentInstance* IGIS_EquipmentActorInterface::GetSourceEquipment_Implementation() const +{ + return nullptr; +} + +void IGIS_EquipmentActorInterface::ReceiveEquipmentBeginPlay_Implementation() +{ +} + +void IGIS_EquipmentActorInterface::ReceiveEquipmentEndPlay_Implementation() +{ +} + +UPrimitiveComponent* IGIS_EquipmentActorInterface::GetPrimitiveComponent_Implementation() const +{ + return nullptr; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInstance.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInstance.cpp new file mode 100644 index 0000000..6c4bb4c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInstance.cpp @@ -0,0 +1,336 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_EquipmentInstance.h" +#include "Engine/World.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/BlueprintGeneratedClass.h" +#include "GameFramework/Character.h" +#include "Net/UnrealNetwork.h" +#include "TimerManager.h" +#if UE_WITH_IRIS +#include "Iris/ReplicationSystem/ReplicationFragmentUtil.h" +#endif // UE_WITH_IRIS +#include "GIS_EquipmentActorInterface.h" +#include "GIS_EquipmentSystemComponent.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_ItemFragment_Equippable.h" +#include "GIS_LogChannels.h" +#include "Pickups/GIS_WorldItemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentInstance) + +class FLifetimeProperty; +class UClass; +class USceneComponent; + +UGIS_EquipmentInstance::UGIS_EquipmentInstance(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + bIsActive = false; +} + +bool UGIS_EquipmentInstance::IsSupportedForNetworking() const +{ + return true; +} + +UWorld* UGIS_EquipmentInstance::GetWorld() const +{ + if (OwningPawn) + { + return OwningPawn->GetWorld(); + } + if (UObject* Outer = GetOuter()) + { + return Outer->GetWorld(); + } + return nullptr; +} + +void UGIS_EquipmentInstance::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + // DOREPLIFETIME(ThisClass, SourceItem); + + // fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4 + UBlueprintGeneratedClass* bpClass = Cast(this->GetClass()); + if (bpClass != nullptr) + { + bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps); + } + DOREPLIFETIME(ThisClass, EquipmentActors); +} + +void UGIS_EquipmentInstance::ReceiveOwningPawn_Implementation(APawn* NewPawn) +{ + OwningPawn = NewPawn; +} + +APawn* UGIS_EquipmentInstance::GetOwningPawn_Implementation() const +{ + return OwningPawn; +} + +void UGIS_EquipmentInstance::ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem) +{ + SourceItem = NewItem; +} + +UGIS_ItemInstance* UGIS_EquipmentInstance::GetSourceItem_Implementation() const +{ + return SourceItem; +} + +void UGIS_EquipmentInstance::OnEquipmentBeginPlay_Implementation() +{ + if (OwningPawn->HasAuthority()) + { + SpawnAndSetupEquipmentActors(SourceItem->FindFragmentByClass()->ActorsToSpawn); + } + else + { + SetupEquipmentActors(EquipmentActors); + } +} + +void UGIS_EquipmentInstance::OnEquipmentTick_Implementation(float DeltaSeconds) +{ +} + +void UGIS_EquipmentInstance::OnEquipmentEndPlay_Implementation() +{ + DestroyEquipmentActors(); +} + +#if UE_WITH_IRIS +void UGIS_EquipmentInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) +{ + using namespace UE::Net; + + // Build descriptors and allocate PropertyReplicationFragments for this object + FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags); +} + +#endif // UE_WITH_IRIS + +bool UGIS_EquipmentInstance::IsEquipmentActive_Implementation() const +{ + return bIsActive; +} + +void UGIS_EquipmentInstance::OnActiveStateChanged_Implementation(bool NewActiveState) +{ + bIsActive = NewActiveState; + SetupActiveStateForEquipmentActors(EquipmentActors); + OnActiveStateChangedEvent.Broadcast(NewActiveState); +} + +APawn* UGIS_EquipmentInstance::GetTypedOwningPawn(TSubclassOf PawnType) const +{ + APawn* Result = nullptr; + if (UClass* ActualPawnType = PawnType) + { + if (APawn* Pawn = Execute_GetOwningPawn(this)) + { + if (Pawn->IsA(ActualPawnType)) + { + Result = Pawn; + } + } + } + return Result; +} + +bool UGIS_EquipmentInstance::CanActivate_Implementation() const +{ + return true; +} + +int32 UGIS_EquipmentInstance::GetIndexOfEquipmentActor(const AActor* InEquipmentActor) const +{ + if (IsValid(InEquipmentActor) && EquipmentActors.Contains(InEquipmentActor)) + { + for (int32 i = 0; i < EquipmentActors.Num(); i++) + { + if (EquipmentActors[i] == InEquipmentActor) + { + return i; + } + } + } + return INDEX_NONE; +} + +AActor* UGIS_EquipmentInstance::GetTypedEquipmentActor(TSubclassOf DesiredClass) const +{ + if (UClass* RealClass = DesiredClass) + { + for (const TObjectPtr& SpawnedActor : EquipmentActors) + { + if (SpawnedActor && SpawnedActor->GetClass()->IsChildOf(RealClass)) + { + return SpawnedActor; + } + } + } + return nullptr; +} + +void UGIS_EquipmentInstance::SpawnAndSetupEquipmentActors(const TArray& ActorsToSpawn) +{ + if (OwningPawn == nullptr || !OwningPawn->HasAuthority()) + { + return; + } + + USceneComponent* AttachParent = GetAttachParentForSpawnedActors(OwningPawn); + + for (int32 i = 0; i < ActorsToSpawn.Num(); ++i) + { + const FGIS_EquipmentActorToSpawn& SpawnInfo = ActorsToSpawn[i]; + if (SpawnInfo.ActorToSpawn.IsNull()) + { + continue; + } + TSubclassOf ActorClass = SpawnInfo.ActorToSpawn.LoadSynchronous(); + if (!ActorClass) + { + GIS_CLOG(Warning, "%s Failed to load actor class at index: %d ", *GetName(), i); + continue; + } + if (!ActorClass->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass())) + { + GIS_CLOG(Warning, "actor class(%s) doesn't implements:%s", *ActorClass->GetName(), *UGIS_EquipmentActorInterface::StaticClass()->GetName()); + continue; + } + AActor* NewActor = GetWorld()->SpawnActorDeferred(ActorClass, FTransform::Identity, OwningPawn); + if (NewActor == nullptr) + { + GIS_CLOG(Warning, "%s Failed to spawn actor of class: %s at index: %d ", *GetName(), *ActorClass->GetName(), i); + continue; + } + BeforeSpawningActor(NewActor); + NewActor->FinishSpawning(FTransform::Identity, /*bIsDefaultTransform=*/true); + if (SpawnInfo.bShouldAttach) + { + NewActor->SetActorRelativeTransform(SpawnInfo.AttachTransform); + NewActor->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform, SpawnInfo.AttachSocket); + } + EquipmentActors.Add(NewActor); + } + SetupEquipmentActors(EquipmentActors); +} + +void UGIS_EquipmentInstance::DestroyEquipmentActors() +{ + if (OwningPawn == nullptr || !OwningPawn->HasAuthority()) + { + return; + } + for (AActor* Actor : EquipmentActors) + { + if (!IsValid(Actor)) + { + continue; + } + + if (IsValid(Actor) && Actor->GetClass()->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass())) + { + IGIS_EquipmentActorInterface::Execute_ReceiveEquipmentEndPlay(Actor); + } + + if (IsValid(Actor)) + { + Actor->Destroy(); + } + } + EquipmentActors.Empty(); +} + +USceneComponent* UGIS_EquipmentInstance::GetAttachParentForSpawnedActors_Implementation(APawn* Pawn) const +{ + if (ACharacter* Char = Cast(Pawn)) + { + return Char->GetMesh(); + } + if (Pawn) + { + return Pawn->FindComponentByClass(); + } + return nullptr; +} + +void UGIS_EquipmentInstance::BeforeSpawningActor_Implementation(AActor* SpawningActor) const +{ +} + +void UGIS_EquipmentInstance::SetupEquipmentActors_Implementation(const TArray& InActors) +{ + if (!OwningPawn) + { + return; + } + + if (OwningPawn->HasAuthority()) + { + for (int32 i = 0; i < InActors.Num(); i++) + { + UGIS_WorldItemComponent* WorldItem = InActors[i]->FindComponentByClass(); + if (WorldItem != nullptr) + { + WorldItem->SetItemInfo(SourceItem, 1); + } + } + } + for (int32 i = 0; i < InActors.Num(); i++) + { + AActor* Actor = InActors[i]; + if (IsValid(Actor)) + { + if (Actor->GetClass()->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass())) + { + IGIS_EquipmentActorInterface::Execute_ReceiveSourceEquipment(Actor, this, i); + IGIS_EquipmentActorInterface::Execute_ReceiveEquipmentBeginPlay(Actor); + } + } + } +} + +void UGIS_EquipmentInstance::OnRep_EquipmentActors() +{ +} + +bool UGIS_EquipmentInstance::IsEquipmentActorsValid(int32 Num) const +{ + if (EquipmentActors.Num() == Num) + { + bool bValid = true; + for (int32 i = 0; i < EquipmentActors.Num(); i++) + { + if (!IsValid(EquipmentActors[i])) + { + bValid = false; + break; + } + } + return bValid; + } + return false; +} + +void UGIS_EquipmentInstance::SetupInitialStateForEquipmentActors(const TArray& InActors) +{ +} + +void UGIS_EquipmentInstance::SetupActiveStateForEquipmentActors(const TArray& InActors) const +{ + for (int32 i = 0; i < InActors.Num(); i++) + { + AActor* Actor = InActors[i]; + if (IsValid(Actor) && Actor->GetClass()->ImplementsInterface(UGIS_EquipmentInterface::StaticClass())) + { + Execute_OnActiveStateChanged(Actor, bIsActive); + } + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInterface.cpp new file mode 100644 index 0000000..71d27b2 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentInterface.cpp @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_EquipmentInterface.h" +#include "GameFramework/Pawn.h" +#include "GIS_EquipmentInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentInterface) + +void IGIS_EquipmentInterface::ReceiveOwningPawn_Implementation(APawn* NewPawn) +{ +} + +APawn* IGIS_EquipmentInterface::GetOwningPawn_Implementation() const +{ + APawn* ReturnPawn = Cast(_getUObject()->GetOuter()); + return ReturnPawn; +} + +void IGIS_EquipmentInterface::ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem) +{ +} + +UGIS_ItemInstance* IGIS_EquipmentInterface::GetSourceItem_Implementation() const +{ + return nullptr; +} + +void IGIS_EquipmentInterface::OnActiveStateChanged_Implementation(bool bNewActiveState) +{ +} + +bool IGIS_EquipmentInterface::IsEquipmentActive_Implementation() const +{ + return false; +} + +void IGIS_EquipmentInterface::OnEquipmentBeginPlay_Implementation() +{ +} + +void IGIS_EquipmentInterface::OnOnEquipmentTick_Implementation(float DeltaSeconds) +{ +} + +void IGIS_EquipmentInterface::OnEquipmentEndPlay_Implementation() +{ +} + + +// Add default functionality here for any IGIS_EquipmentInterface functions that are not pure virtual. + +bool IGIS_EquipmentInterface::IsReplicationManaged_Implementation() +{ + return _getUObject() && _getUObject()->GetClass()->IsChildOf(UGIS_EquipmentInstance::StaticClass()); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentStructLibrary.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentStructLibrary.cpp new file mode 100644 index 0000000..28b2eed --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentStructLibrary.cpp @@ -0,0 +1,179 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_EquipmentStructLibrary.h" + +#include "GIS_EquipmentInstance.h" +#include "GIS_EquipmentSystemComponent.h" +#include "GIS_ItemFragment_Equippable.h" +#include "GIS_ItemInstance.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentStructLibrary) + +FString FGIS_EquipmentEntry::GetDebugString() const +{ + return FString::Printf(TEXT("%s"), *GetNameSafe(Instance)); +} + +bool FGIS_EquipmentEntry::CheckClientDataReady() const +{ + bool bDataReady = Instance != nullptr && ItemInstance != nullptr && EquippedSlot.IsValid(); + if (!bDataReady) + { + return false; + } + UGIS_EquipmentInstance* EquipmentInstance = Cast(Instance); + if (!EquipmentInstance) + return true; + + const UGIS_ItemFragment_Equippable* Equippable = ItemInstance->FindFragmentByClass(); + + if (!Equippable || Equippable->bActorBased || Equippable->ActorsToSpawn.IsEmpty()) + return true; + + TArray EquipmentActors = EquipmentInstance->GetEquipmentActors(); + // actor num doesn't match. + if (EquipmentActors.Num() != Equippable->ActorsToSpawn.Num()) + { + return false; + } + + bool bValid = true; + for (int32 i = 0; i < EquipmentActors.Num(); i++) + { + if (!::IsValid(EquipmentActors[i])) + { + bValid = false; + break; + } + } + return bValid; +} + +bool FGIS_EquipmentEntry::IsValidEntry() const +{ + return Instance != nullptr && ItemInstance != nullptr && EquippedSlot.IsValid(); +} + +void FGIS_EquipmentContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + FGIS_EquipmentEntry& Entry = Entries[Index]; + + // already in the list. + if (OwningComponent->SlotToInstanceMap.Contains(Entry.EquippedSlot)) + { + OwningComponent->OnEquipmentEntryRemoved(Entry, Index); + } + else if (OwningComponent->PendingEquipmentEntries.Contains(Index)) + { + GIS_OWNED_CLOG(OwningComponent, Warning, "Discard pending equipment(%s).", *OwningComponent->PendingEquipmentEntries[Index].GetDebugString()) + OwningComponent->PendingEquipmentEntries.Remove(Index); + } + Entry.bPrevActive = Entry.bActive; + Entry.PrevEquippedGroup = Entry.EquippedGroup; + } +} + +void FGIS_EquipmentContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGIS_EquipmentEntry& Entry = Entries[Index]; + if (OwningComponent) + { + if (OwningComponent->GetOwner() && OwningComponent->IsEquipmentSystemInitialized() && Entry.CheckClientDataReady()) + { + OwningComponent->OnEquipmentEntryAdded(Entry, Index); + } + else + { + OwningComponent->PendingEquipmentEntries.Add(Index, Entry); + } + } + Entry.bPrevActive = Entry.bActive; + } +} + +void FGIS_EquipmentContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + FGIS_EquipmentEntry& Entry = Entries[Index]; + if (OwningComponent->SlotToInstanceMap.Contains(Entry.EquippedSlot)) // Already Added. + { + OwningComponent->OnEquipmentEntryChanged(Entry, Index); + } + else if (OwningComponent->PendingEquipmentEntries.Contains(Index)) // In pending list. + { + OwningComponent->PendingEquipmentEntries.Emplace(Index, Entry); + } + else + { + OwningComponent->PendingEquipmentEntries.Add(Index, Entry); // Add to pending list. + } + Entry.bPrevActive = Entry.bActive; + Entry.PrevEquippedGroup = Entry.EquippedGroup; + } +} + +int32 FGIS_EquipmentContainer::IndexOfBySlot(const FGameplayTag& Slot) const +{ + if (!Slot.IsValid()) + { + return INDEX_NONE; + } + return Entries.IndexOfByPredicate([Slot](const FGIS_EquipmentEntry& Entry) + { + return Entry.EquippedSlot == Slot; + }); +} + +int32 FGIS_EquipmentContainer::IndexOfByGroup(const FGameplayTag& Group) const +{ + if (!Group.IsValid()) + { + return INDEX_NONE; + } + return Entries.IndexOfByPredicate([Group](const FGIS_EquipmentEntry& Entry) + { + return Entry.EquippedGroup.IsValid() && Entry.EquippedGroup == Group; + }); +} + +int32 FGIS_EquipmentContainer::IndexOfByInstance(const UObject* Instance) const +{ + if (!IsValid(Instance)) + { + return INDEX_NONE; + } + return Entries.IndexOfByPredicate([Instance](const FGIS_EquipmentEntry& Entry) + { + return Entry.Instance && Entry.Instance == Instance; + }); +} + +int32 FGIS_EquipmentContainer::IndexOfByItem(const UGIS_ItemInstance* Item) const +{ + if (!IsValid(Item)) + { + return INDEX_NONE; + } + return Entries.IndexOfByPredicate([Item](const FGIS_EquipmentEntry& Entry) + { + return Entry.ItemInstance && Entry.ItemInstance == Item; + }); +} + +int32 FGIS_EquipmentContainer::IndexOfByItemId(const FGuid& ItemId) const +{ + if (!ItemId.IsValid()) + { + return INDEX_NONE; + } + return Entries.IndexOfByPredicate([ItemId](const FGIS_EquipmentEntry& Entry) + { + return Entry.ItemInstance && Entry.ItemInstance->GetItemId() == ItemId; + }); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentSystemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentSystemComponent.cpp new file mode 100644 index 0000000..72abf09 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Equipping/GIS_EquipmentSystemComponent.cpp @@ -0,0 +1,1173 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_EquipmentSystemComponent.h" +#include "Engine/World.h" +#include "GameFramework/Controller.h" +#include "GIS_EquipmentInstance.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" +#include "GIS_ItemFragment_Equippable.h" +#include "GIS_ItemInstance.h" +#include "GIS_ItemSlotCollection.h" +#include "GIS_LogChannels.h" +#include "Engine/ActorChannel.h" +#include "Net/UnrealNetwork.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentSystemComponent) + +UGIS_EquipmentSystemComponent::UGIS_EquipmentSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), Container(this) +{ + PrimaryComponentTick.bCanEverTick = true; + SetIsReplicatedByDefault(true); + bReplicateUsingRegisteredSubObjectList = true; + bWantsInitializeComponent = true; +} + +void UGIS_EquipmentSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + DOREPLIFETIME(ThisClass, Container); + DOREPLIFETIME(ThisClass, bEquipmentSystemInitialized); + DOREPLIFETIME(ThisClass, TargetCollectionDefinition); +} + +void UGIS_EquipmentSystemComponent::EquipItemToSlot(UGIS_ItemInstance* Item, const FGameplayTag& SlotTag) +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + + if (!SlotTag.IsValid()) + { + return; + } + + UObject* EquipmentInstance = CreateEquipmentInstance(GetOwner(), Item); + if (!IsValid(EquipmentInstance)) + { + return; + } + + + FGIS_EquipmentEntry NewEntry; + NewEntry.EquippedSlot = SlotTag; + NewEntry.Instance = EquipmentInstance; + NewEntry.ItemInstance = Item; + NewEntry.bActive = false; + NewEntry.bPrevActive = false; + + AddEquipmentEntry(NewEntry); + + if (auto Equippable = Item->FindFragmentByClass()) + { + if (Equippable->bAutoActivate) + { + SetEquipmentActiveState(SlotTag, true); + } + } +} + +void UGIS_EquipmentSystemComponent::UnequipBySlot(FGameplayTag SlotTag) +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + int32 Idx = Container.IndexOfBySlot(SlotTag); + if (Idx != INDEX_NONE) + { + RemoveEquipmentEntry(Idx); + } +} + +void UGIS_EquipmentSystemComponent::UnequipByItem(const FGuid& ItemId) +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + int32 Idx = Container.IndexOfByItemId(ItemId); + if (Idx != INDEX_NONE) + { + RemoveEquipmentEntry(Idx); + } +} + +bool UGIS_EquipmentSystemComponent::OwnerHasAuthority() const +{ + AActor* Owner = GetOwner(); + return IsValid(Owner) && Owner->HasAuthority(); +} + +TArray UGIS_EquipmentSystemComponent::GetEquipments(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const +{ + TArray Results; + if (SlotQuery.IsEmpty()) + { + return Results; + } + + if (UClass* RealClass = InstanceType) + { + const TArray& MatchedEntries = Container.Entries.FilterByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) + { + return Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); + }); + for (const FGIS_EquipmentEntry& Entry : MatchedEntries) + { + Results.AddUnique(Entry.Instance); + } + } + return Results; +} + +TArray UGIS_EquipmentSystemComponent::GetActiveEquipments(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const +{ + TArray Results; + if (SlotQuery.IsEmpty()) + { + return Results; + } + + if (UClass* RealClass = InstanceType) + { + const TArray& MatchedEntries = Container.Entries.FilterByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) + { + return Entry.bActive && Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); + }); + for (const FGIS_EquipmentEntry& Entry : MatchedEntries) + { + Results.AddUnique(Entry.Instance); + } + } + return Results; +} + +UObject* UGIS_EquipmentSystemComponent::GetEquipment(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const +{ + if (UClass* RealClass = InstanceType) + { + const FGIS_EquipmentEntry* Found = Container.Entries.FindByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) + { + return Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); + }); + if (Found) + { + return Found->Instance; + } + } + return nullptr; +} + +UObject* UGIS_EquipmentSystemComponent::GetActiveEquipment(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const +{ + if (UClass* RealClass = InstanceType) + { + const FGIS_EquipmentEntry* Found = Container.Entries.FindByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) + { + return Entry.bActive && Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); + }); + if (Found) + { + return Found->Instance; + } + } + return nullptr; +} + +UObject* UGIS_EquipmentSystemComponent::GetActiveEquipmentInGroup(FGameplayTag GroupTag, bool bExactMatch) const +{ + if (!GroupTag.IsValid()) + { + return nullptr; + } + int32 Idx = Container.Entries.IndexOfByPredicate([GroupTag,bExactMatch](const FGIS_EquipmentEntry& Entry) + { + return Entry.bActive && Entry.EquippedGroup.IsValid() && bExactMatch ? Entry.EquippedGroup.MatchesTagExact(GroupTag) : Entry.EquippedGroup.MatchesTag(GroupTag); + }); + if (Idx != INDEX_NONE) + { + return Container.Entries[Idx].Instance; + } + return nullptr; +} + +UGIS_EquipmentInstance* UGIS_EquipmentSystemComponent::GetEquipmentInstanceOfActor(AActor* EquipmentActor) const +{ + if (IsValid(EquipmentActor)) + { + for (const FGIS_EquipmentEntry& Entry : Container.Entries) + { + if (UGIS_EquipmentInstance* Instance = Cast(Entry.Instance)) + { + if (Instance->GetEquipmentActors().Contains(EquipmentActor)) + { + return Instance; + } + } + } + } + return nullptr; +} + +UGIS_EquipmentInstance* UGIS_EquipmentSystemComponent::GetTypedEquipmentInstanceOfActor(TSubclassOf InstanceType, AActor* EquipmentActor) const +{ + if (UClass* RealClass = InstanceType) + { + if (UGIS_EquipmentInstance* Instance = GetEquipmentInstanceOfActor(EquipmentActor)) + { + if (Instance->GetClass()->IsChildOf(RealClass)) + { + return Instance; + } + } + } + return nullptr; +} + +bool UGIS_EquipmentSystemComponent::IsSlotEquipped(FGameplayTag SlotTag) const +{ + return SlotToInstanceMap.Contains(SlotTag); +} + +FGameplayTag UGIS_EquipmentSystemComponent::GetSlotByEquipment(UObject* Equipment) const +{ + if (!IsValid(Equipment)) + { + return FGameplayTag::EmptyTag; + } + int32 Idx = Container.IndexOfByInstance(Equipment); + if (Idx != INDEX_NONE) + { + return Container.Entries[Idx].EquippedSlot; + } + return FGameplayTag::EmptyTag; +} + +FGameplayTag UGIS_EquipmentSystemComponent::GetSlotByItem(const UGIS_ItemInstance* Item) const +{ + if (!IsValid(Item)) + { + return FGameplayTag::EmptyTag; + } + int32 Idx = Container.IndexOfByItem(Item); + if (Idx != INDEX_NONE) + { + return Container.Entries[Idx].EquippedSlot; + } + return FGameplayTag::EmptyTag; +} + +int32 UGIS_EquipmentSystemComponent::SlotTagToEquipmentInex(FGameplayTag InSlotTag) const +{ + return Container.IndexOfBySlot(InSlotTag); +} + +int32 UGIS_EquipmentSystemComponent::ItemIdToEquipmentInex(FGuid InItemId) const +{ + return Container.IndexOfByItemId(InItemId); +} + +UObject* UGIS_EquipmentSystemComponent::GetEquipmentInSlot(FGameplayTag SlotTag) const +{ + int32 Idx = Container.IndexOfBySlot(SlotTag); + + if (Idx != INDEX_NONE) + { + return Container.Entries[Idx].Instance; + } + return nullptr; +} + +UObject* UGIS_EquipmentSystemComponent::GetEquipmentByItem(const UGIS_ItemInstance* Item) +{ + if (Item == nullptr) + { + return nullptr; + } + int32 Idx = Container.IndexOfByItem(Item); + if (Idx != INDEX_NONE) + { + return Container.Entries[Idx].Instance; + } + return nullptr; +} + +void UGIS_EquipmentSystemComponent::SetEquipmentActiveState(FGameplayTag SlotTag, bool NewActiveState) +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + int32 Idx = Container.IndexOfBySlot(SlotTag); + if (Idx == INDEX_NONE) + { + GIS_CLOG(Warning, "invalid equipment entry at slot:%s.", *SlotTag.ToString()) + return; + } + const FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; + // same state, return early. + if (Entry.bActive == NewActiveState) + { + GIS_CLOG(Warning, "trying to set same active state to equipment entry at slot:%s.", *SlotTag.ToString()) + return; + } + // check if the item is groupped. + FGameplayTag MatchingGroup = FindMatchingGroupForSlot(Entry.EquippedSlot); + + // has group + if (MatchingGroup.IsValid()) + { + //check occupied if trying to active. + if (NewActiveState) + { + int32 OccupiedIdx = Container.IndexOfByGroup(MatchingGroup); + + // Already occupied and is not this one. + if (OccupiedIdx != INDEX_NONE && OccupiedIdx != Idx) + { + GIS_CLOG(Warning, "can't active equipment at idx(%d) due to target slot(%s) within group(%s) was occupied.", Idx, *SlotTag.ToString(), *MatchingGroup.ToString()) + return; + } + } + UpdateEquipmentState(Idx, NewActiveState, MatchingGroup); + } + else + { + UpdateEquipmentState(Idx, NewActiveState, FGameplayTag::EmptyTag); + } +} + +void UGIS_EquipmentSystemComponent::ServerSetEquipmentActiveState_Implementation(FGameplayTag SlotTag, bool NewActiveState) +{ + SetEquipmentActiveState(SlotTag, NewActiveState); +} + +void UGIS_EquipmentSystemComponent::UpdateEquipmentState(int32 Idx, bool NewActiveState, FGameplayTag NewGroup) +{ + check(Container.Entries.IsValidIndex(Idx)) + if (Container.Entries[Idx].bActive == NewActiveState) + { + // Same state, return. + return; + } + + FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; + Entry.bActive = NewActiveState; + + Entry.PrevEquippedGroup = Entry.EquippedGroup; + Entry.EquippedGroup = NewActiveState ? NewGroup : FGameplayTag::EmptyTag; + + OnEquipmentEntryChanged(Entry, Idx); + Container.MarkItemDirty(Entry); +} + +void UGIS_EquipmentSystemComponent::OnTargetCollectionChanged(const FGIS_InventoryStackUpdateMessage& Message) +{ + // only handle equip/unequip on the server side. + if (!OwnerHasAuthority()) + { + return; + } + + // Make use this message comes from the inventory&&collection I care about. + bool bIsMyConcern = IsValid(Message.Inventory) && Message.Inventory == Inventory && Message.CollectionId == TargetCollection->GetCollectionId(); + + if (!bIsMyConcern) + { + return; + } + + switch (Message.ChangeType) + { + case EGIS_ItemStackChangeType::WasAdded: + { + FGameplayTag SlotTag = TargetCollection->GetItemSlotName(Message.Instance); + if (!SlotTag.IsValid()) + { + return; + } + EquipItemToSlot(Message.Instance, SlotTag); + return; + } + case EGIS_ItemStackChangeType::WasRemoved: + { + UnequipByItem(Message.Instance->GetItemId()); + } + default: + break; + } +} + +void UGIS_EquipmentSystemComponent::ProcessPendingEquipments() +{ + if (!bEquipmentSystemInitialized || !HasBegunPlay() || GetOwner() == nullptr) + { + return; + } + + TArray AddedEquipments; + for (auto& Pair : PendingEquipmentEntries) + { + if (Pair.Value.CheckClientDataReady()) + { + AddedEquipments.Add(Pair.Key); + } + } + + for (int32 i = 0; i < AddedEquipments.Num(); i++) + { + int32 idx = AddedEquipments[i]; + if (PendingEquipmentEntries.Contains(idx)) + { + const FGIS_EquipmentEntry& Entry = PendingEquipmentEntries[idx]; + OnEquipmentEntryAdded(Entry, idx); + PendingEquipmentEntries.Remove(idx); + } + } +} + +void UGIS_EquipmentSystemComponent::OnEquipmentSystemInitialized_Implementation() +{ + OnEquipmentSystemInitializedEvent.Broadcast(); + + TArray Delegates = InitializedDelegates; + for (FGIS_EquipmentSystem_Initialized_DynamicEvent Delegate : Delegates) + { + Delegate.ExecuteIfBound(); + } + InitializedDelegates.Empty(); +} + +void UGIS_EquipmentSystemComponent::OnTargetCollectionRemoved(UGIS_ItemCollection* Collection) +{ + if (Collection && TargetCollection && TargetCollection == Collection) + { + ResetEquipmentSystem(); + } +} + +bool UGIS_EquipmentSystemComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) +{ + bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags); + + for (FGIS_EquipmentEntry& Entry : Container.Entries) + { + if (IsValid(Entry.Instance)) + { + if (IGIS_EquipmentInterface::Execute_IsReplicationManaged(Entry.Instance)) + { + WroteSomething |= Channel->ReplicateSubobject(Entry.Instance, *Bunch, *RepFlags); + } + } + } + + return WroteSomething; +} + +void UGIS_EquipmentSystemComponent::OnRegister() +{ + Super::OnRegister(); +} + +void UGIS_EquipmentSystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + if (GetWorld() && !GetWorld()->IsGameWorld()) + { + return; + } + + Container.OwningComponent = this; + if (!GetOwner()->IsUsingRegisteredSubObjectList()) + { + GIS_CLOG(Error, "requires enable bReplicateUsingRegisteredSubObjectList.") + } +} + +void UGIS_EquipmentSystemComponent::ReadyForReplication() +{ + Super::ReadyForReplication(); + // Register existing Equipment Instance + if (IsUsingRegisteredSubObjectList()) + { + for (const TObjectPtr& PendingObject : PendingReplicatedEquipments) + { + if (!IsReplicatedSubObjectRegistered(PendingObject)) + { + AddReplicatedSubObject(PendingObject); + } + } + PendingReplicatedEquipments.Empty(); + } +} + +// Called when the game starts +void UGIS_EquipmentSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + if (bInitializeOnBeginPlay && OwnerHasAuthority()) + { + InitializeEquipmentSystem(); + } +} + +void UGIS_EquipmentSystemComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGIS_EquipmentSystemComponent::TickComponent"), STAT_UGIS_EquipmentSystemComponent_TickComponent, STATGROUP_GIS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + ProcessPendingEquipments(); + TickEquipmentEntries(DeltaTime); +} + +void UGIS_EquipmentSystemComponent::UninitializeComponent() +{ + Super::UninitializeComponent(); +} + +void UGIS_EquipmentSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (bInitializeOnBeginPlay) + { + ResetEquipmentSystem(); + } + + Super::EndPlay(EndPlayReason); +} + +void UGIS_EquipmentSystemComponent::InitializeEquipmentSystem() +{ + if (bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "already initialized or has no authority!") + return; + } + + UGIS_InventorySystemComponent* InventorySystem = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); + + if (InventorySystem == nullptr) + { + InventorySystem = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetController()); + } + + if (!InventorySystem) + { + GIS_CLOG(Error, "doesn't have valid inventory system component!") + return; + } + + InitializeEquipmentSystemWithInventory(InventorySystem); +} + +void UGIS_EquipmentSystemComponent::InitializeEquipmentSystemWithInventory(UGIS_InventorySystemComponent* InventorySystem) +{ + if (bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "already initialized or has no authority!") + return; + } + + if (!IsValid(InventorySystem)) + { + GIS_CLOG(Error, "the inventory is invalid!") + return; + } + + if (!InventorySystem->IsInventoryInitialized()) + { + GIS_CLOG(Error, "the inventory is not initialized!") + return; + } + + if (!TargetCollectionTag.IsValid()) + { + GIS_CLOG(Error, "doesn't have valid target collection tag!") + return; + } + + UGIS_ItemSlotCollection* Collection = Cast(InventorySystem->GetCollectionByTag(GetTargetCollectionTag())); + + if (Collection == nullptr) + { + GIS_CLOG(Error, "%s's inventory doesn't have valid item slot collection with name:%s", *InventorySystem->GetOwner()->GetName(), *TargetCollectionTag.ToString()); + return; + } + Inventory = InventorySystem; + TargetCollection = Collection; + TargetCollectionDefinition = Collection->GetMyDefinition(); + + TArray Entries; + for (const TPair& Pair : TargetCollectionDefinition->SlotGroupMap) + { + FGIS_EquipmentGroupEntry Entry; + Entry.GroupTag = Pair.Key; + Entry.ActiveSlot = FGameplayTag::EmptyTag; + Entries.Add(Entry); + } + + Inventory->OnInventoryStackUpdate.AddDynamic(this, &ThisClass::OnTargetCollectionChanged); + Inventory->OnCollectionRemovedEvent.AddDynamic(this, &ThisClass::OnTargetCollectionRemoved); + + bEquipmentSystemInitialized = true; + OnEquipmentSystemInitialized(); + + for (const FGIS_ItemInfo& ItemInfo : TargetCollection->GetAllItemInfos()) + { + FGameplayTag SlotTag = TargetCollection->GetItemSlotName(ItemInfo.Item); + EquipItemToSlot(ItemInfo.Item, SlotTag); + } +} + +void UGIS_EquipmentSystemComponent::ResetEquipmentSystem() +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + RemoveAllEquipments(); + if (IsValid(Inventory)) + { + Inventory->OnCollectionRemovedEvent.RemoveDynamic(this, &ThisClass::OnTargetCollectionRemoved); + Inventory->OnInventoryStackUpdate.RemoveDynamic(this, &ThisClass::OnTargetCollectionChanged); + Inventory = nullptr; + TargetCollection = nullptr; + TargetCollectionDefinition = nullptr; + } + bEquipmentSystemInitialized = false; + OnEquipmentSystemInitialized(); +} + +bool UGIS_EquipmentSystemComponent::IsEquipmentSystemInitialized() const +{ + return bEquipmentSystemInitialized; +} + +void UGIS_EquipmentSystemComponent::BindToEquipmentSystemInitialized(FGIS_EquipmentSystem_Initialized_DynamicEvent Delegate) +{ + if (bEquipmentSystemInitialized) + { + Delegate.ExecuteIfBound(); + } + else + { + InitializedDelegates.Add(Delegate); + } +} + +UGIS_EquipmentSystemComponent* UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +bool UGIS_EquipmentSystemComponent::FindEquipmentSystemComponent(const AActor* Actor, UGIS_EquipmentSystemComponent*& Component) +{ + Component = (Actor ? Actor->FindComponentByClass() : nullptr); + return Component != nullptr; +} + +bool UGIS_EquipmentSystemComponent::FindTypedEquipmentSystemComponent(AActor* Actor, TSubclassOf DesiredClass, UGIS_EquipmentSystemComponent*& Component) +{ + if (UClass* RealClass = DesiredClass) + { + if (FindEquipmentSystemComponent(Actor, Component)) + { + if (Component->GetClass()->IsChildOf(RealClass)) + { + return true; + } + } + } + return false; +} + +void UGIS_EquipmentSystemComponent::RemoveAllEquipments() +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + for (int32 i = 0; i < Container.Entries.Num(); i++) + { + RemoveEquipmentEntry(i); + } +} + +void UGIS_EquipmentSystemComponent::AddEquipmentEntry(const FGIS_EquipmentEntry& NewEntry) +{ + check(NewEntry.IsValidEntry()) + int32 Idx = Container.Entries.AddDefaulted(); + Container.Entries[Idx] = NewEntry; + + AddReplicatedEquipmentObject(NewEntry.Instance); + OnEquipmentEntryAdded(NewEntry, Idx); + Container.MarkItemDirty(Container.Entries[Idx]); +} + +void UGIS_EquipmentSystemComponent::TickEquipmentEntries(float DeltaTime) +{ + if (!bEquipmentSystemInitialized) + { + return; + } + for (int32 i = 0; i < Container.Entries.Num(); i++) + { + if (Container.Entries[i].IsValidEntry()) + { + IGIS_EquipmentInterface::Execute_OnEquipmentTick(Container.Entries[i].Instance, DeltaTime); + } + } +} + +void UGIS_EquipmentSystemComponent::RemoveEquipmentEntry(int32 Idx) +{ + check(Container.Entries.IsValidIndex(Idx)); + const FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; + RemoveReplicatedEquipmentObject(Entry.Instance); + OnEquipmentEntryRemoved(Entry, Idx); + Container.Entries.RemoveAt(Idx); + Container.MarkArrayDirty(); +} + +UObject* UGIS_EquipmentSystemComponent::CreateEquipmentInstance_Implementation(AActor* Owner, UGIS_ItemInstance* ItemInstance) const +{ + if (ItemInstance == nullptr) + { + GIS_CLOG(Error, "passed in invalid item instance.") + return nullptr; + } + const UGIS_ItemFragment_Equippable* EquippableItem = ItemInstance->FindFragmentByClass(); + + if (EquippableItem == nullptr) + { + GIS_CLOG(Error, "missing equippable fragment on item(%s)", *ItemInstance->GetDefinition()->GetName()) + return nullptr; + } + + TSubclassOf InstanceType = !EquippableItem->InstanceType.IsNull() ? EquippableItem->InstanceType.LoadSynchronous() : nullptr; + if (InstanceType == nullptr) + { + GIS_CLOG(Error, "missing valid equipment instance type on item(%s)", *ItemInstance->GetDefinition()->GetName()) + return nullptr; + } + if (!InstanceType->ImplementsInterface(UGIS_EquipmentInterface::StaticClass())) + { + GIS_CLOG(Error, "equipment instance type doesn't implement:%s", *UGIS_EquipmentInterface::StaticClass()->GetName()) + return nullptr; + } + + UObject* Instance; + + if (EquippableItem->bActorBased) + { + FActorSpawnParameters SpawnParameters; + SpawnParameters.Owner = Owner; + SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + AActor* SpawnedActor = GetWorld()->SpawnActor(InstanceType, FTransform::Identity, SpawnParameters); + if (SpawnedActor == nullptr) + { + GIS_CLOG(Error, "failed to create equipment instance of type:%s", *InstanceType->GetName()) + return nullptr; + } + Instance = SpawnedActor; + } + else + { + UGIS_EquipmentInstance* EquipmentInstance = NewObject(Owner, InstanceType); // Using the actor instead of component as the outer due to UE-127172 + if (EquipmentInstance == nullptr) + { + GIS_CLOG(Error, "failed to create equipment instance of type:%s", *InstanceType->GetName()) + return nullptr; + } + Instance = EquipmentInstance; + } + + return Instance; +} + +void UGIS_EquipmentSystemComponent::OnEquipmentEntryAdded(const FGIS_EquipmentEntry& Entry, int32 Idx) +{ + APawn* OwningPawn = GetPawn(); + + SlotToInstanceMap.Add(Entry.EquippedSlot, Entry.Instance); + + // Initialize equipment instance + IGIS_EquipmentInterface::Execute_ReceiveOwningPawn(Entry.Instance, OwningPawn); + IGIS_EquipmentInterface::Execute_ReceiveSourceItem(Entry.Instance, Entry.ItemInstance); + IGIS_EquipmentInterface::Execute_OnEquipmentBeginPlay(Entry.Instance); + + // Delay event broadcasting to ensure all initialization is complete + FGIS_EquipmentEntry AddedEntry = Entry; + GetWorld()->GetTimerManager().SetTimerForNextTick(FTimerDelegate::CreateLambda([this, AddedEntry]() + { + OnEquipmentStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, true); + + if (AddedEntry.bActive) + { + IGIS_EquipmentInterface::Execute_OnActiveStateChanged(AddedEntry.Instance, AddedEntry.bActive); + OnEquipmentActiveStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, AddedEntry.bActive); + } + if (AddedEntry.EquippedGroup.IsValid()) + { + OnEquipmentGroupStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, AddedEntry.EquippedGroup);; + } + })); +} + +void UGIS_EquipmentSystemComponent::OnEquipmentEntryChanged(const FGIS_EquipmentEntry& Entry, int32 Idx) +{ + IGIS_EquipmentInterface::Execute_OnActiveStateChanged(Entry.Instance, Entry.bActive); + OnEquipmentActiveStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, Entry.bActive); + if (Entry.PrevEquippedGroup != Entry.EquippedGroup) + { + OnEquipmentGroupStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, Entry.EquippedGroup); + } +} + +void UGIS_EquipmentSystemComponent::OnEquipmentEntryRemoved(const FGIS_EquipmentEntry& Entry, int32 Idx) +{ + // remove but still active, so notify instance to do deactivate behavior. + + SlotToInstanceMap.Remove(Entry.EquippedSlot); + + if (IsValid(Entry.Instance)) // The instance may alreay in pending kill state, so no point to continues execution. + { + if (Entry.bActive) + { + IGIS_EquipmentInterface::Execute_OnActiveStateChanged(Entry.Instance, false); + } + IGIS_EquipmentInterface::Execute_OnEquipmentEndPlay(Entry.Instance); + IGIS_EquipmentInterface::Execute_ReceiveOwningPawn(Entry.Instance, nullptr); + IGIS_EquipmentInterface::Execute_ReceiveSourceItem(Entry.Instance, nullptr); + } + + OnEquipmentStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, false); + OnEquipmentGroupStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, FGameplayTag::EmptyTag); +} + +void UGIS_EquipmentSystemComponent::AddReplicatedEquipmentObject(TObjectPtr Instance) +{ + if (OwnerHasAuthority() && IsValid(Instance)) + { + checkf(Instance->GetClass()->ImplementsInterface(UGIS_EquipmentInterface::StaticClass()), TEXT("%s doesn't implement GIS_EquipmentInterface"), *Instance->GetClass()->GetName()) + bool IsReplicationManaged = IGIS_EquipmentInterface::Execute_IsReplicationManaged(Instance); + if (!IsReplicationManaged) + { + return; + } + if (IsReadyForReplication() && !IsReplicatedSubObjectRegistered(Instance)) + { + AddReplicatedSubObject(Instance); + } + else + { + PendingReplicatedEquipments.AddUnique(Instance); + } + } +} + +void UGIS_EquipmentSystemComponent::RemoveReplicatedEquipmentObject(TObjectPtr Instance) +{ + if (OwnerHasAuthority() && IsValid(Instance)) + { + bool IsReplicationManaged = IGIS_EquipmentInterface::Execute_IsReplicationManaged(Instance); + + if (IsReplicationManaged && IsReplicatedSubObjectRegistered(Instance)) + { + RemoveReplicatedSubObject(Instance); + } + } +} + +#pragma region Equipment Groups + +TMap UGIS_EquipmentSystemComponent::GetLayoutOfGroup(FGameplayTag GroupTag) const +{ + TMap GroupLayout; + if (TargetCollectionDefinition == nullptr) + { + return GroupLayout; + } + if (TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) + { + GroupLayout = TargetCollectionDefinition->SlotGroupMap[GroupTag].IndexToSlotMap; + } + return GroupLayout; +} + +TMap UGIS_EquipmentSystemComponent::GetSlottedLayoutOfGroup(FGameplayTag GroupTag) const +{ + TMap GroupLayout; + if (TargetCollectionDefinition == nullptr) + { + return GroupLayout; + } + if (TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) + { + GroupLayout = TargetCollectionDefinition->SlotGroupMap[GroupTag].SlotToIndexMap; + } + return GroupLayout; +} + +FGameplayTag UGIS_EquipmentSystemComponent::FindMatchingGroupForSlot(FGameplayTag SlotTag) const +{ + if (TargetCollectionDefinition->SlotGroupMap.IsEmpty()) + { + return FGameplayTag::EmptyTag; + } + for (const TPair& Pair : TargetCollectionDefinition->SlotGroupMap) + { + if (Pair.Value.SlotToIndexMap.Contains(SlotTag)) + { + return Pair.Key; + } + } + return FGameplayTag::EmptyTag; +} + +TMap UGIS_EquipmentSystemComponent::GetSlottedEquipmentsOfGroup(FGameplayTag GroupTag) const +{ + TMap GroupLayout = GetSlottedLayoutOfGroup(GroupTag); + + TMap GroupedEquipments; + GroupedEquipments.Reserve(GroupLayout.Num()); + + for (auto& Pair : GroupLayout) + { + int32 Idx = Container.IndexOfBySlot(Pair.Key); + if (Idx != INDEX_NONE) + { + GroupedEquipments.Emplace(Pair.Key, Container.Entries[Idx].Instance); + } + else + { + GroupedEquipments.Emplace(Pair.Key, nullptr); + } + } + return GroupedEquipments; +} + +TMap UGIS_EquipmentSystemComponent::GetEquipmentsOfGroup(FGameplayTag GroupTag) const +{ + TMap GroupLayout = GetLayoutOfGroup(GroupTag); + + TMap GroupedEquipments; + GroupedEquipments.Reserve(GroupLayout.Num()); + + for (auto& Pair : GroupLayout) + { + int32 Idx = Container.IndexOfBySlot(Pair.Value); + if (Idx != INDEX_NONE) + { + GroupedEquipments.Emplace(Pair.Key, Container.Entries[Idx].Instance); + } + else + { + GroupedEquipments.Emplace(Pair.Key, nullptr); + } + } + return GroupedEquipments; +} + +void UGIS_EquipmentSystemComponent::SetGroupActiveSlot(FGameplayTag GroupTag, FGameplayTag NewSlot) +{ + if (!bEquipmentSystemInitialized || !TargetCollectionDefinition || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + + if (TargetCollectionDefinition->SlotGroupMap.IsEmpty()) + { + GIS_CLOG(Warning, "no equipment groups setup within TargetCollectionDefinition(%s)!", *TargetCollectionDefinition->GetName()) + return; + } + + if (!GroupTag.IsValid()) + { + GIS_CLOG(Warning, "requested invalid equipment group!") + return; + } + + if (!TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) + { + GIS_CLOG(Warning, "requested equipment group doesn't exists within TargetCollectionDefinition(%s)!", *TargetCollectionDefinition->GetName()) + return; + } + + int32 IdxToReset = Container.IndexOfByGroup(GroupTag); + if (IdxToReset != INDEX_NONE) + { + const FGIS_EquipmentEntry& ExistingEntry = Container.Entries[IdxToReset]; + + if (ExistingEntry.EquippedGroup == GroupTag && ExistingEntry.EquippedSlot == NewSlot) + { + // Same state, return. + return; + } + + // Deactivate the equipment for prev active slot. + if (Container.Entries[IdxToReset].bActive) + { + UpdateEquipmentState(IdxToReset, false, FGameplayTag::EmptyTag); + } + } + + int32 IdxToSet = NewSlot.IsValid() ? Container.IndexOfBySlot(NewSlot) : INDEX_NONE; + + if (IdxToSet != INDEX_NONE) + { + if (!Container.Entries[IdxToSet].bActive) + { + UpdateEquipmentState(IdxToSet, true, GroupTag); + } + } +} + +void UGIS_EquipmentSystemComponent::ServerSetGroupActiveSlot_Implementation(FGameplayTag GroupTag, FGameplayTag NewSlot) +{ + SetGroupActiveSlot(GroupTag, NewSlot); +} + +void UGIS_EquipmentSystemComponent::CycleGroupActiveSlot(FGameplayTag GroupTag, bool bDirection) +{ + if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) + { + GIS_CLOG(Error, "not initialized or has no authority!") + return; + } + + if (!TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) + { + GIS_CLOG(Warning, "has no equipment group named:%s.", *GroupTag.ToString()) + return; + } + FGameplayTag PrevSlot = FGameplayTag::EmptyTag; + + int32 Idx = Container.IndexOfByGroup(GroupTag); + + if (Idx != INDEX_NONE) + { + PrevSlot = Container.Entries[Idx].EquippedSlot; + } + + FGameplayTag NewSlot = CycleGroupNextSlot(GroupTag, PrevSlot, bDirection); + + if (NewSlot != PrevSlot) + { + SetGroupActiveSlot(GroupTag, NewSlot); + } +} + +FGameplayTag UGIS_EquipmentSystemComponent::CycleGroupNextSlot(FGameplayTag GroupTag, FGameplayTag PrevSlot, bool bDirection) +{ + TMap GroupedEquipments = GetSlottedEquipmentsOfGroup(GroupTag); + + if (GroupedEquipments.IsEmpty()) + { + // No equipments in the group, nothing to cycle + return FGameplayTag::EmptyTag; + } + + if (!TargetCollectionDefinition || !TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) + { + return FGameplayTag::EmptyTag; + } + + const FGIS_ItemSlotGroup& SlotGroup = TargetCollectionDefinition->SlotGroupMap[GroupTag]; + + // Get all slots in the group + TArray AllSlots; + SlotGroup.IndexToSlotMap.GenerateValueArray(AllSlots); + + if (AllSlots.IsEmpty()) + { + return FGameplayTag::EmptyTag; + } + + // Starting index for the search + int32 StartIndex; + if (!PrevSlot.IsValid()) + { + StartIndex = bDirection ? 0 : AllSlots.Num() - 1; + } + else + { + StartIndex = AllSlots.Find(PrevSlot); + if (StartIndex == INDEX_NONE) + { + StartIndex = bDirection ? 0 : AllSlots.Num() - 1; + } + else + { + StartIndex = bDirection ? StartIndex + 1 : StartIndex - 1; + if (bDirection && StartIndex >= AllSlots.Num()) + { + StartIndex = 0; + } + if (!bDirection && StartIndex < 0) + { + StartIndex = AllSlots.Num() - 1; + } + } + } + + // Now, search for the next valid slot starting from StartIndex, wrapping around + int32 CurrentIndex = StartIndex; + bool bWrappedAround = false; + + do + { + FGameplayTag CurrentSlot = AllSlots[CurrentIndex]; + if (GroupedEquipments.Contains(CurrentSlot) && IsValid(GroupedEquipments[CurrentSlot])) + { + // Found a valid equipment at this slot + return CurrentSlot; + } + + // Move to the next index + if (bDirection) + { + CurrentIndex++; + if (CurrentIndex >= AllSlots.Num()) + { + CurrentIndex = 0; + } + } + else + { + CurrentIndex--; + if (CurrentIndex < 0) + { + CurrentIndex = AllSlots.Num() - 1; + } + } + + // Check if we've wrapped around fully + if (CurrentIndex == StartIndex) + { + bWrappedAround = true; + } + } + while (!bWrappedAround); + + // If no valid slot found after full cycle, return empty tag + return FGameplayTag::EmptyTag; +} + +void UGIS_EquipmentSystemComponent::ServerCycleGroupActiveSlot_Implementation(FGameplayTag GroupTag, bool bDirection) +{ + CycleGroupActiveSlot(GroupTag, bDirection); +} + +#pragma endregion diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyContainer.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyContainer.cpp new file mode 100644 index 0000000..f9bdb1c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyContainer.cpp @@ -0,0 +1,46 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_CurrencyContainer.h" + +#include "GIS_CurrencySystemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyContainer) + +void FGIS_CurrencyContainer::PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize) +{ + for (int32 Index : RemovedIndices) + { + FGIS_CurrencyEntry& Entry = Entries[Index]; + if (OwningComponent) + { + OwningComponent->OnCurrencyEntryRemoved(Entry, Index); + } + Entry.PrevAmount = 0; + } +} + +void FGIS_CurrencyContainer::PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize) +{ + for (int32 Index : AddedIndices) + { + FGIS_CurrencyEntry& Entry = Entries[Index]; + if (OwningComponent) + { + OwningComponent->OnCurrencyEntryAdded(Entry, Index); + } + Entry.PrevAmount = Entry.Amount; + } +} + +void FGIS_CurrencyContainer::PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize) +{ + for (int32 Index : ChangedIndices) + { + FGIS_CurrencyEntry& Entry = Entries[Index]; + if (OwningComponent) + { + OwningComponent->OnCurrencyEntryUpdated(Entry, Index, Entry.PrevAmount, Entry.Amount); + } + Entry.PrevAmount = Entry.Amount; + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyDefinition.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyDefinition.cpp new file mode 100644 index 0000000..d12c204 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyDefinition.cpp @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Exchange/GIS_CurrencyDefinition.h" + +#include "GIS_InventorySubsystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyDefinition) + +FGIS_CurrencyExchangeRate::FGIS_CurrencyExchangeRate(const UGIS_CurrencyDefinition* InCurrency, float InExchangeRate) +{ + Currency = InCurrency; + ExchangeRate = InExchangeRate; +} + + +bool UGIS_CurrencyDefinition::TryGetExchangeRateTo(const UGIS_CurrencyDefinition* OtherCurrency, double& ExchangeRate) const +{ + if (OtherCurrency == nullptr) + { + ExchangeRate = -1; + return false; + } + + //same currency + if (OtherCurrency == this) + { + ExchangeRate = 1; + return true; + } + + const FGIS_CurrencyExchangeRate RootExchangeRate = GetRootExchangeRate(); + const FGIS_CurrencyExchangeRate OtherRootExchangeRate = OtherCurrency->GetRootExchangeRate(); + if (RootExchangeRate.Currency != OtherRootExchangeRate.Currency) + { + ExchangeRate = -1; + return false; + } + + ExchangeRate = RootExchangeRate.ExchangeRate / OtherRootExchangeRate.ExchangeRate; + return true; +} + +FGIS_CurrencyExchangeRate UGIS_CurrencyDefinition::GetRootExchangeRate(double AdditionalExchangeRate) const +{ + if (ParentCurrency) + { + return ParentCurrency->GetRootExchangeRate(AdditionalExchangeRate * ExchangeRateToParent); + } + return FGIS_CurrencyExchangeRate(this, AdditionalExchangeRate); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyEntry.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyEntry.cpp new file mode 100644 index 0000000..a1f6e05 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencyEntry.cpp @@ -0,0 +1,44 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_CurrencyEntry.h" +#include "GIS_CurrencyDefinition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyEntry) +FGIS_CurrencyEntry::FGIS_CurrencyEntry() +{ + Definition = nullptr; + Amount = 0; +} + +FGIS_CurrencyEntry::FGIS_CurrencyEntry(const TObjectPtr& InDefinition, float InAmount) +{ + Definition = InDefinition; + Amount = InAmount; +} + +FGIS_CurrencyEntry::FGIS_CurrencyEntry(float InAmount, const TObjectPtr& InDefinition) +{ + Definition = InDefinition; + Amount = InAmount; +} + +bool FGIS_CurrencyEntry::Equals(const FGIS_CurrencyEntry& Other) const +{ + return Amount == Other.Amount && Definition == Other.Definition; +} + +FString FGIS_CurrencyEntry::ToString() const +{ + return FString::Format(TEXT("{0} {1}"), {Definition ? Definition->GetName() : TEXT("None"), Amount}); +} + +bool FGIS_CurrencyEntry::operator==(const FGIS_CurrencyEntry& Rhs) const +{ + return Equals(Rhs); +} + +bool FGIS_CurrencyEntry::operator!=(const FGIS_CurrencyEntry& Rhs) const +{ + return !Equals(Rhs); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencySystemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencySystemComponent.cpp new file mode 100644 index 0000000..3665483 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/GIS_CurrencySystemComponent.cpp @@ -0,0 +1,550 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Exchange/GIS_CurrencySystemComponent.h" +#include "Engine/World.h" +#include "GIS_InventorySubsystem.h" +#include "GameFramework/Actor.h" +#include "UObject/Object.h" +#include "GIS_InventoryTags.h" +#include "GIS_LogChannels.h" +#include "Net/UnrealNetwork.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencySystemComponent) + +UGIS_CurrencySystemComponent::UGIS_CurrencySystemComponent() : Container(this) +{ + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); + bReplicateUsingRegisteredSubObjectList = true; + bWantsInitializeComponent = true; +} + +UGIS_CurrencySystemComponent* UGIS_CurrencySystemComponent::GetCurrencySystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +void UGIS_CurrencySystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, Container); +} + +void UGIS_CurrencySystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + if (GetWorld() && !GetWorld()->IsGameWorld()) + { + return; + } + + Container.OwningComponent = this; +} + +TArray UGIS_CurrencySystemComponent::GetAllCurrencies() const +{ + TArray Ret; + for (const FGIS_CurrencyEntry& Item : Container.Entries) + { + Ret.Add(FGIS_CurrencyEntry(Item.Definition, Item.Amount)); + } + return Ret; +} + +void UGIS_CurrencySystemComponent::SetCurrencies(const TArray& InCurrencyInfos) +{ + TArray NewEntries = InCurrencyInfos.FilterByPredicate([](const FGIS_CurrencyEntry& Item) + { + return Item.Definition != nullptr && Item.Amount > 0; + }); + + Container.Entries.Empty(); + CurrencyMap.Empty(); + Container.Entries = NewEntries; + for (const FGIS_CurrencyEntry& NewItem : NewEntries) + { + CurrencyMap.Add(NewItem.Definition, NewItem.Amount); + } + Container.MarkArrayDirty(); +} + +void UGIS_CurrencySystemComponent::EmptyCurrencies() +{ + Container.Entries.Empty(); + CurrencyMap.Empty(); + Container.MarkArrayDirty(); +} + +bool UGIS_CurrencySystemComponent::GetCurrency(TSoftObjectPtr CurrencyDefinition, FGIS_CurrencyEntry& OutCurrencyInfo) const +{ + if (CurrencyDefinition.IsNull()) + { + return false; + } + + const UGIS_CurrencyDefinition* Definition = CurrencyDefinition.LoadSynchronous(); + + return GetCurrencyInternal(Definition, OutCurrencyInfo); +} + +bool UGIS_CurrencySystemComponent::GetCurrencies(TArray> CurrencyDefinitions, TArray& OutCurrencyInfos) const +{ + TArray> Definitions; + for (TSoftObjectPtr Currency : CurrencyDefinitions) + { + if (const UGIS_CurrencyDefinition* Definition = !Currency.IsNull() ? Currency.LoadSynchronous() : nullptr) + { + Definitions.AddUnique(Definition); + } + } + return GetCurrenciesInternal(Definitions, OutCurrencyInfos); +} + +bool UGIS_CurrencySystemComponent::AddCurrency(FGIS_CurrencyEntry CurrencyInfo) +{ + return AddCurrencyInternal(CurrencyInfo); +} + +bool UGIS_CurrencySystemComponent::RemoveCurrency(FGIS_CurrencyEntry CurrencyInfo) +{ + return RemoveCurrencyInternal(CurrencyInfo); +} + +bool UGIS_CurrencySystemComponent::HasCurrency(FGIS_CurrencyEntry CurrencyInfo) const +{ + return HasCurrencyInternal(CurrencyInfo); +} + +bool UGIS_CurrencySystemComponent::HasCurrencies(const TArray& CurrencyInfos) +{ + bool bOk = true; + for (auto& Currency : CurrencyInfos) + { + if (!HasCurrencyInternal(Currency)) + { + bOk = false; + break; + } + } + return bOk; +} + +bool UGIS_CurrencySystemComponent::AddCurrencies(const TArray& CurrencyInfos) +{ + for (const FGIS_CurrencyEntry& Currency : CurrencyInfos) + { + AddCurrencyInternal(Currency); + } + return true; +} + +bool UGIS_CurrencySystemComponent::RemoveCurrencies(const TArray& CurrencyInfos) +{ + for (const FGIS_CurrencyEntry& Currency : CurrencyInfos) + { + RemoveCurrencyInternal(Currency); + } + return true; +} + +void UGIS_CurrencySystemComponent::BeginPlay() +{ + Super::BeginPlay(); + AddInitialCurrencies(); +} + +void UGIS_CurrencySystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); +} + +#pragma region Static Calculations (To be continued) +#if 0 +bool UGIS_CurrencySystemComponent::AddCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify) +{ + if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0) + { + FFrame::KismetExecutionMessage(TEXT("An invalid currency definition was passed."), ELogVerbosity::Warning); + return false; + } + + auto discreteCurrencyAmounts = ConvertToDiscrete(CurrencyInfo.Definition, CurrencyInfo.Amount); + + + auto currencyAmountCopy = Container.Entries; + + auto added = DiscreteAddition(currencyAmountCopy, discreteCurrencyAmounts); + + SetCurrencyInfos(added); + return true; +} + +int32 UGIS_CurrencySystemComponent::FindIndexWithCurrency(const UGIS_CurrencyDefinition* Currency, const TArray& List) +{ + for (int32 i = 0; i < List.Num(); i++) + { + if (List[i].Definition == Currency) + { + return i; + } + } + return INDEX_NONE; +} + +int32 UGIS_CurrencySystemComponent::FindOrCreateCurrencyIndex(const UGIS_CurrencyDefinition* Currency, TArray& Result) +{ + int32 Index = FindIndexWithCurrency(Currency, Result); + if (Index != INDEX_NONE) + { + return Index; + } + + Result.Add(FGIS_CurrencyEntry(0.0f, Currency)); + return Result.Num() - 1; +} + +TArray UGIS_CurrencySystemComponent::DiscreteAddition(const TArray& Lhs, const TArray& Rhs) +{ + TArray Result; + TArray TempArray = Lhs; // 复制 Lhs 作为初始结果 + + for (int32 i = 0; i < Rhs.Num(); i++) + { + // 交替使用 Result 和 TempArray 避免重复分配 + TArray& Target = (i % 2 == 0) ? Result : TempArray; + TempArray = DiscreteAddition(TempArray, Rhs[i].Definition, Rhs[i].Amount); + } + + // 确保最终结果存储在 Result 中 + if (TempArray != Result) + { + Result = TempArray; + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::DiscreteAddition(const TArray& CurrencyAmounts, const UGIS_CurrencyDefinition* Currency, float Amount) +{ + TArray Result = CurrencyAmounts; // 复制输入数组 + while (true) + { + int32 Index = FindOrCreateCurrencyIndex(Currency, Result); + + float Sum = Amount + Result[Index].Amount; + float Mod = FMath::Fmod(Sum, Currency->MaxAmount + 1.0f); + Result[Index] = FGIS_CurrencyEntry(Mod, Currency); + + float Overflow = Sum - Mod; + if (Overflow <= 0.0f) + { + break; + } + + const UGIS_CurrencyDefinition* OverflowCurrency = Currency->OverflowCurrency; + double Rate; + if (!OverflowCurrency || !Currency->TryGetExchangeRateTo(OverflowCurrency, Rate)) + { + return SetAllFractionToMax(CurrencyAmounts, Currency); + } + + double OverflowDouble = Overflow * Rate; // 使用 Rate 而非 Amount + int32 OverflowInt = FMath::TruncToInt(OverflowDouble); + + double Diff = OverflowDouble - OverflowInt; + if (Diff > 0.0) + { + TArray DiscreteDiff = ConvertToDiscrete(OverflowCurrency, Diff); + Result = DiscreteAddition(Result, DiscreteDiff); + } + + Currency = OverflowCurrency; + Amount = static_cast(OverflowInt); + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::SetAllFractionToMax(const TArray& CurrencyAmounts, const UGIS_CurrencyDefinition* Currency) +{ + TArray Result = CurrencyAmounts; + + while (Currency != nullptr) + { + int32 Index = FindOrCreateCurrencyIndex(Currency, Result); + Result[Index] = FGIS_CurrencyEntry(Currency->MaxAmount, Currency); + Currency = Currency->FractionCurrency; + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::MaxedOutAmount(const UGIS_CurrencyDefinition* Currency) +{ + TArray Result; + int32 Index = 0; + + while (Currency != nullptr) + { + Result.SetNum(Index + 1); + Result[Index] = FGIS_CurrencyEntry(Currency->MaxAmount, Currency); + Index++; + Currency = Currency->FractionCurrency; + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::ConvertToDiscrete(const UGIS_CurrencyDefinition* Currency, double Amount) +{ + TArray Result; + int32 Index = 0; + + TArray OverflowCurrencies = ConvertOverflow(Currency, Amount); + + bool bMaxed = false; + for (int32 i = OverflowCurrencies.Num() - 1; i >= 0; i--) + { + Result.SetNum(Index + 1); + if (Currency->FractionCurrency == OverflowCurrencies[i].Definition) + { + bMaxed = true; + } + Result[Index] = OverflowCurrencies[i]; + Index++; + } + + if (!bMaxed) + { + TArray FractionCurrencies = ConvertFraction(Currency, Amount); + for (int32 i = 0; i < FractionCurrencies.Num(); i++) + { + Result.SetNum(Index + 1); + Result[Index] = FractionCurrencies[i]; + Index++; + } + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::ConvertOverflow(const UGIS_CurrencyDefinition* Currency, double Amount) +{ + TArray Result; + int32 Index = 0; + const UGIS_CurrencyDefinition* NextCurrency = Currency; + + Amount = FMath::TruncToDouble(Amount); + + while (NextCurrency != nullptr && Amount > 0.0) + { + double Mod = fmod(Amount, NextCurrency->MaxAmount + 1.0); + float IntMod = static_cast(Mod); + if (IntMod > 0.0f) + { + Result.SetNum(Index + 1); + Result[Index] = FGIS_CurrencyEntry(IntMod, NextCurrency); + Index++; + } + + if (Amount - IntMod <= 0.0) + { + break; + } + + double Rate; + if (!NextCurrency->OverflowCurrency || !NextCurrency->TryGetExchangeRateTo(NextCurrency->OverflowCurrency, Rate)) + { + return MaxedOutAmount(NextCurrency); + } + + Amount -= Mod; + Amount *= Rate; + NextCurrency = NextCurrency->OverflowCurrency; + } + + return Result; +} + +TArray UGIS_CurrencySystemComponent::ConvertFraction(const UGIS_CurrencyDefinition* Currency, double Amount) +{ + TArray Result; + if (FMath::IsNearlyZero(fmod(Amount, 1.0))) + { + return Result; + } + + int32 Index = 0; + const UGIS_CurrencyDefinition* NextCurrency = Currency; + double DecimalAmount = fmod(Amount, 1.0); + + while (NextCurrency != nullptr && DecimalAmount > 0.0) + { + if (!NextCurrency->FractionCurrency) + { + break; + } + + double Rate; + if (!NextCurrency->TryGetExchangeRateTo(NextCurrency->FractionCurrency, Rate)) + { + break; + } + + NextCurrency = NextCurrency->FractionCurrency; + DecimalAmount *= Rate; + int32 Floor = static_cast(FMath::Floor(DecimalAmount)); + + if (Floor > 0) + { + Result.SetNum(Index + 1); + Result[Index] = FGIS_CurrencyEntry(static_cast(Floor), NextCurrency); + Index++; + } + + DecimalAmount -= Floor; + } + + return Result; +} +#endif +#pragma endregion +void UGIS_CurrencySystemComponent::OnCurrencyEntryAdded(const FGIS_CurrencyEntry& Entry, int32 Idx) +{ + CurrencyMap.Add(Entry.Definition, Entry.Amount); + OnCurrencyChangedEvent.Broadcast(Entry.Definition, 0, Entry.Amount); +} + +void UGIS_CurrencySystemComponent::OnCurrencyEntryRemoved(const FGIS_CurrencyEntry& Entry, int32 Idx) +{ + CurrencyMap.Remove(Entry.Definition); + OnCurrencyChangedEvent.Broadcast(Entry.Definition, Entry.PrevAmount, 0); +} + +void UGIS_CurrencySystemComponent::OnCurrencyEntryUpdated(const FGIS_CurrencyEntry& Entry, int32 Idx, float OldAmount, float NewAmount) +{ + CurrencyMap.Emplace(Entry.Definition, NewAmount); + OnCurrencyChangedEvent.Broadcast(Entry.Definition, OldAmount, NewAmount); +} + + +void UGIS_CurrencySystemComponent::OnCurrencyChanged(TObjectPtr Currency, float OldValue, float NewValue) +{ + OnCurrencyChangedEvent.Broadcast(Currency, OldValue, NewValue); +} + + +void UGIS_CurrencySystemComponent::AddInitialCurrencies_Implementation() +{ + if (GetOwner()->HasAuthority()) + { + //TODO check records from save game. + if (!DefaultCurrencies.IsEmpty()) + { + AddCurrencies(DefaultCurrencies); + } + } +} + +bool UGIS_CurrencySystemComponent::GetCurrencyInternal(const TObjectPtr& Currency, FGIS_CurrencyEntry& OutCurrencyInfo) const +{ + if (CurrencyMap.Contains(Currency)) + { + OutCurrencyInfo = FGIS_CurrencyEntry(Currency, CurrencyMap[Currency]); + return true; + } + return false; +} + +bool UGIS_CurrencySystemComponent::GetCurrenciesInternal(const TArray>& Currencies, TArray& OutCurrencies) const +{ + TArray Result; + for (int32 i = 0; i < Currencies.Num(); i++) + { + FGIS_CurrencyEntry Info; + if (GetCurrencyInternal(Currencies[i], Info)) + { + Result.Add(Info); + } + } + + OutCurrencies = Result; + + return !OutCurrencies.IsEmpty(); +} + +bool UGIS_CurrencySystemComponent::AddCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify) +{ + if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0) + { + GIS_CLOG(Warning, "An invalid currency definition was passed.") + // FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed."), ELogVerbosity::Warning); + return false; + } + + for (int32 i = 0; i < Container.Entries.Num(); i++) + { + // handle adding to existing value. + FGIS_CurrencyEntry& Entry = Container.Entries[i]; + if (Entry.Definition == CurrencyInfo.Definition) + { + const float OldValue = Entry.Amount; + const float NewValue = Entry.Amount + CurrencyInfo.Amount; + Entry.Amount = NewValue; + OnCurrencyEntryUpdated(Entry, i, OldValue, NewValue); + Container.MarkItemDirty(Entry); + return true; + } + } + + int32 Idx = Container.Entries.AddDefaulted(); + Container.Entries[Idx] = CurrencyInfo; + OnCurrencyEntryAdded(Container.Entries[Idx], Idx); + Container.MarkItemDirty(Container.Entries[Idx]); + return true; +} + +bool UGIS_CurrencySystemComponent::RemoveCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify) +{ + if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0) + { + GIS_CLOG(Warning, "An invalid tag was passed to RemoveItem") + // FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning); + return false; + } + + for (int32 i = 0; i < Container.Entries.Num(); i++) + { + FGIS_CurrencyEntry& Entry = Container.Entries[i]; + if (Entry.Definition == CurrencyInfo.Definition) + { + if (Entry.Amount <= CurrencyInfo.Amount) + { + OnCurrencyEntryRemoved(Entry, i); + Container.Entries.RemoveAt(i); + Container.MarkArrayDirty(); + } + else + { + const float OldValue = Entry.Amount; + const float NewValue = Entry.Amount - CurrencyInfo.Amount; + Entry.Amount = NewValue; + OnCurrencyEntryUpdated(Entry, i, OldValue, NewValue); + Container.MarkItemDirty(Entry); + } + return true; + } + } + return false; +} + +bool UGIS_CurrencySystemComponent::HasCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo) const +{ + if (CurrencyMap.Contains(CurrencyInfo.Definition) && CurrencyInfo.Amount > 0) + { + return CurrencyMap[CurrencyInfo.Definition] >= CurrencyInfo.Amount; + } + return false; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopCondition.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopCondition.cpp new file mode 100644 index 0000000..73e48a0 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopCondition.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Exchange/Shops/GIS_ShopCondition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ShopCondition) +// Add default functionality here for any IGShopCondition functions that are not pure virtual. diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopSystemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopSystemComponent.cpp new file mode 100644 index 0000000..02cc7ab --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Exchange/Shops/GIS_ShopSystemComponent.cpp @@ -0,0 +1,365 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Exchange/Shops/GIS_ShopSystemComponent.h" +#include "UObject/Object.h" +#include "GameFramework/Actor.h" +#include "GIS_CurrencySystemComponent.h" +#include "GIS_InventoryFunctionLibrary.h" +#include "GIS_InventorySubsystem.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" +#include "GIS_ItemFragment_Shoppable.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_LogChannels.h" +#include "Exchange/Shops/GIS_ShopCondition.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ShopSystemComponent) + + +UGIS_ShopSystemComponent::UGIS_ShopSystemComponent() +{ + PrimaryComponentTick.bStartWithTickEnabled = false; + PrimaryComponentTick.bCanEverTick = false; +} + +UGIS_ShopSystemComponent* UGIS_ShopSystemComponent::GetShopSystemComponent(const AActor* Actor) +{ + return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; +} + +UGIS_InventorySystemComponent* UGIS_ShopSystemComponent::GetInventory() const +{ + return OwningInventory; +} + +bool UGIS_ShopSystemComponent::BuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) +{ + return BuyItemInternal(BuyerInventory, CurrencySystem, ItemInfo); +} + +bool UGIS_ShopSystemComponent::SellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) +{ + return SellItemInternal(SellerInventory, CurrencySystem, ItemInfo); +} + +bool UGIS_ShopSystemComponent::CanBuyerBuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const +{ + UGIS_ItemCollection* TargetCollection = BuyerInventory->GetCollectionByTag(TargetItemCollectionToAddOnBuy); + if (TargetCollection == nullptr) + { + GIS_CLOG(Warning, "buyer's inventory missing collection named:%s", *TargetItemCollectionToAddOnBuy.ToString()); + return false; + } + + FGIS_ItemInfo CanAddItemInfo; + if (!TargetCollection->CanAddItem(ItemInfo, CanAddItemInfo)) + { + GIS_CLOG(Warning, "buyer's collection can't add this item(%s)", *ItemInfo.GetDebugString()); + return false; + } + + for (int i = 0; i < BuyConditions.Num(); i++) + { + if (BuyConditions[i]->CanBuy(this, BuyerInventory, CurrencySystem, ItemInfo)) { continue; } + GIS_CLOG(Warning, "buy collection(%s) reject buying item(%s)", *BuyConditions[i].GetObject()->GetClass()->GetName(), *ItemInfo.GetDebugString()); + return false; + } + + return CanBuyerBuyItemInternal(BuyerInventory, CurrencySystem, ItemInfo); +} + +bool UGIS_ShopSystemComponent::CanSellerSellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const +{ + for (int i = 0; i < SellConditions.Num(); i++) + { + if (SellConditions[i]->CanSell(this, SellerInventory, CurrencySystem, ItemInfo)) { continue; } + GIS_CLOG(Warning, "sell collection(%s) reject selling item(%s)", *SellConditions[i].GetObject()->GetClass()->GetName(), *ItemInfo.GetDebugString()); + return false; + } + return CanSellerSellItemInternal(SellerInventory, CurrencySystem, ItemInfo); +} + +bool UGIS_ShopSystemComponent::IsItemBuyable(const FGIS_ItemInfo& ItemInfo) const +{ + if (OwningInventory == nullptr) { return false; } + + if (!ItemInfo.IsValid()) + { + GIS_CLOG(Warning, "invalid item to buy."); + return false; + } + + UGIS_ItemCollection* ItemCollection = ItemInfo.Item->GetOwningCollection(); + if (ItemCollection == nullptr) { ItemCollection = OwningInventory->GetDefaultCollection(); } + + if (!ItemCollection->HasItem(ItemInfo.Item, 1)) + { + GIS_CLOG(Warning, "shop's inventory doesn't have item:%s", *ItemInfo.Item->GetDefinition()->GetName()); + return false; + } + const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass(); + if (Shoppable == nullptr) + { + GIS_CLOG(Warning, "item(%s) is not buyable, missing Shoppable fragment!", *ItemInfo.GetDebugString()); + return false; + } + if (Shoppable->BuyCurrencyAmounts.IsEmpty()) + { + GIS_CLOG(Warning, "item(%s) is not buyable, missing BuyCurrencyAmounts in shoppable fragment!", *ItemInfo.GetDebugString()); + return false; + } + return true; +} + +bool UGIS_ShopSystemComponent::IsItemSellable(const FGIS_ItemInfo& ItemInfo) const +{ + if (!ItemInfo.IsValid()) + { + GIS_CLOG(Warning, "invalid item to sell."); + return false; + } + + const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass(); + + if (Shoppable == nullptr) + { + GIS_CLOG(Warning, "item(%s) is not sellable, missing Shoppable fragment!", *ItemInfo.GetDebugString()); + return false; + } + + if (Shoppable->SellCurrencyAmounts.IsEmpty()) + { + GIS_CLOG(Warning, "item(%s) is not sellable, missing SellCurrencyAmounts in shoppable fragment!", *ItemInfo.GetDebugString()); + return false; + } + return true; +} + +float UGIS_ShopSystemComponent::GetBuyModifierForBuyer_Implementation(UGIS_InventorySystemComponent* BuyerInventory) const +{ + return 1 + BuyPriceModifier; +} + +// float UGIS_ShopSystemComponent::GetBuyModifierForItem(UGIS_InventorySystemComponent* BuyerInventory, FGIS_ItemInfo ItemInfo) const +// { +// return 1; +// } + +float UGIS_ShopSystemComponent::GetSellModifierForSeller_Implementation(UGIS_InventorySystemComponent* SellerInventory) const +{ + return 1 + SellPriceModifer; +} + +// float UGIS_ShopSystemComponent::GetSellModifierForItem(UGIS_InventorySystemComponent* SellerInventory, const FGIS_ItemInfo& ItemInfo) const +// { +// return 1; +// } + +bool UGIS_ShopSystemComponent::TryGetBuyValueForBuyer_Implementation(UGIS_InventorySystemComponent* Buyer, const FGIS_ItemInfo& ItemInfo, TArray& BuyValue) const +{ + if (!IsValid(ItemInfo.Item)) + { + GIS_CLOG(Warning, "invalid item to buy."); + return false; + } + const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass(); + if (!IsValid(Shoppable)) + { + GIS_CLOG(Warning, "missing Shoppable fragment for item:%s!", *GetNameSafe(ItemInfo.Item->GetDefinition())); + return false; + } + + float Modifier = GetBuyModifierForBuyer(Buyer); + BuyValue = UGIS_InventoryFunctionLibrary::MultiplyCurrencies(Shoppable->BuyCurrencyAmounts, Modifier * ItemInfo.Amount); + + return BuyValue.IsEmpty() == false; +} + +bool UGIS_ShopSystemComponent::TryGetSellValueForSeller_Implementation(UGIS_InventorySystemComponent* Seller, const FGIS_ItemInfo& ItemInfo, TArray& SellValue) const +{ + if (!IsValid(ItemInfo.Item)) + { + GIS_CLOG(Warning, "invalid item to sell."); + return false; + } + + const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass(); + if (!IsValid(Shoppable)) + { + GIS_CLOG(Warning, "missing Shoppable fragment for item:%s!", *GetNameSafe(ItemInfo.Item->GetDefinition())); + return false; + } + + float Modifier = GetSellModifierForSeller(Seller); + + SellValue = UGIS_InventoryFunctionLibrary::MultiplyCurrencies(Shoppable->SellCurrencyAmounts, Modifier * ItemInfo.Amount); + + return SellValue.IsEmpty() == false; +} + +void UGIS_ShopSystemComponent::BeginPlay() +{ + OwningInventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); + if (!OwningInventory) + { + GIS_CLOG(Error, "Requires inventory system component!"); + } + { + TArray Components = GetOwner()->GetComponentsByInterface(UGIS_ShopBuyCondition::StaticClass()); + BuyConditions.Empty(); + for (const auto Component : Components) + { + BuyConditions.Add(Component); + } + } + + { + TArray Components = GetOwner()->GetComponentsByInterface(UGIS_ShopSellCondition::StaticClass()); + SellConditions.Empty(); + for (const auto Component : Components) + { + SellConditions.Add(Component); + } + } + + Super::BeginPlay(); +} + +void UGIS_ShopSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); +} + +bool UGIS_ShopSystemComponent::CanSellerSellItemInternal_Implementation(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, + const FGIS_ItemInfo& ItemInfo) const +{ + UGIS_ItemCollection* ItemCollection = ItemInfo.ItemCollection; + if (ItemCollection == nullptr || ItemCollection->GetOwningInventory() == SellerInventory) + { + ItemCollection = SellerInventory->GetDefaultCollection(); + } + + if (ItemCollection == nullptr) + { + GIS_CLOG(Warning, "seller:%s doesn't have valid default collection.", *GetNameSafe(SellerInventory)); + return false; + } + + //atleast has one. + if (!ItemCollection->HasItem(ItemInfo.Item, 1)) + { + return false; + } + + return true; +} + +bool UGIS_ShopSystemComponent::CanBuyerBuyItemInternal_Implementation(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const +{ + TArray BuyPrice; + if (TryGetBuyValueForBuyer(BuyerInventory, ItemInfo, BuyPrice)) + { + return CurrencySystem->HasCurrencies(BuyPrice); + } + return false; +} + +bool UGIS_ShopSystemComponent::SellItemInternal_Implementation(UGIS_InventorySystemComponent* Seller, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) +{ + if (!IsValid(Seller) || !ItemInfo.IsValid() || !IsValid(CurrencySystem)) + { + GIS_CLOG(Warning, "passed invalid parameters!"); + return false; + } + if (!IsItemSellable(ItemInfo)) + { + GIS_CLOG(Warning, "item:%s is not sellable", *ItemInfo.GetDebugString()); + return false; + } + if (!CanSellerSellItem(Seller, CurrencySystem, ItemInfo)) + { + GIS_CLOG(Warning, "seller can sell this item:%s", *ItemInfo.GetDebugString()); + return false; + } + UGIS_ItemCollection* ItemCollection = ItemInfo.ItemCollection; + if (ItemCollection == nullptr || ItemCollection->GetOwningInventory() == Seller) + { + ItemCollection = Seller->GetDefaultCollection(); + } + if (ItemCollection->RemoveItem(ItemInfo).Amount != ItemInfo.Amount) + { + GIS_CLOG(Error, "Failed to remove item(%s) from inventory!", *ItemInfo.GetDebugString()); + return false; + } + + TArray SellCurrencyAmount; + if (!TryGetSellValueForSeller(Seller, ItemInfo, SellCurrencyAmount)) + { + GIS_CLOG(Error, "can't get sell value for item:%s", *ItemInfo.GetDebugString()); + return false; + } + + CurrencySystem->AddCurrencies(SellCurrencyAmount); + + OwningInventory->AddItem(ItemInfo); + return true; +} + +bool UGIS_ShopSystemComponent::BuyItemInternal_Implementation(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) +{ + if (!IsValid(BuyerInventory) || !ItemInfo.IsValid() || !IsValid(CurrencySystem)) + { + return false; + } + if (!IsItemBuyable(ItemInfo)) + { + return false; + } + if (!CanBuyerBuyItem(BuyerInventory, CurrencySystem, ItemInfo)) + { + return false; + } + + UGIS_ItemCollection* TargetCollection = BuyerInventory->GetCollectionByTag(TargetItemCollectionToAddOnBuy); + if (TargetCollection == nullptr) + { + return false; + } + + //remove currency from buyer's currency system. + TArray BuyPrice; + if (TryGetBuyValueForBuyer(BuyerInventory, ItemInfo, BuyPrice)) + { + CurrencySystem->RemoveCurrencies(BuyPrice); + } + else + { + return false; + } + + if (ItemInfo.Item->IsUnique()) + { + for (int32 i = 0; i < ItemInfo.Amount; ++i) + { + UGIS_ItemInstance* NewItem = UGIS_InventorySubsystem::Get(this)->CreateItem(BuyerInventory->GetOwner(), ItemInfo.Item->GetDefinition()); + TargetCollection->AddItem(NewItem, 1); + } + } + else + { + TargetCollection->AddItem(ItemInfo); + } + + // Remove item from shop's inventory. + OwningInventory->RemoveItem(ItemInfo); + + // Add currency to shop's currency system. + if (UGIS_CurrencySystemComponent* OwningCurrencySystem = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner())) + { + OwningCurrencySystem->AddCurrencies(BuyPrice); + } + + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFactory.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFactory.cpp new file mode 100644 index 0000000..8c32372 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFactory.cpp @@ -0,0 +1,400 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_InventoryFactory.h" +#include "UObject/Object.h" +#include "GameFramework/Actor.h" +#include "Serialization/MemoryReader.h" +#include "Serialization/MemoryWriter.h" +#include "GIS_CollectionContainer.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemDefinition.h" +#include "GIS_ItemFragment.h" +#include "GIS_LogChannels.h" +#include "Items/GIS_ItemInstance.h" +#include "Misc/DataValidation.h" +#include "Serialization/ObjectAndNameAsStringProxyArchive.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryFactory) + +UGIS_ItemInstance* UGIS_InventoryFactory::DuplicateItem_Implementation(AActor* Owner, UGIS_ItemInstance* SrcItem, bool bGenerateNewId) +{ + if (!IsValid(SrcItem) || SrcItem->GetDefinition() == nullptr) + { + GIS_LOG(Error, "Missing src item or src item doesn't have valid definition."); + return nullptr; + } + UGIS_ItemInstance* NewItem = DuplicateObject(SrcItem, Owner); + if (bGenerateNewId) + { + NewItem->SetItemId(FGuid::NewGuid()); + } + NewItem->OnItemDuplicated(SrcItem); + return NewItem; +} + +UGIS_ItemCollection* UGIS_InventoryFactory::CreateCollection_Implementation(AActor* Owner, const UGIS_ItemCollectionDefinition* Definition) +{ + if (!IsValid(Owner)) + { + GIS_LOG(Error, "Missing owner."); + return nullptr; + } + if (!IsValid(Definition)) + { + GIS_LOG(Error, "Cannot create collection with null collection Definition."); + return nullptr; + } + TSubclassOf CollectionClass = Definition->GetCollectionInstanceClass(); + if (CollectionClass == nullptr) + { + GIS_LOG(Error, "definition(%s) doesn't specify valid item collection class.", *Definition->GetName()) + return nullptr; + } + UGIS_ItemCollection* NewCollection = NewObject(Owner, CollectionClass); + if (NewCollection == nullptr) + { + GIS_LOG(Error, "failed to create instance of %s", *GetNameSafe(Definition)) + return nullptr; + } + return NewCollection; +} + +UGIS_InventoryFactory::UGIS_InventoryFactory() +{ + DefaultItemInstanceClass = UGIS_ItemInstance::StaticClass(); +} + +UGIS_ItemInstance* UGIS_InventoryFactory::CreateItem_Implementation(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition) +{ + if (!IsValid(Owner)) + { + GIS_LOG(Error, "Missing owner."); + return nullptr; + } + + if (ItemDefinition == nullptr) + { + GIS_LOG(Error, "Cannot create Item with null Item Definition."); + return nullptr; + } + + TSubclassOf ItemInstanceClass = DefaultItemInstanceClass.LoadSynchronous(); + + if (ItemInstanceClass == nullptr) + { + GIS_LOG(Error, "ItemDefinition: %s has invalid InstanceType.", *ItemDefinition->GetName()); + return nullptr; + } + + UGIS_ItemInstance* Item = NewObject(Owner, ItemInstanceClass); + if (Item == nullptr) + { + GIS_LOG(Error, "ItemInstanceClass: %s create failed.", *ItemInstanceClass->GetName()); + return nullptr; + } + + Item->SetItemId(FGuid::NewGuid()); + Item->SetDefinition(ItemDefinition); + + for (const UGIS_ItemFragment* Fragment : ItemDefinition->Fragments) + { + if (Fragment == nullptr) + { + continue; + } + if (Fragment->GetCompatibleMixinDataType() != nullptr) + { + FInstancedStruct FragmentState; + if (Fragment->MakeDefaultMixinData(FragmentState)) + { + if (FragmentState.IsValid() && FragmentState.GetScriptStruct() == Fragment->GetCompatibleMixinDataType()) + { + Item->SetFragmentStateByClass(Fragment->GetClass(), FragmentState); + } + } + } + Fragment->OnInstanceCreated(Item); + } + return Item; +} + +UGIS_ItemInstance* UGIS_InventoryFactory::DeserializeItem_Implementation(AActor* Owner, const FGIS_ItemRecord& Record) +{ + if (!IsValid(Owner)) + { + GIS_LOG(Error, "Missing owner."); + return nullptr; + } + + const FSoftObjectPath ItemDefinitionAssetPath = FSoftObjectPath(Record.DefinitionAssetPath); + const TSoftObjectPtr ItemDefinitionReference = TSoftObjectPtr(ItemDefinitionAssetPath); + + UGIS_ItemDefinition* ItemDefinition = !ItemDefinitionReference.IsNull() ? ItemDefinitionReference.LoadSynchronous() : nullptr; + if (!IsValid(ItemDefinition)) + { + GIS_LOG(Warning, "invalid item definition on path:%s", *ItemDefinitionAssetPath.ToString()); + return nullptr; + } + + UGIS_ItemInstance* ItemInstance = CreateItem(Owner, ItemDefinition); + if (!IsValid(ItemInstance)) + { + GIS_LOG(Warning, "failed to create item instance from definition:%s", *GetNameSafe(ItemDefinition)); + return nullptr; + } + + ItemInstance->SetItemId(Record.ItemId); + ItemInstance->SetDefinition(ItemDefinition); + + TArray ConvertedMixins = FGIS_MixinContainer::ConvertRecordsToMixins(Record.FragmentStateRecords); + ConvertedMixins = ConvertedMixins.FilterByPredicate([ItemDefinition](const FGIS_Mixin& Mixin) + { + return ItemDefinition->Fragments.Contains(Mixin.Target); + }); + + for (const FGIS_Mixin& ConvertedMixin : ConvertedMixins) + { + ItemInstance->SetFragmentStateByClass(ConvertedMixin.Target->GetClass(), ConvertedMixin.Data); + } + + FMemoryReader Reader(Record.ByteData); + FObjectAndNameAsStringProxyArchive Ar2(Reader, true); + Ar2.ArIsSaveGame = true; + ItemInstance->Serialize(Ar2); + return ItemInstance; +} + +bool UGIS_InventoryFactory::SerializeItem_Implementation(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record) +{ + if (!IsValid(Item)) + { + GIS_LOG(Error, "Missing item."); + return false; + } + + Record.ItemId = Item->GetItemId(); + const FSoftObjectPath AssetPath = FSoftObjectPath(Item->GetDefinition()); + Record.DefinitionAssetPath = AssetPath.ToString(); + Record.FragmentStateRecords = Item->GetFragmentStates().GetSerializableMixinRecords(); + + + FMemoryWriter Writer(Record.ByteData); + FObjectAndNameAsStringProxyArchive Ar(Writer, true); + Ar.ArIsSaveGame = true; + Item->Serialize(Ar); + return true; +} + +bool UGIS_InventoryFactory::SerializeCollection_Implementation(UGIS_ItemCollection* Collection, FGIS_CollectionRecord& Record) +{ + if (!IsValid(Collection)) + { + GIS_LOG(Error, "Missing collection."); + return false; + } + + Record.Tag = Collection->GetCollectionTag(); + Record.Id = Collection->GetCollectionId(); + const FSoftObjectPath AssetPath = FSoftObjectPath(Collection->GetDefinition()); + Record.DefinitionAssetPath = AssetPath.ToString(); + const TArray& ValidStacks = Collection->GetAllItemStacks().FilterByPredicate([](const FGIS_ItemStack& ItemStack) + { + return ItemStack.IsValidStack(); + }); + + for (const FGIS_ItemStack& Stack : ValidStacks) + { + FGIS_StackRecord StackRecord; + StackRecord.Id = Stack.Id; + StackRecord.CollectionId = Stack.Collection->GetCollectionId(); + StackRecord.ItemId = Stack.Item->GetItemId(); + StackRecord.Amount = Stack.Amount; + Record.StackRecords.Add(StackRecord); + } + + return Record.IsValid(); +} + +void UGIS_InventoryFactory::DeserializeCollection_Implementation(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap& ItemsMap) +{ + if (!IsValid(InventorySystem)) + { + GIS_LOG(Error, "Missing inventory system."); + return; + } + const FSoftObjectPath DefinitionAssetPath = FSoftObjectPath(Record.DefinitionAssetPath); + const TSoftObjectPtr DefinitionReference = TSoftObjectPtr(DefinitionAssetPath); + UGIS_ItemCollectionDefinition* Definition = DefinitionReference.LoadSynchronous(); + + if (Definition == nullptr) + { + GIS_LOG(Error, "failed to load definition from path:%s", *DefinitionAssetPath.ToString()); + return; + } + + UGIS_ItemCollection* NewCollection = CreateCollection(InventorySystem->GetOwner(), Definition); + if (NewCollection == nullptr) + { + GIS_LOG(Error, "failed to create collection from definition:%s", *GetNameSafe(Definition)); + return; + } + + FGIS_CollectionEntry NewEntry; + NewEntry.Id = Record.Id; + NewEntry.Instance = NewCollection; + NewEntry.Definition = Definition; + InventorySystem->AddCollectionEntry(NewEntry); + + for (const FGIS_StackRecord& StackRecord : Record.StackRecords) + { + FGIS_ItemInfo Info; + Info.Item = ItemsMap[StackRecord.ItemId]; + Info.Amount = StackRecord.Amount; + Info.ItemCollection = NewCollection; + InventorySystem->AddItem(Info); + } +} + +bool UGIS_InventoryFactory::SerializeInventory_Implementation(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record) +{ + if (!IsValid(InventorySystem)) + { + GIS_LOG(Error, "Missing inventory system."); + return false; + } + + TArray CollectionRecords; + TArray ItemRecords; + + TArray Collections = InventorySystem->GetItemCollections(); + TArray Items; + + // build collection records. + for (UGIS_ItemCollection* Collection : Collections) + { + if (Collection->IsInitialized()) + { + FGIS_CollectionRecord CollectionRecord; + if (SerializeCollection(Collection, CollectionRecord)) + { + CollectionRecords.Add(CollectionRecord); + } + Items.Append(Collection->GetAllItems()); + } + } + + ItemRecords.Reserve(Items.Num()); + for (UGIS_ItemInstance* Item : Items) + { + FGIS_ItemRecord ItemRecord; + if (SerializeItem(Item, ItemRecord)) + { + ItemRecords.Add(ItemRecord); + } + } + + Record.ItemRecords = ItemRecords; + Record.CollectionRecords = CollectionRecords; + + return true; +} + +void UGIS_InventoryFactory::DeserializeInventory_Implementation(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& InRecord) +{ + if (!IsValid(InventorySystem)) + { + GIS_LOG(Error, "Missing inventory system."); + return; + } + + TMap ItemsMap; + for (const FGIS_ItemRecord& ItemRecord : InRecord.ItemRecords) + { + if (UGIS_ItemInstance* Instance = DeserializeItem(InventorySystem->GetOwner(), ItemRecord)) + { + ItemsMap.Emplace(Instance->GetItemId(), Instance); + } + } + + for (const FGIS_CollectionRecord& CollectionRecord : InRecord.CollectionRecords) + { + DeserializeCollection(InventorySystem, CollectionRecord, ItemsMap); + } + + if (!InventorySystem->IsDefaultCollectionCreated()) + { + GIS_OWNED_CLOG(InventorySystem, Warning, + "The default collection definitions is not match with collections restored from inventory record. That may be a problem as you changed the layout of inventory.") + } +} + +// TArray UGIS_InventoryFactory::FilterSerializableFragmentStates(const UGIS_ItemInstance* ItemInstance) +// { +// TArray Mixins = ItemInstance->GetFragmentStates().GetSerializableMixins(); +// TArray Records; +// for (const FGIS_Mixin& Mixin : Mixins) +// { +// if (Mixin.Target->IsA()) +// { +// FGIS_ItemFragmentStateRecord Record; +// Record.FragmentClass = Mixin.Target->GetClass(); +// Record.FragmentState = Mixin.Data; +// Records.Add(Record); +// } +// } +// return Records; +// } + +// TArray UGIS_InventoryFactory::FilterCompatibleFragmentStateRecords(const UGIS_ItemDefinition* ItemDefinition, const FGIS_ItemRecord& Record) +// { +// TArray ConvertedMixins = FGIS_MixinContainer::ConvertRecordsToMixins(Record.FragmentStateRecords); +// +// TArray CompatibleRecords; +// for (const FGIS_ItemFragmentStateRecord& StateRecord : Record.FragmentStateRecords) +// { +// if (StateRecord.FragmentClass == nullptr || !StateRecord.FragmentState.IsValid()) +// { +// GIS_LOG(Warning, "Skip restoring invalid fragment state for item:%s", *ItemDefinition->GetName()); +// continue; +// } +// const UGIS_ItemFragment* Fragment = ItemDefinition->GetFragment(StateRecord.FragmentClass); +// +// if (Fragment == nullptr) +// { +// GIS_LOG(Warning, "Skip restoring fragment's state, as fragment(%s) existed in record no longer exists on item(%s).", +// *GetNameSafe(StateRecord.FragmentClass), *ItemDefinition->GetName()); +// continue; +// } +// +// if (!Fragment->IsMixinDataSerializable()) +// { +// GIS_LOG(Warning, "Skip restoring fragment's state, as fragment(%s) existed in record no longer considered serializable on item(%s).", +// *GetNameSafe(StateRecord.FragmentClass), *ItemDefinition->GetName()); +// continue; +// } +// +// if (Fragment->GetCompatibleMixinDataType() != StateRecord.FragmentState.GetScriptStruct()) +// { +// GIS_LOG(Warning, +// "Skip restoring fragment's state, as fragment(%s)'s state type(%s) in record no longer compatible with the new type(%s) on item(%s).", +// *GetNameSafe(StateRecord.FragmentClass), *GetNameSafe(StateRecord.FragmentState.GetScriptStruct()), *GetNameSafe(Fragment->GetCompatibleMixinDataType()), +// *ItemDefinition->GetName()); +// } +// CompatibleRecords.Add(StateRecord); +// } +// return CompatibleRecords; +// } + +#if WITH_EDITOR +EDataValidationResult UGIS_InventoryFactory::IsDataValid(class FDataValidationContext& Context) const +{ + if (DefaultItemInstanceClass.IsNull()) + { + Context.AddError(FText::FromString(TEXT("Missing Default Item Instance Class"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFunctionLibrary.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFunctionLibrary.cpp new file mode 100644 index 0000000..6b96d91 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryFunctionLibrary.cpp @@ -0,0 +1,63 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_InventoryFunctionLibrary.h" + +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemInstance.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryFunctionLibrary) + +TArray UGIS_InventoryFunctionLibrary::MultiplyItemAmounts(const TArray& ItemAmounts, int32 Multiplier) +{ + TArray Results; + for (int32 i = 0; i < ItemAmounts.Num(); i++) + { + Results.Add(FGIS_ItemDefinitionAmount(ItemAmounts[i].Definition, ItemAmounts[i].Amount * Multiplier)); + } + return Results; +} + +TArray UGIS_InventoryFunctionLibrary::MultiplyCurrencies(const TArray& Currencies, float Multiplier) +{ + TArray Results; + for (int32 i = 0; i < Currencies.Num(); i++) + { + Results.Add(FGIS_CurrencyEntry(Currencies[i].Definition, Currencies[i].Amount * Multiplier)); + } + return Results; +} + +TArray UGIS_InventoryFunctionLibrary::FilterItemInfosByTagQuery(const TArray& ItemInfos, const FGameplayTagQuery& Query) +{ + return ItemInfos.FilterByPredicate([&](const FGIS_ItemInfo& ItemInfo) + { + return ItemInfo.Item != nullptr && ItemInfo.Item->GetItemTags().MatchesQuery(Query); + }); +} + + +TArray UGIS_InventoryFunctionLibrary::FilterItemStacksByTagQuery(const TArray& ItemStacks, const FGameplayTagQuery& TagQuery) +{ + return ItemStacks.FilterByPredicate([TagQuery](const FGIS_ItemStack& Stack) + { + return TagQuery.Matches(Stack.Item->GetItemTags()); + }); +} + +TArray UGIS_InventoryFunctionLibrary::FilterItemStacksByDefinition(const TArray& ItemStacks, const UGIS_ItemDefinition* Definition) +{ + return ItemStacks.FilterByPredicate([Definition](const FGIS_ItemStack& Stack) + { + return Stack.Item->GetDefinition() == Definition; + }); +} + +TArray UGIS_InventoryFunctionLibrary::FilterItemStacksByCollectionTags(const TArray& ItemStacks, const FGameplayTagContainer& CollectionTags) +{ + return ItemStacks.FilterByPredicate([CollectionTags](const FGIS_ItemStack& Stack) + { + return Stack.Collection->GetCollectionTag().MatchesAnyExact(CollectionTags); + }); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySubsystem.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySubsystem.cpp new file mode 100644 index 0000000..70fbd93 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySubsystem.cpp @@ -0,0 +1,146 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_InventorySubsystem.h" +#include "Engine/World.h" +#include "GIS_InventorySystemSettings.h" +#include "GIS_InventoryFactory.h" +#include "GIS_LogChannels.h" +#include "Items/GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" +#include "Items/GIS_ItemInterface.h" +#include "Kismet/KismetMathLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventorySubsystem) + +UGIS_InventorySubsystem* UGIS_InventorySubsystem::Get(const UObject* WorldContextObject) +{ + if (WorldContextObject) + { + return WorldContextObject->GetWorld()->GetGameInstance()->GetSubsystem(); + } + return nullptr; +} + +void UGIS_InventorySubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + InitializeFactory(); +} + +void UGIS_InventorySubsystem::Deinitialize() +{ + Super::Deinitialize(); + Factory = nullptr; +} + +UGIS_ItemInstance* UGIS_InventorySubsystem::CreateItem(AActor* Owner, TSoftObjectPtr ItemDefinition) +{ + if (Factory && !ItemDefinition.IsNull()) + { + UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous(); + + if (LoadedDefinition == nullptr) + { + GIS_LOG(Error, "Cannot create Item with invalid Item Definition."); + return nullptr; + } + + return Factory->CreateItem(Owner, LoadedDefinition); + } + return nullptr; +} + +UGIS_ItemInstance* UGIS_InventorySubsystem::CreateItem(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition) +{ + if (Factory && ItemDefinition != nullptr) + { + return Factory->CreateItem(Owner, ItemDefinition); + } + return nullptr; +} + +UGIS_ItemInstance* UGIS_InventorySubsystem::DuplicateItem(AActor* Owner, UGIS_ItemInstance* FromItem, bool bGenerateNewId) +{ + if (Factory) + { + return Factory->DuplicateItem(Owner, FromItem, bGenerateNewId); + } + return nullptr; +} + +bool UGIS_InventorySubsystem::SerializeItem(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record) +{ + if (Factory) + { + return Factory->SerializeItem(Item, Record); + } + return false; +} + +UGIS_ItemInstance* UGIS_InventorySubsystem::DeserializeItem(AActor* Owner, const FGIS_ItemRecord& Record) +{ + if (Factory) + { + return Factory->DeserializeItem(Owner, Record); + } + return nullptr; +} + +bool UGIS_InventorySubsystem::SerializeCollection(UGIS_ItemCollection* ItemCollection, FGIS_CollectionRecord& Record) +{ + if (Factory) + { + return Factory->SerializeCollection(ItemCollection, Record); + } + return false; +} + +void UGIS_InventorySubsystem::DeserializeCollection(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap& ItemsMap) +{ + if (Factory) + { + return Factory->DeserializeCollection(InventorySystem, Record, ItemsMap); + } +} + +bool UGIS_InventorySubsystem::SerializeInventory(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record) +{ + if (Factory) + { + return Factory->SerializeInventory(InventorySystem, Record); + } + return false; +} + +void UGIS_InventorySubsystem::DeserializeInventory(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& Record) +{ + if (Factory) + { + return Factory->DeserializeInventory(InventorySystem, Record); + } +} + + +void UGIS_InventorySubsystem::InitializeFactory() +{ + if (UGIS_InventorySystemSettings::Get() == nullptr || UGIS_InventorySystemSettings::Get()->InventoryFactoryClass.IsNull()) + { + GIS_LOG(Error, "Missing ItemFactoryClass in inventory system settings."); + return; + } + const UClass* FactoryClass = UGIS_InventorySystemSettings::Get()->InventoryFactoryClass.LoadSynchronous(); + + if (FactoryClass == nullptr) + { + GIS_LOG(Error, "invalid ItemFactoryClass found inventory system settings."); + return; + } + + UGIS_InventoryFactory* TempFactory = NewObject(this, FactoryClass); + if (TempFactory == nullptr) + { + GIS_LOG(Error, "Failed to create item factory instance.Class:%s", *FactoryClass->GetName()); + return; + } + Factory = TempFactory; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemComponent.cpp new file mode 100644 index 0000000..9cbfa62 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemComponent.cpp @@ -0,0 +1,1193 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_InventorySystemComponent.h" +#include "GameFramework/Pawn.h" +#include "Engine/World.h" +#include "GameFramework/PlayerState.h" +#include "GIS_InventoryTags.h" +#include "GIS_InventorySubsystem.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_ItemCollection.h" +#include "Net/UnrealNetwork.h" +#include "GIS_ItemSlotCollection.h" +#include "GIS_CurrencySystemComponent.h" +#include "GIS_LogChannels.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventorySystemComponent) + +// const FName UGIS_InventorySystemComponent::NAME_ActorFeatureName("InventorySystem"); + +UGIS_InventorySystemComponent::UGIS_InventorySystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), CollectionContainer(this) +{ + PrimaryComponentTick.bCanEverTick = true; + SetIsReplicatedByDefault(true); + bReplicateUsingRegisteredSubObjectList = true; + bWantsInitializeComponent = true; +} + +bool UGIS_InventorySystemComponent::FindInventorySystemComponent(const AActor* Actor, UGIS_InventorySystemComponent*& Inventory) +{ + Inventory = GetInventorySystemComponent(Actor); + + return IsValid(Inventory); +} + +UGIS_InventorySystemComponent* UGIS_InventorySystemComponent::GetInventorySystemComponent(const AActor* Actor) +{ + UGIS_InventorySystemComponent* Inventory = Actor ? Actor->FindComponentByClass() : nullptr; + if (!IsValid(Inventory)) + { + if (const APawn* Pawn = Cast(Actor)) + { + if (APlayerState* PS = Cast(Pawn->GetPlayerState())) + { + Inventory = PS->FindComponentByClass(); + } + } + } + return Inventory; +} + +UGIS_InventorySystemComponent* UGIS_InventorySystemComponent::FindInventorySystemComponent(const AActor* Actor) +{ + return GetInventorySystemComponent(Actor); +} + +void UGIS_InventorySystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, bInventorySystemInitialized); + DOREPLIFETIME(ThisClass, CollectionContainer); +} + +bool UGIS_InventorySystemComponent::ReplicateSubobjects(class UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags) +{ + return Super::ReplicateSubobjects(Channel, Bunch, RepFlags); +} + +void UGIS_InventorySystemComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGIS_InventorySystemComponent::TickComponent"), STAT_UGIS_InventorySystemComponent_TickComponent, STATGROUP_GIS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + ProcessPendingCollections(); +} + +bool UGIS_InventorySystemComponent::CanAddItem(const FGIS_ItemInfo& InItemInfo, FGIS_ItemInfo& OutItemInfo) const +{ + if (UGIS_ItemCollection* Collection = DetermineTargetCollection(InItemInfo)) + { + return Collection->CanAddItem(InItemInfo, OutItemInfo); + } + return false; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::DetermineTargetCollection(const FGIS_ItemInfo& ItemInfo) const +{ + UGIS_ItemCollection* Collection = nullptr; + + if (ItemInfo.CollectionId.IsValid()) + { + Collection = GetCollectionById(ItemInfo.CollectionId); + } + if (Collection == nullptr && ItemInfo.CollectionTag.IsValid()) + { + Collection = GetCollectionByTag(ItemInfo.CollectionTag); + } + if (Collection == nullptr) + { + Collection = GetDefaultCollection(); + } + return Collection; +} + +FGIS_ItemInfo UGIS_InventorySystemComponent::AddItem(const FGIS_ItemInfo& ItemInfo) +{ + if (UGIS_ItemCollection* Collection = DetermineTargetCollection(ItemInfo)) + { + const FGIS_ItemInfo AddedItem = Collection->AddItem(ItemInfo); + return AddedItem; + } + return FGIS_ItemInfo::None; +} + +TArray UGIS_InventorySystemComponent::AddItems(TArray ItemInfos) +{ + TArray AddedItemInfos; + for (const FGIS_ItemInfo& ItemInfo : ItemInfos) + { + AddedItemInfos.Add(AddItem(ItemInfo)); + } + return AddedItemInfos; +} + +FGIS_ItemInfo UGIS_InventorySystemComponent::AddItemByDefinition(const FGameplayTag CollectionTag, TSoftObjectPtr ItemDefinition, const int32 NewAmount) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "Has no authority!") + return FGIS_ItemInfo::None; + } + if (UGIS_ItemInstance* NewItem = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(GetOwner(), ItemDefinition)) + { + return AddItem(FGIS_ItemInfo(NewItem, NewAmount, CollectionTag)); + } + return FGIS_ItemInfo::None; +} + +void UGIS_InventorySystemComponent::ServerAddItemByDefinition_Implementation(const FGameplayTag CollectionTag, const TSoftObjectPtr& ItemDefinition, const int32 NewAmount) +{ + AddItemByDefinition(CollectionTag, ItemDefinition, NewAmount); +} + +bool UGIS_InventorySystemComponent::CanMoveItem(const FGIS_ItemInfo& ItemInfo) const +{ + if (!ItemInfo.IsValid() || ItemInfo.Item->GetOwningCollection() == nullptr) + { + GIS_CLOG(Verbose, "item:%s has no source collection!", *ItemInfo.GetDebugString()) + return false; + } + + UGIS_ItemCollection* SrcCollection = ItemInfo.Item->GetOwningCollection(); + + if (ItemInfo.Item->GetOwningInventory() != this) + { + GIS_CLOG(Warning, "item:%s not belong to this inventory.", *ItemInfo.GetDebugString()) + return false; + } + + UGIS_ItemCollection* TargetCollection = DetermineTargetCollection(ItemInfo); + + if (TargetCollection == nullptr || TargetCollection == SrcCollection) + { + GIS_CLOG(Warning, "no dest collection for item:%s to move", *ItemInfo.GetDebugString()) + return false; + } + if (TargetCollection == SrcCollection) + { + GIS_CLOG(Warning, "item:%s already in same collection.", *ItemInfo.GetDebugString()) + return false; + } + + return true; +} + +void UGIS_InventorySystemComponent::MoveItem(const FGIS_ItemInfo& ItemInfo) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "Has no authority!") + return; + } + + if (!CanMoveItem(ItemInfo)) + { + return; + } + + UGIS_ItemInstance* Item = ItemInfo.Item; + UGIS_ItemCollection* SrcCollection = Item->GetOwningCollection(); + + UGIS_ItemCollection* DestCollection = DetermineTargetCollection(ItemInfo); + + // This action used to give the item one way and then the other. + // The action now removes the item, before it adds it to the other collection to allow restrictions to work properly + FGIS_ItemInfo OriginalItem = SrcCollection->RemoveItem(ItemInfo); + FGIS_ItemInfo MovedItemInfo = ItemInfo.None; + + if (UGIS_ItemSlotCollection* DestSlotCollection = Cast(DestCollection)) + { + int32 slotIndex = ItemInfo.StackId.IsValid() ? DestSlotCollection->StackIdToSlotIndex(ItemInfo.StackId) : INDEX_NONE; + + if (slotIndex == INDEX_NONE) // fallback to suitable index. + { + slotIndex = DestSlotCollection->GetTargetSlotIndex(Item); + } + if (slotIndex != INDEX_NONE) + { + FGIS_ItemInfo previousItemInSlot = DestSlotCollection->GetItemInfoAtSlot(slotIndex); + + if (previousItemInSlot.Item != nullptr) + { + // If the previous item is stackable don't remove it. + if (previousItemInSlot.Item->StackableEquivalentTo(OriginalItem.Item)) + { + previousItemInSlot = ItemInfo.None; + } + else + { + previousItemInSlot = DestSlotCollection->RemoveItem(slotIndex); + } + } + + MovedItemInfo = DestSlotCollection->AddItem(OriginalItem, slotIndex); + + if (previousItemInSlot.Item != nullptr) + { + SrcCollection->AddItem(previousItemInSlot); + } + } + } + else + { + MovedItemInfo = DestCollection->AddItem(OriginalItem); + } + + // Not all the item was added, return the items to the default collection. + if (MovedItemInfo.Amount != OriginalItem.Amount) + { + int32 AmountToReturn = OriginalItem.Amount - MovedItemInfo.Amount; + SrcCollection->AddItem(FGIS_ItemInfo(AmountToReturn, OriginalItem)); + } +} + +void UGIS_InventorySystemComponent::ServerMoveItem_Implementation(const FGIS_ItemInfo& ItemInfo) +{ + MoveItem(ItemInfo); +} + +bool UGIS_InventorySystemComponent::CanRemoveItem(const FGIS_ItemInfo& ItemInfo) const +{ + if (ItemInfo.IsValid()) + { + FGIS_ItemInfo ItemInfoToRemove; + return ItemInfo.Item->GetOwningCollection()->RemoveItemCondition(ItemInfo, ItemInfoToRemove); + } + return false; +} + +FGIS_ItemInfo UGIS_InventorySystemComponent::RemoveItem(const FGIS_ItemInfo& ItemInfo) +{ + if (ItemInfo.Item->GetOwningCollection() != nullptr && ItemInfo.Item->GetOwningCollection()->GetOwningInventory() == this) + { + return ItemInfo.Item->GetOwningCollection()->RemoveItem(ItemInfo); + } + return DetermineTargetCollection(ItemInfo)->RemoveItem(ItemInfo); +} + +void UGIS_InventorySystemComponent::ServerRemoveItem_Implementation(FGIS_ItemInfo ItemInfo) +{ + check(GetOwnerRole() == ROLE_Authority) + RemoveItem(ItemInfo); +} + +FGIS_ItemInfo UGIS_InventorySystemComponent::RemoveItemByDefinition(const TSoftObjectPtr ItemDefinition, const int32 Amount) +{ + if (ItemDefinition.IsNull() || Amount == 0) + { + return FGIS_ItemInfo::None; + } + + // The item can be in multiple stacks, for example if it is Unique. + + int32 AmountRemoved = 0; + int32 AmountToRemove = Amount; + FGIS_ItemInfo LastItemInfoRemoved = FGIS_ItemInfo::None; + + for (int32 i = 0; i < Amount; i++) + { + FGIS_ItemInfo ItemInfo; + if (!GetItemInfoByDefinition(ItemDefinition, ItemInfo)) + { + break; + } + + LastItemInfoRemoved = RemoveItem(ItemInfo); + + AmountRemoved += LastItemInfoRemoved.Amount; + AmountToRemove = Amount - AmountRemoved; + + if (AmountToRemove == 0) + { + break; + } + } + + return FGIS_ItemInfo(AmountRemoved, LastItemInfoRemoved); +} + +void UGIS_InventorySystemComponent::RemoveAllItems(bool RemoveItemsFromIgnoredCollections, bool DisableEventsWhileRemoving) +{ + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + TObjectPtr ItemCollection = CollectionContainer.Entries[i].Instance; + if (RemoveItemsFromIgnoredCollections == false && IsIgnoredCollection(ItemCollection)) + { + continue; + } + ItemCollection->RemoveAll(); + } +} + +int32 UGIS_InventorySystemComponent::GetItemAmount(UGIS_ItemInstance* Item, bool SimilarItem) const +{ + if (Item == nullptr) + { + return 0; + } + + int32 Amount = 0; + + for (int i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + + Amount += CollectionContainer.Entries[i].Instance->GetItemAmount(Item); + } + + return Amount; +} + +int32 UGIS_InventorySystemComponent::GetItemAmountByDefinition(TSoftObjectPtr ItemDefinition, bool Unique) const +{ + int32 Amount = 0; + for (int i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + + Amount += CollectionContainer.Entries[i].Instance->GetItemAmount(ItemDefinition, Unique); + } + + return Amount; +} + +bool UGIS_InventorySystemComponent::GetItemInfoInCollection(UGIS_ItemInstance* Item, const FGameplayTag CollectionTag, FGIS_ItemInfo& OutItemInfo) const +{ + if (UGIS_ItemCollection* Collection = GetCollectionByTag(CollectionTag)) + { + return Collection->GetItemInfo(Item, OutItemInfo); + } + return false; +} + +bool UGIS_InventorySystemComponent::FindItemInfoInCollection(UGIS_ItemInstance* Item, const FGameplayTag CollectionTag, FGIS_ItemInfo& OutItemInfo) const +{ + return GetItemInfoInCollection(Item, CollectionTag, OutItemInfo); +} + +bool UGIS_InventorySystemComponent::GetAllItemInfosInCollection(const FGameplayTag CollectionTag, TArray& OutItemInfos) const +{ + if (UGIS_ItemCollection* Collection = GetCollectionByTag(CollectionTag)) + { + OutItemInfos = Collection->GetAllItemInfos(); + return OutItemInfos.Num() != 0; + } + return false; +} + +bool UGIS_InventorySystemComponent::FindAllItemInfosInCollection(const FGameplayTag CollectionTag, TArray& OutItemInfos) const +{ + return GetAllItemInfosInCollection(CollectionTag, OutItemInfos); +} + +bool UGIS_InventorySystemComponent::GetItemInfo(UGIS_ItemInstance* Item, FGIS_ItemInfo& ItemInfo) const +{ + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + if (CollectionContainer.Entries[i].Instance->GetItemInfo(Item, ItemInfo)) + { + return true; + } + } + return false; +} + +bool UGIS_InventorySystemComponent::FindItemInfo(UGIS_ItemInstance* Item, FGIS_ItemInfo& ItemInfo) const +{ + return GetItemInfo(Item, ItemInfo); +} + +bool UGIS_InventorySystemComponent::GetItemInfoByDefinition(const TSoftObjectPtr ItemDefinition, FGIS_ItemInfo& OutItemInfo) const +{ + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + if (CollectionContainer.Entries[i].Instance->GetItemInfoByDefinition(ItemDefinition, OutItemInfo)) + { + return OutItemInfo.IsValid(); + } + } + + return false; +} + +bool UGIS_InventorySystemComponent::FindItemInfoByDefinition(const TSoftObjectPtr ItemDefinition, FGIS_ItemInfo& OutItemInfo) const +{ + return GetItemInfoByDefinition(ItemDefinition, OutItemInfo); +} + +bool UGIS_InventorySystemComponent::GetItemInfosByDefinition(const TSoftObjectPtr ItemDefinition, TArray& OutItemInfos) const +{ + if (ItemDefinition.IsNull()) + { + return false; + } + + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + TArray ItemInfos; + if (CollectionContainer.Entries[i].Instance->GetItemInfosByDefinition(ItemDefinition, ItemInfos)) + { + ItemInfos.Append(ItemInfos); + } + } + + return !OutItemInfos.IsEmpty(); +} + +bool UGIS_InventorySystemComponent::FindItemInfosByDefinition(const TSoftObjectPtr ItemDefinition, TArray& OutItemInfos) const +{ + return GetItemInfosByDefinition(ItemDefinition, OutItemInfos); +} + +TArray UGIS_InventorySystemComponent::GetItemInfos() const +{ + TArray Ret; + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (IsIgnoredCollection(CollectionContainer.Entries[i].Instance)) + { + continue; + } + + TArray AllItemInfos = CollectionContainer.Entries[i].Instance->GetAllItemInfos(); + Ret.Append(AllItemInfos); + } + return Ret; +} + +bool UGIS_InventorySystemComponent::HasEnoughItem(const TSoftObjectPtr ItemDefinition, int32 Amount) const +{ + TArray OutItemInfos; + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + UGIS_ItemCollection* Collection = CollectionContainer.Entries[i].Instance; + TArray OutItemInfosTemp; + Collection->GetItemInfosByDefinition(ItemDefinition, OutItemInfosTemp); + OutItemInfos.Append(OutItemInfosTemp); + } + if (OutItemInfos.Num() >= Amount) + { + return true; + } + for (const auto& It : OutItemInfos) + { + if (It.Amount >= Amount) + { + return true; + } + } + return false; +} + +TArray UGIS_InventorySystemComponent::GetItemCollections() const +{ + TArray Ret; + Ret.Reserve(CollectionContainer.Entries.Num()); + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + Ret.Add(CollectionContainer.Entries[i].Instance); + } + return Ret; +} + +bool UGIS_InventorySystemComponent::IsDefaultCollectionCreated() const +{ + for (auto& Definition : CollectionDefinitions) + { + bool bFound = false; + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + const FGIS_CollectionEntry& Entry = CollectionContainer.Entries[i]; + if (Entry.IsValidEntry() && Entry.Definition == Definition && Entry.Instance->GetCollectionTag() == Definition->CollectionTag) + { + bFound = true; + break; + } + } + if (!bFound) + { + return false; + } + } + return true; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::GetDefaultCollection() const +{ + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + auto& Instance = CollectionContainer.Entries[i].Instance; + if (Instance && Instance->GetCollectionTag().MatchesTagExact(GIS_CollectionTags::Main)) + { + return Instance; + } + } + return nullptr; +} + +int32 UGIS_InventorySystemComponent::GetCollectionCount() const +{ + return CollectionContainer.Entries.Num(); +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::GetCollectionByTag(const FGameplayTag CollectionTag) const +{ + if (!CollectionTag.IsValid()) + { + return nullptr; + } + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + auto& Instance = CollectionContainer.Entries[i].Instance; + + if (Instance && Instance->GetCollectionTag() == CollectionTag) + { + return Instance; + } + } + return nullptr; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::GetCollectionByTags(FGameplayTagContainer Tags) +{ + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + if (CollectionContainer.Entries[i].Instance && CollectionContainer.Entries[i].Instance->GetCollectionTag().MatchesAnyExact(Tags)) + { + return CollectionContainer.Entries[i].Instance; + } + } + return nullptr; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::GetCollectionById(FGuid CollectionId) const +{ + if (CollectionIdToInstanceMap.Contains(CollectionId)) + { + return CollectionIdToInstanceMap[CollectionId]; + } + return nullptr; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::GetTypedCollectionByTag(const FGameplayTag CollectionTag, TSubclassOf DesiredClass) const +{ + if (UClass* RealClass = DesiredClass) + { + if (UGIS_ItemCollection* Collection = GetCollectionByTag(CollectionTag)) + { + if (Collection->GetClass() == RealClass) + { + return Collection; + } + } + } + return nullptr; +} + +bool UGIS_InventorySystemComponent::FindTypedCollectionByTag(const FGameplayTag CollectionTag, TSubclassOf DesiredClass, UGIS_ItemCollection*& OutCollection) +{ + if (UClass* RealClass = DesiredClass) + { + if (UGIS_ItemCollection* Collection = GetCollectionByTag(CollectionTag)) + { + if (Collection->GetClass() == RealClass) + { + OutCollection = Collection; + return true; + } + } + } + return false; +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::AddCollectionByDefinition(TSoftObjectPtr CollectionDefinition) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "Has no authority!") + return nullptr; + } + + if (CollectionDefinition.IsNull()) + { + GIS_CLOG(Warning, "Try to add collection with invalid definition.") + return nullptr; + } + + const UGIS_ItemCollectionDefinition* Definition = CollectionDefinition.LoadSynchronous(); + if (!IsValid(Definition)) + { + GIS_CLOG(Warning, "Try to add collection with invalid definition.") + return nullptr; + } + + UGIS_ItemCollection* NewCollection = CreateCollectionInstance(Definition); + + check(NewCollection) + + FGIS_CollectionEntry NewEntry; + NewEntry.Id = FGuid::NewGuid(); + NewEntry.Instance = NewCollection; + NewEntry.Definition = Definition; + if (AddCollectionEntry(NewEntry)) + { + return NewEntry.Instance; + } + return nullptr; +} + +bool UGIS_InventorySystemComponent::AddCollectionEntry(const FGIS_CollectionEntry& NewEntry) +{ + if (!NewEntry.IsValidEntry()) + { + return false; + } + if (NewEntry.Instance && NewEntry.Instance->IsInitialized()) + { + GIS_CLOG(Warning, "Try to add already initialized collection.") + return false; + } + + FGIS_CollectionEntry& AddedEntry = CollectionContainer.Entries.Add_GetRef(NewEntry); + if (GetOwnerRole() >= ROLE_Authority && IsReadyForReplication()) + { + if (!IsReplicatedSubObjectRegistered(AddedEntry.Instance)) + { + AddReplicatedSubObject(AddedEntry.Instance); + } + } + OnCollectionAdded(NewEntry); + CollectionContainer.MarkItemDirty(AddedEntry); + GetOwner()->ForceNetUpdate(); + return true; +} + +void UGIS_InventorySystemComponent::RemoveCollectionEntry(int32 Idx) +{ + if (CollectionContainer.Entries.IsValidIndex(Idx)) + { + const FGIS_CollectionEntry& Entry = CollectionContainer.Entries[Idx]; + if (IsValid(Entry.Instance) && Entry.Instance->IsInitialized()) + { + OnCollectionRemoved(Entry); + RemoveReplicatedSubObject(Entry.Instance); + CollectionContainer.Entries.RemoveAt(Idx); + CollectionContainer.MarkArrayDirty(); + } + } +} + +UGIS_ItemCollection* UGIS_InventorySystemComponent::CreateCollectionInstance(const UGIS_ItemCollectionDefinition* CollectionDefinition) +{ + if (!IsValid(CollectionDefinition)) + { + GIS_CLOG(Warning, "Try to add collection with invalid definition.") + return nullptr; + } + TSubclassOf CollectionClass = CollectionDefinition->GetCollectionInstanceClass(); + if (CollectionClass == nullptr) + { + GIS_CLOG(Warning, "definition(%s) doesn't specify valid item collection class.", *CollectionDefinition->GetName()) + return nullptr; + } + UGIS_ItemCollection* NewCollection = NewObject(GetOwner(), CollectionClass); + if (NewCollection == nullptr) + { + GIS_CLOG(Error, "failed to create instance of %s", *GetNameSafe(CollectionDefinition)) + return nullptr; + } + return NewCollection; +} + +UGIS_CurrencySystemComponent* UGIS_InventorySystemComponent::GetCurrencySystem() const +{ + return CurrencySystem; +} + + +void UGIS_InventorySystemComponent::ReadyForReplication() +{ + Super::ReadyForReplication(); + + // Register existing Item Collections. + if (IsUsingRegisteredSubObjectList()) + { + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + auto& Instance = CollectionContainer.Entries[i].Instance; + if (IsValid(Instance)) + { + if (!IsReplicatedSubObjectRegistered(Instance)) + { + AddReplicatedSubObject(Instance); + } + for (const FGIS_ItemStack& ItemStack : Instance->GetAllItemStacks()) + { + if (ItemStack.Item == nullptr) + { + continue; + } + if (!IsReplicatedSubObjectRegistered(ItemStack.Item)) + { + AddReplicatedSubObject(ItemStack.Item); + } + } + } + } + } +} + +void UGIS_InventorySystemComponent::LoadDefaultLoadouts() +{ + if (!GetOwner()->HasAuthority()) + { + return; + } + for (int32 i = 0; i < DefaultLoadouts.Num(); i++) + { + const FGIS_DefaultLoadout& Loadout = DefaultLoadouts[i]; + if (!Loadout.Tag.IsValid()) + { + continue; + } + for (const FGIS_ItemDefinitionAmount& DefaultItem : Loadout.DefaultItems) + { + if (DefaultItem.Amount <= 0) + { + continue; + } + + if (DefaultItem.Definition.IsNull()) + { + continue; + } + UGIS_ItemInstance* Item = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(Cast(GetOuter()), DefaultItem.Definition); + if (Item == nullptr) + { + GIS_CLOG(Warning, "Failed to add DefaultLoadout{Definition:%s}", *DefaultItem.Definition.ToString()) + continue; + } + FGIS_ItemInfo Info; + Info.Item = Item; + Info.Amount = DefaultItem.Amount; + Info.CollectionTag = Loadout.Tag; + AddItem(Info); + } + } +} + +#pragma region InitState + +// FName UGIS_InventorySystemComponent::GetFeatureName() const +// { +// return NAME_ActorFeatureName; +// } +// +// bool UGIS_InventorySystemComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const +// { +// check(Manager); +// AActor* Owner = GetOwner(); +// if (!CurrentState.IsValid() && DesiredState == GIS_InventoryInitState::Spawned) +// { +// // As long as we are on a valid actor, we count as spawned +// if (IsValid(Owner)) +// { +// return true; +// } +// } +// if (CurrentState == GIS_InventoryInitState::Spawned && DesiredState == GIS_InventoryInitState::DataAvailable) +// { +// // requires controller if owner is pawn. +// if (APawn* Pawn = Cast(Owner)) +// { +// // The player state is required. +// if (!Pawn->GetController()) +// { +// return false; +// } +// } +// +// // requires pawn if owner is player state. +// if (APlayerState* OwningPlayerState = Cast(Owner)) +// { +// if (!OwningPlayerState->GetPawn()) +// { +// return false; +// } +// } +// +// return true; +// } +// +// if (CurrentState == GIS_InventoryInitState::DataAvailable && DesiredState == GIS_InventoryInitState::DataInitialized) +// { +// for (const auto& Definition : CollectionDefinitions) +// { +// bool bFound = false; +// +// for (const FGIS_CollectionEntry& Entry : CollectionContainer.Entries) +// { +// if (IsValid(Entry.Instance) && IsValid(Entry.Definition) && Entry.Definition == Definition) +// { +// bFound = true; +// break; +// } +// } +// if (!bFound) +// { +// return false; +// } +// } +// return true; +// } +// +// if (CurrentState == GIS_InventoryInitState::DataInitialized && DesiredState == GIS_InventoryInitState::GameplayReady) +// { +// return true; +// } +// +// return false; +// } +// +// void UGIS_InventorySystemComponent::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) +// { +// if (CurrentState == GIS_InventoryInitState::Spawned && DesiredState == GIS_InventoryInitState::DataAvailable) +// { +// if (GetOwner()->HasAuthority()) +// { +// InitializeInventorySystem(); +// } +// } +// if (CurrentState == GIS_InventoryInitState::DataInitialized && DesiredState == GIS_InventoryInitState::GameplayReady) +// { +// if (GetOwner()->HasAuthority()) +// { +// LoadDefaultLoadouts(); +// } +// } +// CurrentInitState = DesiredState; +// } +// +// void UGIS_InventorySystemComponent::OnActorInitStateChanged(const FActorInitStateChangedParams& Params) +// { +// } +// +// bool UGIS_InventorySystemComponent::HasReachedInitState(FGameplayTag State) const +// { +// return IGameFrameworkInitStateInterface::HasReachedInitState(State); +// } +// +// void UGIS_InventorySystemComponent::CheckDefaultInitialization() +// { +// // Before checking our progress, try progressing any other features we might depend on +// CheckDefaultInitializationForImplementers(); +// +// static const TArray StateChain = { +// GIS_InventoryInitState::Spawned, GIS_InventoryInitState::DataAvailable, GIS_InventoryInitState::DataInitialized, GIS_InventoryInitState::GameplayReady +// }; +// +// // This will try to progress from spawned (which is only set in BeginPlay) through the data initialization stages until it gets to gameplay ready +// ContinueInitStateChain(StateChain); +// } +// +// void UGIS_InventorySystemComponent::CheckInventoryInitialization() +// { +// CheckDefaultInitialization(); +// } + +#pragma endregion + +void UGIS_InventorySystemComponent::ServerLoadDefaultLoadouts_Implementation() +{ + ServerLoadDefaultLoadouts(); +} + +void UGIS_InventorySystemComponent::OnInventorySystemInitialized_Implementation() +{ + OnInventorySystemInitializedEvent.Broadcast(); + + TArray Delegates = InitializedDelegates; + for (FGIS_InventorySystem_Initialized_DynamicEvent Delegate : Delegates) + { + Delegate.ExecuteIfBound(); + } + InitializedDelegates.Empty(); +} + +void UGIS_InventorySystemComponent::InitializeInventorySystem() +{ + if (bInventorySystemInitialized || !GetOwner()->HasAuthority()) + { + GIS_CLOG(Verbose, "already initialized or has no authority!"); + return; + } + CollectionIdToInstanceMap.Empty(); + PendingCollections.Empty(); + for (int32 i = 0; i < CollectionDefinitions.Num(); i++) + { + if (CollectionDefinitions[i] == nullptr || !CollectionDefinitions[i]->CollectionTag.IsValid()) + { + GIS_CLOG(Error, "default collection definition at index(%d) is invalid or mising collection tag!", i); + continue; + } + if (AddCollectionByDefinition(CollectionDefinitions[i]) == nullptr) + { + GIS_CLOG(Warning, "failed to initialize collection:%s", *GetNameSafe(CollectionDefinitions[i])) + } + } + LoadDefaultLoadouts(); + + bInventorySystemInitialized = true; + OnInventorySystemInitialized(); +} + +void UGIS_InventorySystemComponent::InitializeInventorySystemWithRecord(const FGIS_InventoryRecord& InventoryRecord) +{ + if (bInventorySystemInitialized || !GetOwner()->HasAuthority()) + { + GIS_CLOG(Verbose, "already initialized or has no authority!"); + return; + } + + if (!InventoryRecord.IsValid()) + { + GIS_CLOG(Verbose, "provided invalid records"); + return; + } + + UGIS_InventorySubsystem* Subsystem = UGIS_InventorySubsystem::Get(GetWorld()); + if (Subsystem == nullptr) + { + GIS_CLOG(Verbose, "missing inventory sub system"); + return; + } + + CollectionIdToInstanceMap.Empty(); + PendingCollections.Empty(); + + ResetInventorySystem(); + + Subsystem->DeserializeInventory(this, InventoryRecord); + + bInventorySystemInitialized = true; + OnInventorySystemInitialized(); +} + +void UGIS_InventorySystemComponent::ResetInventorySystem() +{ + if (!bInventorySystemInitialized || !GetOwner()->HasAuthority()) + { + GIS_CLOG(Verbose, "not initialized or has no authority!"); + return; + } + + //remove all items inside collections. + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + auto& Instance = CollectionContainer.Entries[i].Instance; + if (IsValid(Instance) && Instance->IsInitialized()) + { + Instance->RemoveAll(); + } + } + + //remove all remaining collection itself. + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + RemoveCollectionEntry(i); + } + + bInventorySystemInitialized = false; +} + +bool UGIS_InventorySystemComponent::IsInventoryInitialized() const +{ + return bInventorySystemInitialized; +} + +void UGIS_InventorySystemComponent::BindToInventorySystemInitialized(FGIS_InventorySystem_Initialized_DynamicEvent Delegate) +{ + if (bInventorySystemInitialized) + { + Delegate.ExecuteIfBound(); + } + else + { + InitializedDelegates.Add(Delegate); + } +} + +void UGIS_InventorySystemComponent::OnRegister() +{ + Super::OnRegister(); + + // // Register with the init state system early, this will only work if this is a game world + // if (bUseInitStateChain) + // { + // RegisterInitStateFeature(); + // } +} + +void UGIS_InventorySystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + if (GetWorld() && !GetWorld()->IsGameWorld()) + { + return; + } + + CollectionContainer.OwningComponent = this; + + if (!GetOwner()->IsUsingRegisteredSubObjectList()) + { + GIS_CLOG(Error, "requires enable bReplicateUsingRegisteredSubObjectList.") + } + + CurrencySystem = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner()); +} + +// Called when the game starts +void UGIS_InventorySystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + // Notifies state manager that we have spawned, then try rest of default initialization + // if (bUseInitStateChain) + // { + // ensure(TryToChangeInitState(GIS_InventoryInitState::Spawned)); + // CheckDefaultInitialization(); + // } + // else + // { + // if (bInitializeOnBeginplay && GetOwner()->HasAuthority()) + // { + // InitializeInventorySystem(); + // } + // } + + if (bInitializeOnBeginplay && GetOwner()->HasAuthority()) + { + InitializeInventorySystem(); + } +} + +bool UGIS_InventorySystemComponent::IsIgnoredCollection(UGIS_ItemCollection* ItemCollection) const +{ + if (ItemCollection == nullptr) + { + return true; + } + return IgnoredCollections.HasTagExact(ItemCollection->GetCollectionTag()); +} + +void UGIS_InventorySystemComponent::OnCollectionAdded(const FGIS_CollectionEntry& Entry) +{ + check(IsValid(Entry.Instance) && Entry.Id.IsValid()) + CollectionIdToInstanceMap.Add(Entry.Id, Entry.Instance); + Entry.Instance->SetInventory(this); + Entry.Instance->SetCollectionId(Entry.Id); + Entry.Instance->SetCollectionTag(Entry.Definition->CollectionTag); + Entry.Instance->SetDefinition(Entry.Definition); + OnCollectionAddedEvent.Broadcast(Entry.Instance); + GIS_CLOG(Verbose, "added collection:%s", *GetNameSafe(Entry.Instance->GetDefinition())) +} + +void UGIS_InventorySystemComponent::OnCollectionRemoved(const FGIS_CollectionEntry& Entry) +{ + check(IsValid(Entry.Instance) && Entry.Id.IsValid()) + + OnCollectionRemovedEvent.Broadcast(Entry.Instance); + // clear collection data. + Entry.Instance->SetInventory(nullptr); + // remove cache. + if (CollectionIdToInstanceMap.Contains(Entry.Id)) + { + CollectionIdToInstanceMap.Remove(Entry.Id); + } + Entry.Instance->PendingItemStacks.Empty(); + GIS_CLOG(Verbose, "removed collection:%s", *GetNameSafe(Entry.Instance->GetDefinition())) +} + +void UGIS_InventorySystemComponent::OnCollectionUpdated(const FGIS_CollectionEntry& Entry) +{ +} + +void UGIS_InventorySystemComponent::ProcessPendingCollections() +{ + if (HasBegunPlay() && GetOwner() != nullptr) + { + TArray Added; + for (const auto& Pending : PendingCollections) + { + if (Pending.Value.IsValidEntry()) + { + Added.AddUnique(Pending.Key); + } + } + + for (int32 i = 0; i < Added.Num(); i++) + { + FGuid AddedIndex = Added[i]; + const FGIS_CollectionEntry& AddedEntry = PendingCollections[AddedIndex]; + OnCollectionAdded(AddedEntry); + GIS_CLOG(Verbose, "added collection:%s from pending list.", *GetNameSafe(AddedEntry.Instance->GetDefinition())) + PendingCollections.Remove(AddedIndex); + } + + for (int32 i = 0; i < CollectionContainer.Entries.Num(); i++) + { + UGIS_ItemCollection* Collection = CollectionContainer.Entries[i].Instance; + if (IsValid(Collection) && Collection->IsInitialized()) + { + Collection->ProcessPendingItemStacks(); + } + } + } +} + +#if WITH_EDITOR + +void UGIS_InventorySystemComponent::PostLoad() +{ + Super::PostLoad(); +} + +void UGIS_InventorySystemComponent::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + for (FGIS_DefaultLoadout& DefaultLoadout : DefaultLoadouts) + { + TArray Amounts; + for (FGIS_ItemDefinitionAmount& DefaultItem : DefaultLoadout.DefaultItems) + { + DefaultItem.EditorFriendlyName = FString::Format(TEXT("{0}x {1}"), {DefaultItem.Amount, DefaultItem.Definition.IsNull() ? TEXT("Invalid Item") : DefaultItem.Definition.GetAssetName()}); + } + } +} + +EDataValidationResult UGIS_InventorySystemComponent::IsDataValid(FDataValidationContext& Context) const +{ + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemSettings.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemSettings.cpp new file mode 100644 index 0000000..2af9eb8 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventorySystemSettings.cpp @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GIS_InventorySystemSettings.h" +#include "GIS_InventoryFactory.h" +#include "Items/GIS_ItemDefinitionSchema.h" +#include "Misc/DataValidation.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventorySystemSettings) + +UGIS_InventorySystemSettings::UGIS_InventorySystemSettings() +{ + InventoryFactoryClass = UGIS_InventoryFactory::StaticClass(); +} + +FName UGIS_InventorySystemSettings::GetCategoryName() const +{ + return TEXT("Game"); +} + +const UGIS_InventorySystemSettings* UGIS_InventorySystemSettings::Get() +{ + return GetDefault(); +} + +const UGIS_ItemDefinitionSchema* UGIS_InventorySystemSettings::GetItemDefinitionSchemaForAsset(const FString& AssetPath) const +{ + // Check path-specific schemas first + for (const FGIS_ItemDefinitionSchemaEntry& Entry : ItemDefinitionSchemaMap) + { + if (!Entry.PathPrefix.IsEmpty() && AssetPath.StartsWith(Entry.PathPrefix)) + { + if (Entry.Schema.IsValid()) + { + if (UGIS_ItemDefinitionSchema* Schema = Cast(Entry.Schema.TryLoad())) + { + UE_LOG(LogTemp, Log, TEXT("Using path-specific schema %s for asset %s"), *Entry.Schema.ToString(), *AssetPath); + return Schema; + } + } + } + } + + // Fall back to default schema + if (DefaultItemDefinitionSchema.IsValid()) + { + if (UGIS_ItemDefinitionSchema* Schema = Cast(DefaultItemDefinitionSchema.TryLoad())) + { + UE_LOG(LogTemp, Log, TEXT("Using default schema %s for asset %s"), *DefaultItemDefinitionSchema.ToString(), *AssetPath); + return Schema; + } + } + + UE_LOG(LogTemp, Warning, TEXT("No valid schema found for asset %s"), *AssetPath); + return nullptr; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryTags.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryTags.cpp new file mode 100644 index 0000000..373cd04 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_InventoryTags.cpp @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_InventoryTags.h" + +namespace GIS_CollectionTags +{ + UE_DEFINE_GAMEPLAY_TAG(Main, "GIS.Collection.Main"); + UE_DEFINE_GAMEPLAY_TAG(Equipped, "GIS.Collection.Equipped"); + UE_DEFINE_GAMEPLAY_TAG(Hidden, "GIS.Collection.Hidden"); + UE_DEFINE_GAMEPLAY_TAG(QuickBar, "GIS.Collection.QuickBar"); +} + +namespace GIS_InventoryInitState +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Spawned, "GIS.InitState.Spawned", "1: Actor/component has initially spawned and can be extended"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(DataAvailable, "GIS.InitState.DataAvailable", "2: All required data has been loaded/replicated and is ready for initialization"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(DataInitialized, "GIS.InitState.DataInitialized", "3: The available data has been initialized for this actor/component, but it is not ready for full gameplay"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(GameplayReady, "GIS.InitState.GameplayReady", "4: The actor/component is fully ready for active gameplay"); +} + +// namespace GIS_MessageTags +// { +// UE_DEFINE_GAMEPLAY_TAG(ItemStackUpdate, "GIS.Message.StackUpdate") +// UE_DEFINE_GAMEPLAY_TAG(InventoryUpdate, "GIS.Message.Inventory.Update") +// UE_DEFINE_GAMEPLAY_TAG(CollectionUpdate, "GIS.Message.Inventory.Collection.Update") +// UE_DEFINE_GAMEPLAY_TAG(InventoryAddItemInfo, "GIS.Message.Inventory.AddItemInfo") +// UE_DEFINE_GAMEPLAY_TAG(InventoryAddItemInfoRejected, "GIS.Message.Inventory.AddItemInfo.Rejected") +// UE_DEFINE_GAMEPLAY_TAG(InventoryRemoveItemInfo, "GIS.Message.Inventory.RemoveItemInfo") +// UE_DEFINE_GAMEPLAY_TAG(QuickBarSlotsChanged, "GIS.Message.QuickBar.SlotsChanged") +// UE_DEFINE_GAMEPLAY_TAG(QuickBarActiveIndexChanged, "GIS.Message.QuickBar.ActiveIndexChanged") +// } + +namespace GIS_AttributeTags +{ + UE_DEFINE_GAMEPLAY_TAG(Dummy, "GIS.Attribute.Dummy"); + // UE_DEFINE_GAMEPLAY_TAG(EnhancedLevel, "GIS.Attribute.EnhancedLevel"); + // UE_DEFINE_GAMEPLAY_TAG(MaxEnhancedLevel, "GIS.Attribute.MaxEnhancedLevel"); + UE_DEFINE_GAMEPLAY_TAG(StackSizeLimit, "GIS.Attribute.StackSizeLimit"); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_LogChannels.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_LogChannels.cpp new file mode 100644 index 0000000..df0a35c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GIS_LogChannels.cpp @@ -0,0 +1,77 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_LogChannels.h" + +#include "GIS_EquipmentInstance.h" +#include "UObject/Object.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" +#include "GIS_ItemInstance.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemDefinition.h" + +DEFINE_LOG_CATEGORY(LogGIS) + + +FString GetGISLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + if (AActor* ActorOwner = Cast(Component->GetOuter())) + { + Role = ActorOwner->GetLocalRole(); + Name = ActorOwner->GetName(); + } + else + { + const AActor* Owner = Component->GetOwner(); + Role = IsValid(Owner) ? Owner->GetLocalRole() : ROLE_None; + Name = IsValid(Owner) ? Owner->GetName() : TEXT("None"); + } + } + else if (const UGIS_ItemInstance* ItemInstance = Cast(ContextObject)) + { + if (AActor* ActorOwner = Cast(ItemInstance->GetOuter())) + { + Role = ActorOwner->GetLocalRole(); + Name = ActorOwner->GetName(); + } + else + { + return FString::Printf(TEXT("(%s)'s instance(%s) "), *ItemInstance->GetDefinition()->GetName(), *ItemInstance->GetName()); + } + } + else if (const UGIS_ItemCollection* Collection = Cast(ContextObject)) + { + if (AActor* ActorOwner = Cast(Collection->GetOuter())) + { + Role = ActorOwner->GetLocalRole(); + Name = ActorOwner->GetName(); + } + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)'s %s"), *RoleName, *Name, *Collection->GetCollectionName()); + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/GenericInventorySystem.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/GenericInventorySystem.cpp new file mode 100644 index 0000000..45ca821 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/GenericInventorySystem.cpp @@ -0,0 +1,22 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericInventorySystem.h" + +#define LOCTEXT_NAMESPACE "FGenericInventorySystemModule" + +void FGenericInventorySystemModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +} + +void FGenericInventorySystemModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericInventorySystemModule, GenericInventorySystem) \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_CurrencyPickupComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_CurrencyPickupComponent.cpp new file mode 100644 index 0000000..6caa7ab --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_CurrencyPickupComponent.cpp @@ -0,0 +1,53 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Pickups/GIS_CurrencyPickupComponent.h" +#include "GIS_CurrencySystemComponent.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyPickupComponent) + +void UGIS_CurrencyPickupComponent::BeginPlay() +{ + OwningCurrencySystem = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner()); + if (OwningCurrencySystem == nullptr) + { + GIS_CLOG(Warning, "Mising CurrencySystemComponent!"); + } + Super::BeginPlay(); +} + +bool UGIS_CurrencyPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "has no authority!"); + return false; + } + if (OwningCurrencySystem == nullptr || !IsValid(OwningCurrencySystem)) + { + GIS_CLOG(Warning, "mising CurrencySystemComponent!"); + return false; + } + if (Picker == nullptr || !IsValid(Picker)) + { + GIS_CLOG(Warning, "passed-in invalid picker."); + return false; + } + + UGIS_CurrencySystemComponent* PickerCurrencySystem = Picker->GetCurrencySystem(); + if (PickerCurrencySystem == nullptr) + { + GIS_CLOG(Warning, "Picker:%s has no CurrencySystem!", *Picker->GetOwner()->GetName()); + return false; + } + + if (PickerCurrencySystem->AddCurrencies(OwningCurrencySystem->GetAllCurrencies())) + { + OwningCurrencySystem->EmptyCurrencies(); + return true; + } + return false; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_InventoryPickupComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_InventoryPickupComponent.cpp new file mode 100644 index 0000000..867cf44 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_InventoryPickupComponent.cpp @@ -0,0 +1,91 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Pickups/GIS_InventoryPickupComponent.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_InventoryTags.h" +#include "GIS_ItemCollection.h" +#include "GIS_LogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryPickupComponent) + +void UGIS_InventoryPickupComponent::BeginPlay() +{ + if (!CollectionTag.IsValid()) + { + CollectionTag = GIS_CollectionTags::Main; + } + + Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); + if (!Inventory) + { + GIS_CLOG(Warning, "InventoryPickup requries an inventory system component on the same actor!") + } + + Super::BeginPlay(); +} + +bool UGIS_InventoryPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "has no authority!"); + return false; + } + if (Inventory == nullptr || !IsValid(Inventory)) + { + GIS_CLOG(Warning, "doesn't have an inventory system component to function.") + return false; + } + if (!CollectionTag.IsValid() || !IsValid(Picker)) + { + GIS_CLOG(Warning, "doesn't have valid picker to function.") + return false; + } + + UGIS_ItemCollection* DestCollection = Picker->GetCollectionByTag(CollectionTag); + if (DestCollection == nullptr) + { + GIS_CLOG(Warning, "picker(%s) doesn't have valid collection named:%s", *Picker->GetOwner()->GetName(), *CollectionTag.ToString()); + return false; + } + + return AddPickupToCollection(DestCollection); +} + +UGIS_InventorySystemComponent* UGIS_InventoryPickupComponent::GetOwningInventory() const +{ + return Inventory; +} + +bool UGIS_InventoryPickupComponent::AddPickupToCollection(UGIS_ItemCollection* DestCollection) +{ + TArray PickupItems = Inventory->GetDefaultCollection()->GetAllItemInfos(); + bool bAtLeastOneCanBeAdded = false; + for (int32 i = 0; i < PickupItems.Num(); i++) + { + FGIS_ItemInfo ItemInfo = PickupItems[i]; + FGIS_ItemInfo CanAddedItemInfo; + if (DestCollection->CanAddItem(ItemInfo, CanAddedItemInfo)) + { + if (CanAddedItemInfo.Amount != 0) + { + bAtLeastOneCanBeAdded = true; + } + } + } + if (bAtLeastOneCanBeAdded == false) + { + NotifyPickupFailed(); + return false; + } + + for (int32 i = 0; i < PickupItems.Num(); i++) + { + DestCollection->AddItem(PickupItems[i]); + } + Inventory->GetDefaultCollection()->RemoveAll(); + NotifyPickupSuccess(); + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_ItemPickupComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_ItemPickupComponent.cpp new file mode 100644 index 0000000..42a6545 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_ItemPickupComponent.cpp @@ -0,0 +1,87 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Pickups/GIS_ItemPickupComponent.h" +#include "GameFramework/Actor.h" +#include "GIS_InventoryTags.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_LogChannels.h" +#include "Pickups/GIS_WorldItemComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemPickupComponent) + +bool UGIS_ItemPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker) +{ + if (!GetOwner()->HasAuthority()) + { + GIS_CLOG(Warning, "has no authority!"); + return false; + } + if (!CollectionTag.IsValid() || !IsValid(Picker)) + { + GIS_CLOG(Warning, "passed-in invalid picker."); + return false; + } + if (WorldItemComponent == nullptr && WorldItemComponent->GetItemInstance()->IsItemValid()) + { + GIS_CLOG(Warning, "doesn't have valid WordItem component attached or it has invalid item instance reference."); + return false; + } + + return TryAddToCollection(Picker); +} + +UGIS_WorldItemComponent* UGIS_ItemPickupComponent::GetWorldItem() const +{ + return WorldItemComponent; +} + +// Called when the game starts +void UGIS_ItemPickupComponent::BeginPlay() +{ + if (!CollectionTag.IsValid()) + { + CollectionTag = GIS_CollectionTags::Main; + } + + Super::BeginPlay(); + WorldItemComponent = GetOwner()->FindComponentByClass(); + + if (WorldItemComponent == nullptr) + { + GIS_CLOG(Error, "requires GIS_WorldItemComponent to function!") + } +} + +bool UGIS_ItemPickupComponent::TryAddToCollection(UGIS_InventorySystemComponent* Picker) +{ + UGIS_ItemInstance* NewItemInstance = WorldItemComponent->GetDuplicatedItemInstance(Picker->GetOwner()); + + if (NewItemInstance == nullptr) + { + GIS_CLOG(Error, "referenced invalid item! Pickup failed!"); + NotifyPickupFailed(); + return false; + } + + FGIS_ItemInfo NewItemInfo; + NewItemInfo.Item = NewItemInstance; + NewItemInfo.Amount = WorldItemComponent->GetItemAmount(); + const FGameplayTag TargetCollection = CollectionTag.IsValid() ? CollectionTag : GIS_CollectionTags::Main; + NewItemInfo.CollectionTag = TargetCollection; + + FGIS_ItemInfo CanAddedItemInfo; + const bool bResult = Picker->CanAddItem(NewItemInfo, CanAddedItemInfo); + + if (!bResult || CanAddedItemInfo.Amount == 0 || (bFailIfFullAmountNotFit && CanAddedItemInfo.Amount != NewItemInfo.Amount)) + { + NotifyPickupFailed(); + return false; + } + + Picker->AddItem(NewItemInfo); + NotifyPickupSuccess(); + return true; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupActorInterface.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupActorInterface.cpp new file mode 100644 index 0000000..76c7d99 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupActorInterface.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Pickups/GIS_PickupActorInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_PickupActorInterface) diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupComponent.cpp new file mode 100644 index 0000000..bb92109 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_PickupComponent.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Pickups/GIS_PickupComponent.h" +#include "Sound/SoundBase.h" +#include "Kismet/GameplayStatics.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_PickupComponent) + +// Sets default values for this component's properties +UGIS_PickupComponent::UGIS_PickupComponent() +{ + PrimaryComponentTick.bStartWithTickEnabled = false; + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); +} + +bool UGIS_PickupComponent::Pickup(UGIS_InventorySystemComponent* Picker) +{ + return true; +} + +void UGIS_PickupComponent::NotifyPickupSuccess() +{ + OnPickupSuccess.Broadcast(); +} + +void UGIS_PickupComponent::NotifyPickupFailed() +{ + OnPickupFail.Broadcast(); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_WorldItemComponent.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_WorldItemComponent.cpp new file mode 100644 index 0000000..8ba042a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Pickups/GIS_WorldItemComponent.cpp @@ -0,0 +1,154 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Pickups/GIS_WorldItemComponent.h" +#include "UObject/Object.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" +#include "GIS_InventorySubsystem.h" +#include "GIS_InventorySystemComponent.h" +#include "Items/GIS_ItemDefinition.h" +#include "Items/GIS_ItemInstance.h" +#include "GIS_LogChannels.h" +#include "Net/UnrealNetwork.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_WorldItemComponent) + +UGIS_WorldItemComponent::UGIS_WorldItemComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + PrimaryComponentTick.bStartWithTickEnabled = false; + PrimaryComponentTick.bCanEverTick = false; + + SetIsReplicatedByDefault(true); + + bReplicateUsingRegisteredSubObjectList = true; +} + +void UGIS_WorldItemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ItemInfo, Parameters) +} + +UGIS_WorldItemComponent* UGIS_WorldItemComponent::GetWorldItemComponent(const AActor* Actor) +{ + if (IsValid(Actor)) + { + return Actor->FindComponentByClass(); + } + return nullptr; +} + +void UGIS_WorldItemComponent::CreateItemFromDefinition(FGIS_ItemDefinitionAmount ItemDefinition) +{ + if (!ItemDefinition.Definition.IsNull() && ItemDefinition.Amount >= 1) + { + if (!ItemInfo.IsValid()) + { + UGIS_ItemInstance* NewItemInstance = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(GetOwner(), ItemDefinition.Definition.LoadSynchronous()); + if (NewItemInstance == nullptr) + { + GIS_CLOG(Error, "failed to create item instance from definition!"); + } + else + { + SetItemInfo(NewItemInstance, ItemDefinition.Amount); + } + } + else + { + GIS_CLOG(Warning, "Already have valid item info, skip creation.") + } + } + else + { + GIS_CLOG(Error, "passed invalid definition setup,skip item instance creating!"); + } +} + +bool UGIS_WorldItemComponent::HasValidDefinition() const +{ + return !Definition.Definition.IsNull() && Definition.Amount >= 1; +} + + +void UGIS_WorldItemComponent::SetItemInfo(UGIS_ItemInstance* InItem, int32 InAmount) +{ + if (InItem == nullptr || InAmount <= 0) + { + return; + } + + if (ItemInfo.IsValid()) + { + GIS_CLOG(Warning, "Already have valid item info.") + return; + } + + ItemInfo = FGIS_ItemInfo(InItem, InAmount); + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ItemInfo, this) + + // add to ReplicatedSubObject list only if it was created by this component. + if (bReplicateUsingRegisteredSubObjectList && InItem->GetOuter() == GetOwner()) + { + AddReplicatedSubObject(ItemInfo.Item); + } +} + +void UGIS_WorldItemComponent::ResetItemInfo() +{ + if (ItemInfo.IsValid()) + { + // remove from replicated sub object list only if it was created by this component. + if (bReplicateUsingRegisteredSubObjectList && ItemInfo.Item->GetOuter() == GetOwner()) + { + RemoveReplicatedSubObject(ItemInfo.Item); + } + } +} + +UGIS_ItemInstance* UGIS_WorldItemComponent::GetItemInstance() +{ + return ItemInfo.Item; +} + +UGIS_ItemInstance* UGIS_WorldItemComponent::GetDuplicatedItemInstance(AActor* NewOwner) +{ + if (ItemInfo.IsValid()) + { + return UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(NewOwner, ItemInfo.Item); + } + return nullptr; +} + +FGIS_ItemInfo UGIS_WorldItemComponent::GetItemInfo() const +{ + return ItemInfo; +} + +int32 UGIS_WorldItemComponent::GetItemAmount() const +{ + return ItemInfo.Amount; +} + +void UGIS_WorldItemComponent::BeginPlay() +{ + if (HasValidDefinition() && GetOwner()->HasAuthority()) + { + CreateItemFromDefinition(Definition); + } + Super::BeginPlay(); +} + +void UGIS_WorldItemComponent::OnRep_ItemInfo() +{ + if (ItemInfo.IsValid()) + { + GIS_CLOG(Verbose, "item:%s replicated!", *ItemInfo.Item->GetDefinition()->GetName()); + ItemInfoSetEvent.Broadcast(ItemInfo); + } +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Private/Serialization/GIS_SerializationStructLibrary.cpp b/Plugins/GIS/Source/GenericInventorySystem/Private/Serialization/GIS_SerializationStructLibrary.cpp new file mode 100644 index 0000000..f6333bd --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Private/Serialization/GIS_SerializationStructLibrary.cpp @@ -0,0 +1,42 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GIS_SerializationStructLibrary.h" +#include "GIS_ItemFragment.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_SerializationStructLibrary) + +bool FGIS_ItemRecord::operator==(const FGIS_ItemRecord& Other) const +{ + return ItemId == Other.ItemId && DefinitionAssetPath == Other.DefinitionAssetPath; +} + +bool FGIS_ItemRecord::IsValid() const +{ + return ItemId.IsValid() && !DefinitionAssetPath.IsEmpty(); +} + +// bool FGIS_ItemFragmentStateRecord::operator==(const FGIS_ItemFragmentStateRecord& Other) const +// { +// return FragmentClass == Other.FragmentClass; +// } +// +// bool FGIS_ItemFragmentStateRecord::IsValid() const +// { +// return FragmentClass != nullptr && FragmentState.IsValid(); +// } + +bool FGIS_StackRecord::IsValid() const +{ + return ItemId.IsValid() && Id.IsValid() && CollectionId.IsValid(); +} + +bool FGIS_CollectionRecord::IsValid() const +{ + return Id.IsValid() && !DefinitionAssetPath.IsEmpty(); +} + +FGIS_CurrencyRecord::FGIS_CurrencyRecord() +{ + Key = NAME_None; +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_Wait.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_Wait.h new file mode 100644 index 0000000..d30dfa1 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_Wait.h @@ -0,0 +1,239 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_LogChannels.h" +#include "Engine/TimerHandle.h" +#include "Engine/Engine.h" +#include "UObject/Object.h" +#include "Engine/CancellableAsyncAction.h" +#include "GIS_AsyncAction_Wait.generated.h" + +/** + * Delegate triggered when an async wait action completes or is cancelled. + * 异步等待动作完成或取消时触发的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGIS_AsyncAction_WaitSignature); + +/** + * Base class for asynchronous wait actions on an actor. + * 在演员上执行异步等待动作的基类。 + * @details Provides functionality to wait for specific conditions on an actor, with timer-based checks. + * @细节 提供在演员上等待特定条件的功能,通过定时器检查。 + */ +UCLASS(Abstract) +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_Wait : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Constructor for the async wait action. + * 异步等待动作的构造函数。 + */ + UGIS_AsyncAction_Wait(); + + /** + * Delegate triggered when the wait action completes successfully. + * 等待动作成功完成时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_AsyncAction_WaitSignature OnCompleted; + + /** + * Delegate triggered when the wait action is cancelled. + * 等待动作取消时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_AsyncAction_WaitSignature OnCancelled; + + /** + * Gets the world associated with this action. + * 获取与此动作关联的世界。 + * @return The world object, or nullptr if not set. 世界对象,如果未设置则返回nullptr。 + */ + virtual UWorld* GetWorld() const override; + + /** + * Gets the target actor for the wait action. + * 获取等待动作的目标演员。 + * @return The target actor, or nullptr if not set. 目标演员,如果未设置则返回nullptr。 + */ + virtual AActor* GetActor() const; + + /** + * Activates the wait action, starting the timer. + * 激活等待动作,启动定时器。 + */ + virtual void Activate() override; + + /** + * Completes the wait action, triggering the OnCompleted delegate. + * 完成等待动作,触发OnCompleted委托。 + */ + virtual void Complete(); + + /** + * Cancels the wait action, triggering the OnCancelled delegate. + * 取消等待动作,触发OnCancelled委托。 + */ + virtual void Cancel() override; + + /** + * Determines whether delegates should be broadcast. + * 确定是否应广播委托。 + * @return True if delegates should be broadcast, false otherwise. 如果应广播委托则返回true,否则返回false。 + */ + virtual bool ShouldBroadcastDelegates() const override; + + /** + * Called when the target actor is destroyed. + * 目标演员销毁时调用。 + * @param DestroyedActor The actor that was destroyed. 被销毁的演员。 + */ + UFUNCTION() + virtual void OnTargetDestroyed(AActor* DestroyedActor); + +protected: + /** + * Creates a new wait action instance. + * 创建新的等待动作实例。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param TargetActor The target actor to wait for. 要等待的目标演员。 + * @param WaitInterval The interval between checks (in seconds). 检查间隔(以秒为单位)。 + * @param MaxWaitTimes The maximum number of checks before timeout (-1 for no limit). 最大检查次数,超时前(-1表示无限制)。 + * @return The created wait action, or nullptr if invalid parameters. 创建的等待动作,如果参数无效则返回nullptr。 + * @details Logs warnings if the world context, world, target actor, or wait interval is invalid. + * @细节 如果世界上下文、世界、目标演员或等待间隔无效,则记录警告。 + */ + template + static ActionType* CreateWaitAction(UObject* WorldContext, AActor* TargetActor, float WaitInterval, int32 MaxWaitTimes) + { + if (!IsValid(WorldContext)) + { + GIS_LOG(Warning, "invalid world context!") + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::LogAndReturnNull); + if (!IsValid(World)) + { + GIS_LOG(Warning, "can't get world from context:%s", *GetNameSafe(WorldContext)); + return nullptr; + } + + if (!IsValid(TargetActor)) + { + GIS_LOG(Warning, "invalid target actor."); + return nullptr; + } + + if (WaitInterval <= 0.f) + { + GIS_LOG(Warning, "WaitInterval %f must be greater than zero!", WaitInterval); + return nullptr; + } + + ActionType* NewAction = Cast(NewObject(GetTransientPackage(), ActionType::StaticClass())); + NewAction->SetWorld(World); + NewAction->SetTargetActor(TargetActor); + NewAction->SetWaitInterval(WaitInterval); + NewAction->SetMaxWaitTimes(MaxWaitTimes); + NewAction->RegisterWithGameInstance(World->GetGameInstance()); + return NewAction; + } + + /** + * Sets the world for the wait action. + * 设置等待动作的世界。 + * @param NewWorld The world to set. 要设置的世界。 + */ + void SetWorld(UWorld* NewWorld); + + /** + * Sets the target actor for the wait action. + * 设置等待动作的目标演员。 + * @param NewTargetActor The target actor to set. 要设置的目标演员。 + */ + void SetTargetActor(AActor* NewTargetActor); + + /** + * Sets the interval between checks. + * 设置检查间隔。 + * @param NewWaitInterval The interval (in seconds). 间隔(以秒为单位)。 + */ + void SetWaitInterval(float NewWaitInterval); + + /** + * Sets the maximum number of checks before timeout. + * 设置超时前的最大检查次数。 + * @param NewMaxWaitTimes The maximum number of checks (-1 for no limit). 最大检查次数(-1表示无限制)。 + */ + void SetMaxWaitTimes(int32 NewMaxWaitTimes); + + /** + * Stops the timer for the wait action. + * 停止等待动作的定时器。 + */ + void StopWaiting(); + + /** + * Called when the timer ticks to check the wait condition. + * 定时器触发时调用以检查等待条件。 + */ + UFUNCTION() + void OnTimer(); + + /** + * Cleans up resources used by the wait action. + * 清理等待动作使用的资源。 + */ + virtual void Cleanup(); + + /** + * Executes the specific wait condition check. + * 执行特定的等待条件检查。 + */ + UFUNCTION() + virtual void OnExecutionAction(); + +private: + /** + * Weak reference to the world for the wait action. + * 等待动作的世界的弱引用。 + */ + TWeakObjectPtr WorldPtr{nullptr}; + + /** + * Weak reference to the target actor for the wait action. + * 等待动作的目标演员的弱引用。 + */ + UPROPERTY() + TWeakObjectPtr TargetActorPtr{nullptr}; + + /** + * Handle for the timer used to check the wait condition. + * 用于检查等待条件的定时器句柄。 + */ + UPROPERTY() + FTimerHandle TimerHandle; + + /** + * Interval between checks (in seconds). + * 检查间隔(以秒为单位)。 + */ + float WaitInterval = 0.2f; + + /** + * Current number of checks performed. + * 当前执行的检查次数。 + */ + int32 WaitTimes = 0; + + /** + * Maximum number of checks before timeout (-1 for no limit). + * 超时前的最大检查次数(-1表示无限制)。 + */ + int32 MaxWaitTimes{-1}; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitEquipmentSystem.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitEquipmentSystem.h new file mode 100644 index 0000000..99adadc --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitEquipmentSystem.h @@ -0,0 +1,84 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_AsyncAction_Wait.h" +#include "GIS_AsyncAction_WaitEquipmentSystem.generated.h" + +class UGIS_EquipmentSystemComponent; + +/** + * Async action to wait for a valid equipment system component on an actor. + * 在演员上等待有效装备系统组件的异步动作。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitEquipmentSystem : public UGIS_AsyncAction_Wait +{ + GENERATED_BODY() + +public: + /** + * Waits for a valid equipment system component on the target actor. + * 在目标演员上等待有效的装备系统组件。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param TargetActor The target actor to wait for. 要等待的目标演员。 + * @return The created wait action. 创建的等待动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true")) + static UGIS_AsyncAction_WaitEquipmentSystem* WaitEquipmentSystem(UObject* WorldContext, AActor* TargetActor); + +protected: + /** + * Checks for the presence of a valid equipment system component. + * 检查是否存在有效的装备系统组件。 + */ + virtual void OnExecutionAction() override; +}; + +/** + * Async action to wait for a valid and initialized equipment system component on an actor. + * 在演员上等待有效且已初始化的装备系统组件的异步动作。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitEquipmentSystemInitialized : public UGIS_AsyncAction_WaitEquipmentSystem +{ + GENERATED_BODY() + +public: + /** + * Waits for a valid and initialized equipment system component on the target actor. + * 在目标演员上等待有效且已初始化的装备系统组件。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param TargetActor The target actor to wait for. 要等待的目标演员。 + * @return The created wait action. 创建的等待动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true")) + static UGIS_AsyncAction_WaitEquipmentSystem* WaitEquipmentSystemInitialized(UObject* WorldContext, AActor* TargetActor); + +protected: + /** + * Checks for the presence and initialization of the equipment system component. + * 检查装备系统组件的存在和初始化状态。 + */ + virtual void OnExecutionAction() override; + + /** + * Cleans up resources and event bindings. + * 清理资源和事件绑定。 + */ + virtual void Cleanup() override; + + /** + * Called when the equipment system component is initialized. + * 装备系统组件初始化时调用。 + */ + UFUNCTION() + virtual void OnSystemInitialized(); + + /** + * Weak reference to the equipment system component being waited for. + * 等待的装备系统组件的弱引用。 + */ + TWeakObjectPtr EquipmentSystemPtr; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitInventorySystem.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitInventorySystem.h new file mode 100644 index 0000000..d31446d --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitInventorySystem.h @@ -0,0 +1,83 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_AsyncAction_Wait.h" +#include "GIS_InventoryMeesages.h" +#include "GIS_AsyncAction_WaitInventorySystem.generated.h" + +/** + * Async action to wait for a valid inventory system component on an actor. + * 在演员上等待有效库存系统组件的异步动作。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitInventorySystem : public UGIS_AsyncAction_Wait +{ + GENERATED_BODY() + +public: + /** + * Waits for a valid inventory system component on the target actor. + * 在目标演员上等待有效的库存系统组件。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param TargetActor The target actor to wait for. 要等待的目标演员。 + * @return The created wait action. 创建的等待动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true")) + static UGIS_AsyncAction_WaitInventorySystem* WaitInventorySystem(UObject* WorldContext, AActor* TargetActor); + +protected: + /** + * Checks for the presence of a valid inventory system component. + * 检查是否存在有效的库存系统组件。 + */ + virtual void OnExecutionAction() override; +}; + +/** + * Async action to wait for a valid and initialized inventory system component on an actor. + * 在演员上等待有效且已初始化的库存系统组件的异步动作。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitInventorySystemInitialized : public UGIS_AsyncAction_WaitInventorySystem +{ + GENERATED_BODY() + +public: + /** + * Waits for a valid and initialized inventory system component on the target actor. + * 在目标演员上等待有效且已初始化的库存系统组件。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param TargetActor The target actor to wait for. 要等待的目标演员。 + * @return The created wait action. 创建的等待动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true")) + static UGIS_AsyncAction_WaitInventorySystem* WaitInventorySystemInitialized(UObject* WorldContext, AActor* TargetActor); + +protected: + /** + * Checks for the presence and initialization of the inventory system component. + * 检查库存系统组件的存在和初始化状态。 + */ + virtual void OnExecutionAction() override; + + /** + * Cleans up resources and event bindings. + * 清理资源和事件绑定。 + */ + virtual void Cleanup() override; + + /** + * Called when the inventory system component is initialized. + * 库存系统组件初始化时调用。 + */ + UFUNCTION() + virtual void OnSystemInitialized(); + + /** + * Weak reference to the inventory system component being waited for. + * 等待的库存系统组件的弱引用。 + */ + TWeakObjectPtr InventorySysPtr; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.h new file mode 100644 index 0000000..aa6760c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Async/GIS_AsyncAction_WaitItemFragmentDataChanged.h @@ -0,0 +1,56 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/CancellableAsyncAction.h" +#include "Templates/SubclassOf.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GIS_AsyncAction_WaitItemFragmentDataChanged.generated.h" + +class UGIS_ItemFragment; +class UGIS_ItemInstance; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGIS_WaitFragmentStateChangedSignature, const UGIS_ItemFragment*, Fragment, const FInstancedStruct&, Data); + + +/** + * Async action to wait for a fragment data changed on an item instance. + * 在道具实例上等待指定道具片段的运行时数据变更。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitItemFragmentDataChanged : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Wait for a fragment data changed on an item instance. + * 在道具实例上等待指定道具片段的运行时数据变更。 + * @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。 + * @param ItemInstance The target item instance to wait for. 要等待的目标道具。 + * @param FragmentClass The fragment type to wait for. 要等待的片段类型。 + * @return The created wait action. 创建的等待动作。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="ItemInstnace", BlueprintInternalUseOnly = "true")) + static UGIS_AsyncAction_WaitItemFragmentDataChanged* WaitItemFragmentStateChanged(UObject* WorldContext, UGIS_ItemInstance* ItemInstance, TSoftClassPtr FragmentClass); + + virtual void Activate() override; + virtual void Cancel() override; + + UPROPERTY(BlueprintAssignable, Category="GIS|Async") + FGIS_WaitFragmentStateChangedSignature OnStateChanged; + +protected: + UFUNCTION() + void OnFragmentStateChanged(const UGIS_ItemFragment* Fragment, const FInstancedStruct& State); + + TWeakObjectPtr ItemInstance; + + TSubclassOf FragmentClass; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagFloat.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagFloat.h new file mode 100644 index 0000000..5e5364f --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagFloat.h @@ -0,0 +1,259 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Interface.h" +#include "GameplayTagContainer.h" +#include "GIS_GameplayTagFloat.generated.h" + +struct FGIS_GameplayTagFloatContainer; + +/** + * Interface for objects that own a gameplay tag float container. + * 拥有游戏标签浮点容器对象的接口。 + */ +UINTERFACE(meta=(CannotImplementInterfaceInBlueprint)) +class GENERICINVENTORYSYSTEM_API UGIS_GameplayTagFloatContainerOwner : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface class for handling updates to float attributes in a gameplay tag container. + * 处理游戏标签容器中浮点属性更新的接口类。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_GameplayTagFloatContainerOwner +{ + GENERATED_BODY() + +public: + /** + * Called when a float attribute associated with a gameplay tag is updated. + * 当与游戏标签关联的浮点属性更新时调用。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + virtual void OnTagFloatUpdate(const FGameplayTag& Tag, float OldValue, float NewValue) = 0; +}; + +/** + * Represents a gameplay tag and float value pair. + * 表示一个游戏标签和浮点值的键值对。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagFloat : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * Default constructor for the gameplay tag float pair. + * 游戏标签浮点对的默认构造函数。 + */ + FGIS_GameplayTagFloat() + { + } + + /** + * Constructor for the gameplay tag float pair with initial values. + * 使用初始值构造游戏标签浮点对。 + * @param InTag The gameplay tag for the pair. 键值对的游戏标签。 + * @param InValue The float value for the pair. 键值对的浮点值。 + */ + FGIS_GameplayTagFloat(FGameplayTag InTag, float InValue) + : Tag(InTag) + , Value(InValue) + { + } + + /** + * Gets a debug string representation of the tag-value pair. + * 获取标签-值对的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString() const; + + friend FGIS_GameplayTagFloatContainer; + + /** + * The gameplay tag identifying the attribute. + * 标识属性的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGameplayTag Tag; + + /** + * The float value associated with the tag. + * 与标签关联的浮点值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + float Value = 0; + + /** + * The previous float value of the attribute (not replicated). + * 属性的前一个浮点值(不复制)。 + */ + UPROPERTY(NotReplicated) + float PrevValue = 0; +}; + +/** + * Container for storing gameplay tag float pairs. + * 存储游戏标签浮点对的容器。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagFloatContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for the float container. + * 浮点容器的默认构造函数。 + */ + FGIS_GameplayTagFloatContainer() + // : Owner(nullptr) + { + } + + /** + * Constructor for the float container with an owning object. + * 使用拥有对象构造浮点容器。 + * @param InObject The object that owns this container. 拥有此容器的对象。 + */ + FGIS_GameplayTagFloatContainer(UObject* InObject) + : ContainerOwner(InObject) + { + } + + /** + * Adds a new tag-value pair to the container. + * 向容器添加新的标签-值对。 + * @param Tag The gameplay tag to add. 要添加的游戏标签。 + * @param Value The float value to associate with the tag. 与标签关联的浮点值。 + */ + void AddItem(FGameplayTag Tag, float Value); + + /** + * Sets the value for an existing tag or adds a new tag-value pair. + * 为现有标签设置值或添加新的标签-值对。 + * @param Tag The gameplay tag to set. 要设置的游戏标签。 + * @param Value The float value to set. 要设置的浮点值。 + */ + void SetItem(FGameplayTag Tag, float Value); + + /** + * Removes a specified amount from a tag-value pair. + * 从标签-值对中移除指定数量。 + * @param Tag The gameplay tag to remove value from. 要移除值的游戏标签。 + * @param Value The amount to remove. 要移除的数量。 + */ + void RemoveItem(FGameplayTag Tag, float Value); + + /** + * Sets all items in the container. + * 设置容器中的所有条目。 + * @param NewItems The array of tag-value pairs to set. 要设置的标签-值对数组。 + */ + void SetItems(const TArray& NewItems); + + /** + * Clears all items from the container. + * 清空容器中的所有条目。 + */ + void EmptyItems(); + + /** + * Gets the value associated with a specific tag. + * 获取与指定标签关联的值。 + * @param Tag The gameplay tag to query. 要查询的游戏标签。 + * @return The float value associated with the tag. 与标签关联的浮点值。 + */ + float GetValue(FGameplayTag Tag) const + { + return TagToValueMap.FindRef(Tag); + } + + /** + * Checks if the container contains a specific tag. + * 检查容器是否包含指定标签。 + * @param Tag The gameplay tag to check. 要检查的游戏标签。 + * @return True if the tag exists in the container, false otherwise. 如果标签存在于容器中则返回true,否则返回false。 + */ + bool ContainsTag(FGameplayTag Tag) const + { + return TagToValueMap.Contains(Tag); + } + + //~FFastArraySerializer contract + /** + * Called before items are removed during replication. + * 复制期间移除条目前调用。 + * @param RemovedIndices The indices of items to remove. 要移除的条目索引。 + * @param FinalSize The final size of the items array after removal. 移除后条目数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after items are added during replication. + * 复制期间添加条目后调用。 + * @param AddedIndices The indices of added items. 添加的条目索引。 + * @param FinalSize The final size of the items array after addition. 添加后条目数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after items are changed during replication. + * 复制期间条目更改后调用。 + * @param ChangedIndices The indices of changed items. 更改的条目索引。 + * @param FinalSize The final size of the items array after change. 更改后条目数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for network replication. + * 处理网络复制的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Items, DeltaParms, *this); + } + + /** + * The object that owns this container. + * 拥有此容器的对象。 + */ + UPROPERTY() + TObjectPtr ContainerOwner{nullptr}; + + /** + * Replicated list of gameplay tag float pairs. + * 游戏标签浮点对的复制列表。 + */ + UPROPERTY(EditAnywhere, SaveGame, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="{Tag}->{Value}")) + TArray Items; + + /** + * Accelerated map of tags to values for efficient queries. + * 标签到值的加速映射,用于高效查询。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, NotReplicated, SaveGame, Category="GIS", meta=(ForceInlineRow)) + TMap TagToValueMap; +}; + +/** + * Traits for the float container to enable network delta serialization. + * 浮点容器的特性,用于启用网络增量序列化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true, + }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagInteger.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagInteger.h new file mode 100644 index 0000000..4798eb2 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Attributes/GIS_GameplayTagInteger.h @@ -0,0 +1,248 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Interface.h" +#include "GameplayTagContainer.h" +#include "GIS_GameplayTagInteger.generated.h" + +struct FGIS_GameplayTagIntegerContainer; + +/** + * Interface for objects that own a gameplay tag integer container. + * 拥有游戏标签整型容器对象的接口。 + */ +UINTERFACE(meta=(CannotImplementInterfaceInBlueprint)) +class GENERICINVENTORYSYSTEM_API UGIS_GameplayTagIntegerContainerOwner : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface class for handling updates to integer attributes in a gameplay tag container. + * 处理游戏标签容器中整型属性更新的接口类。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_GameplayTagIntegerContainerOwner +{ + GENERATED_BODY() + +public: + /** + * Called when an integer attribute associated with a gameplay tag is updated. + * 当与游戏标签关联的整型属性更新时调用。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + virtual void OnTagIntegerUpdate(const FGameplayTag& Tag, int32 OldValue, int32 NewValue) = 0; +}; + +/** + * Represents a gameplay tag and integer value pair. + * 表示一个游戏标签和整型值的键值对。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagInteger : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * Default constructor for the gameplay tag integer pair. + * 游戏标签整型对的默认构造函数。 + */ + FGIS_GameplayTagInteger() + { + } + + /** + * Constructor for the gameplay tag integer pair with initial values. + * 使用初始值构造游戏标签整型对。 + * @param InTag The gameplay tag for the pair. 键值对的游戏标签。 + * @param InValue The integer value for the pair. 键值对的整型值。 + */ + FGIS_GameplayTagInteger(FGameplayTag InTag, int32 InValue) + : Tag(InTag) + , Value(InValue) + { + } + + /** + * Gets a debug string representation of the tag-value pair. + * 获取标签-值对的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString() const; + + friend FGIS_GameplayTagIntegerContainer; + + /** + * The gameplay tag identifying the attribute. + * 标识属性的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGameplayTag Tag; + + /** + * The integer value associated with the tag. + * 与标签关联的整型值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + int32 Value = 0; + + /** + * The previous integer value of the attribute (not replicated). + * 属性的前一个整型值(不复制)。 + * @attention Likely a typo in the original code; should be int32 instead of float. + * @注意 原始代码中可能有误,应为int32而非float。 + */ + UPROPERTY(NotReplicated) + float PrevValue = 0; +}; + +/** + * Container for storing gameplay tag integer pairs. + * 存储游戏标签整型对的容器。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagIntegerContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for the integer container. + * 整型容器的默认构造函数。 + */ + FGIS_GameplayTagIntegerContainer() + // : Owner(nullptr) + { + } + + /** + * Constructor for the integer container with an owning object. + * 使用拥有对象构造整型容器。 + * @param InObject The object that owns this container. 拥有此容器的对象。 + */ + FGIS_GameplayTagIntegerContainer(UObject* InObject) + : ContainerOwner(InObject) + { + } + + /** + * Adds a new tag-value pair to the container. + * 向容器添加新的标签-值对。 + * @param Tag The gameplay tag to add. 要添加的游戏标签。 + * @param Value The integer value to associate with the tag. 与标签关联的整型值。 + */ + void AddItem(FGameplayTag Tag, int32 Value); + + /** + * Sets the value for an existing tag or adds a new tag-value pair. + * 为现有标签设置值或添加新的标签-值对。 + * @param Tag The gameplay tag to set. 要设置的游戏标签。 + * @param Value The integer value to set. 要设置的整型值。 + */ + void SetItem(FGameplayTag Tag, int32 Value); + + /** + * Removes a specified amount from a tag-value pair. + * 从标签-值对中移除指定数量。 + * @param Tag The gameplay tag to remove value from. 要移除值的游戏标签。 + * @param Value The amount to remove. 要移除的数量。 + */ + void RemoveItem(FGameplayTag Tag, int32 Value); + + /** + * Gets the value associated with a specific tag. + * 获取与指定标签关联的值。 + * @param Tag The gameplay tag to query. 要查询的游戏标签。 + * @return The integer value associated with the tag. 与标签关联的整型值。 + */ + int32 GetValue(FGameplayTag Tag) const + { + return TagToValueMap.FindRef(Tag); + } + + /** + * Checks if the container contains a specific tag. + * 检查容器是否包含指定标签。 + * @param Tag The gameplay tag to check. 要检查的游戏标签。 + * @return True if the tag exists in the container, false otherwise. 如果标签存在于容器中则返回true,否则返回false。 + */ + bool ContainsTag(FGameplayTag Tag) const + { + return TagToValueMap.Contains(Tag); + } + + //~FFastArraySerializer contract + /** + * Called before items are removed during replication. + * 复制期间移除条目前调用。 + * @param RemovedIndices The indices of items to remove. 要移除的条目索引。 + * @param FinalSize The final size of the items array after removal. 移除后条目数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after items are added during replication. + * 复制期间添加条目后调用。 + * @param AddedIndices The indices of added items. 添加的条目索引。 + * @param FinalSize The final size of the items array after addition. 添加后条目数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after items are changed during replication. + * 复制期间条目更改后调用。 + * @param ChangedIndices The indices of changed items. 更改的条目索引。 + * @param FinalSize The final size of the items array after change. 更改后条目数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for network replication. + * 处理网络复制的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Items, DeltaParms, *this); + } + + /** + * The object that owns this container. + * 拥有此容器的对象。 + */ + UPROPERTY() + TObjectPtr ContainerOwner{nullptr}; + + /** + * Replicated list of gameplay tag integer pairs. + * 游戏标签整型对的复制列表。 + */ + UPROPERTY(EditAnywhere, SaveGame, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="{Tag}->{Value}")) + TArray Items; + + /** + * Accelerated map of tags to values for efficient queries. + * 标签到值的加速映射,用于高效查询。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, NotReplicated, SaveGame, Category="GIS", meta=(ForceInlineRow)) + TMap TagToValueMap; +}; + +/** + * Traits for the integer container to enable network delta serialization. + * 整型容器的特性,用于启用网络增量序列化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true, + }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_CollectionContainer.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_CollectionContainer.h new file mode 100644 index 0000000..530000d --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_CollectionContainer.h @@ -0,0 +1,145 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "GIS_CollectionContainer.generated.h" + +class UGIS_ItemCollectionDefinition; +class UGIS_InventorySystemComponent; +class UGIS_ItemCollection; +struct FGIS_CollectionContainer; + +/** + * Structure representing an entry in the collection container. + * 表示集合容器中条目的结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_CollectionEntry : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * Default constructor for collection entry. + * 集合条目的默认构造函数。 + */ + FGIS_CollectionEntry() : Instance(nullptr) + { + } + + /** + * Unique ID of the collection entry. + * 集合条目的唯一ID。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + FGuid Id = FGuid(); + + /** + * The collection definition associated with this entry. + * 与此条目关联的集合定义。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + TObjectPtr Definition; + + /** + * The collection instance associated with this entry. + * 与此条目关联的集合实例。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS", meta=(ShowInnerProperties)) + TObjectPtr Instance = nullptr; + + /** + * Checks if the collection entry is valid. + * 检查集合条目是否有效。 + * @return True if the entry is valid, false otherwise. 如果条目有效则返回true,否则返回false。 + */ + bool IsValidEntry() const; +}; + +/** + * Container for storing a list of item collections with replication support. + * 用于存储道具集合列表的容器,支持复制。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_CollectionContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for collection container. + * 集合容器的默认构造函数。 + */ + FGIS_CollectionContainer() + : OwningComponent(nullptr) + { + } + + /** + * Constructor for collection container with an owning inventory component. + * 使用所属库存组件构造集合容器。 + * @param InInventory The owning inventory system component. 所属的库存系统组件。 + */ + FGIS_CollectionContainer(UGIS_InventorySystemComponent* InInventory); + + //~FFastArraySerializer contract + /** + * Called before collection entries are removed during replication. + * 复制期间在移除集合条目前调用。 + * @param RemovedIndices The indices of removed entries. 移除条目的索引。 + * @param FinalSize The final size of the array after removal. 移除后数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after collection entries are added during replication. + * 复制期间在添加集合条目后调用。 + * @param AddedIndices The indices of added entries. 添加条目的索引。 + * @param FinalSize The final size of the array after addition. 添加后数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after collection entries are changed during replication. + * 复制期间在集合条目更改后调用。 + * @param ChangedIndices The indices of changed entries. 更改条目的索引。 + * @param FinalSize The final size of the array after changes. 更改后数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for the collection container. + * 处理集合容器的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Entries, DeltaParms, *this); + } + + /** + * Replicated list of collection entries. + * 复制的集合条目列表。 + */ + UPROPERTY(VisibleAnywhere, Category="InventorySystem", meta=(ShowOnlyInnerProperties, DisplayName="Collections")) + TArray Entries; + + /** + * The inventory system component that owns this container. + * 拥有此容器的库存系统组件。 + */ + UPROPERTY() + TObjectPtr OwningComponent; +}; + +/** + * Template specialization to enable network delta serialization for the collection container. + * 为集合容器启用网络增量序列化的模板特化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum { WithNetDeltaSerializer = true }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemCollection.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemCollection.h new file mode 100644 index 0000000..167fc7a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemCollection.h @@ -0,0 +1,651 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/EngineTypes.h" +#include "Engine/DataAsset.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_InventoryMeesages.h" +#include "Items/GIS_ItemInfo.h" +#include "Items/GIS_ItemStack.h" +#include "GIS_ItemCollection.generated.h" + +class UGIS_ItemRestriction; +class UGIS_SerializationFunctionLibrary; +class UGIS_ItemRestrictionSet; +class UGIS_InventorySubsystem; +class UGIS_ItemCollection; +class UGIS_InventorySystemComponent; + +/** + * Delegate triggered when the item collection is updated. + * 道具集合更新时触发的委托。 + * @param Message The update message containing collection changes. 包含集合变更的更新消息。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Collection_UpdateSignature, const FGIS_InventoryCollectionUpdateMessage&, Message); + +/** + * Holds static configuration for an item collection. + * 存储道具集合的静态配置。 + */ +UCLASS(BlueprintType, NotBlueprintable) +class GENERICINVENTORYSYSTEM_API UGIS_ItemCollectionDefinition : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Checks if the collection definition supports networking. + * 检查集合定义是否支持网络。 + * @return True if networking is supported, false otherwise. 如果支持网络则返回true,否则返回false。 + */ + virtual bool IsSupportedForNetworking() const override; + + /** + * The unique tag of the item collection for querying in the inventory. + * 道具集合的唯一标签,用于在库存中查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Common", meta=(Categories="GIS.Collection")) + FGameplayTag CollectionTag; + + /** + * Restrictions applied to the collection for complex use cases. + * 为复杂用例应用于集合的限制。 + * @details Restrictions perform pre-checks to determine if items can be added or removed. + * @细节 限制会在道具操作前执行检查,以决定是否可以添加或移除道具。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Instanced, Category="Common") + TArray> Restrictions; + + /** + * Options for handling overflow when an item cannot fit in the collection. + * 当道具无法放入集合时处理溢出的选项。 + */ + UPROPERTY(EditAnywhere, Category="Common") + FGIS_ItemOverflowOptions OverflowOptions; + + /** + * Gets the class for instantiating the collection. + * 获取用于实例化集合的类。 + * @return The collection instance class. 集合实例类。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Common") + virtual TSubclassOf GetCollectionInstanceClass() const; +}; + +/** + * Base class for a normal item collection. + * 常规道具集合的基类。 + */ +UCLASS(BlueprintType, DefaultToInstanced, EditInlineNew, CollapseCategories, DisplayName="GIS Collection (Normal)") +class GENERICINVENTORYSYSTEM_API UGIS_ItemCollection : public UObject +{ + GENERATED_BODY() + + friend UGIS_ItemCollectionDefinition; + friend UGIS_InventorySystemComponent; + friend FGIS_ItemStackContainer; + +public: + /** + * Helper struct to lock notifications during collection updates. + * 用于在集合更新期间锁定通知的辅助结构体。 + */ + struct GIS_CollectionNotifyLocker + { + /** + * Constructor that locks notifications for the collection. + * 为集合锁定通知的构造函数。 + * @param InItemCollection The collection to lock notifications for. 要锁定通知的集合。 + */ + GIS_CollectionNotifyLocker(UGIS_ItemCollection& InItemCollection); + + /** + * Destructor that unlocks notifications. + * 解锁通知的析构函数。 + */ + ~GIS_CollectionNotifyLocker(); + + /** + * The collection being managed. + * 被管理的集合。 + */ + UGIS_ItemCollection& ItemCollection; + }; + + /** + * Constructor for the item collection. + * 道具集合的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_ItemCollection(const FObjectInitializer& ObjectInitializer); + + //~UObject interface + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Calls a remote function on the object. + * 在对象上调用远程函数。 + * @param Function The function to call. 要调用的函数。 + * @param Parms The function parameters. 函数参数。 + * @param OutParms The output parameters. 输出参数。 + * @param Stack The function call stack. 函数调用堆栈。 + * @return True if the function call was successful, false otherwise. 如果函数调用成功则返回true,否则返回false。 + */ + virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override; + + /** + * Gets the function call space for the object. + * 获取对象的函数调用空间。 + * @param Function The function to query. 要查询的函数。 + * @param Stack The function call stack. 函数调用堆栈。 + * @return The call space identifier. 调用空间标识符。 + */ + virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override; + + /** + * Checks if the object supports networking. + * 检查对象是否支持网络。 + * @return True if networking is supported, false otherwise. 如果支持网络则返回true,否则返回false。 + */ + virtual bool IsSupportedForNetworking() const override { return true; } + //~End of UObject interface + + /** + * Checks if the collection is initialized. + * 检查集合是否已初始化。 + * @return True if the collection is initialized, false otherwise. 如果集合已初始化则返回true,否则返回false。 + */ + bool IsInitialized() const; + + /** + * Gets the inventory that owns this collection. + * 获取拥有此集合的库存。 + * @return The owning inventory, or nullptr if not set. 所属库存,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemCollection") + UGIS_InventorySystemComponent* GetOwningInventory() const { return OwningInventory; }; + + /** + * Gets the unique tag of this collection. + * 获取此集合的唯一标签。 + * @return The collection tag. 集合标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + FGameplayTag GetCollectionTag() const { return CollectionTag; }; + + /** + * Gets the unique ID of this collection. + * 获取此集合的唯一ID。 + * @return The collection ID. 集合ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + FGuid GetCollectionId() const { return CollectionId; }; + + /** + * Gets the definition from which this collection was created. + * 获取此集合的源定义。 + * @return The collection definition, or nullptr if not set. 集合定义,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + const UGIS_ItemCollectionDefinition* GetDefinition() const { return Definition; }; + + /** + * Gets the display name of the collection. + * 获取集合的显示名称。 + * @return The display name. 显示名称。 + */ + FString GetCollectionName() const; + + /** + * Gets a debug string representation of the collection. + * 获取集合的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString() const; + +#pragma region HasRegion + /** + * Checks if the collection contains a specified item. + * 检查集合是否包含指定道具。 + * @param Item The item instance to check. 要检查的道具实例。 + * @param Amount The amount to check for. 要检查的数量。 + * @param SimilarItem Whether to check for similar items or exact matches. 是否检查相似道具或精确匹配。 + * @return True if the collection contains at least the specified amount, false otherwise. 如果集合包含至少指定数量则返回true,否则返回false。 + */ + virtual bool HasItem(const UGIS_ItemInstance* Item, int32 Amount, bool SimilarItem = true) const; + +#pragma endregion HasRegion + +#pragma region Add&Remove + /** + * Adds an item to the collection. + * 将道具添加到集合。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @return The item that was not added or empty if added successfully. 未添加的道具,如果成功添加则为空。 + */ + virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Adds multiple items to the collection. + * 将多个道具添加到集合。 + * @param ItemInfos The array of item information to add. 要添加的道具信息数组。 + * @return The number of items added. 添加的道具数量。 + */ + virtual int32 AddItems(const TArray& ItemInfos); + + /** + * Adds a specific amount of an item to the collection. + * 将指定数量的道具添加到集合。 + * @param Item The item instance to add. 要添加的道具实例。 + * @param Amount The amount to add. 要添加的数量。 + * @return The item that was actually added. 实际添加的道具。 + */ + FGIS_ItemInfo AddItem(UGIS_ItemInstance* Item, int32 Amount); + + /** + * Checks if an item can be added to the collection. + * 检查道具是否可以添加到集合。 + * @param InItemInfo The item information to check. 要检查的道具信息。 + * @param OutItemInfo The item information that can be added (output). 可添加的道具信息(输出)。 + * @return True if at least one item can be added, false otherwise. 如果至少可以添加一个道具则返回true,否则返回false。 + */ + virtual bool CanAddItem(const FGIS_ItemInfo& InItemInfo, FGIS_ItemInfo& OutItemInfo); + + /** + * Checks if an item can be stacked with an existing stack. + * 检查道具是否可以与现有栈堆叠。 + * @param ItemInfo The item information to check. 要检查的道具信息。 + * @param ItemStack The item stack to check against. 要检查的道具栈。 + * @return True if the item can be stacked, false otherwise. 如果道具可以堆叠则返回true,否则返回false。 + */ + virtual bool CanItemStack(const FGIS_ItemInfo& ItemInfo, const FGIS_ItemStack& ItemStack) const; + + /** + * Checks conditions for removing an item from the collection. + * 检查从集合移除道具的条件。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @param OutItemInfo The item information that can be removed (output). 可移除的道具信息(输出)。 + * @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true,否则返回false。 + */ + virtual bool RemoveItemCondition(const FGIS_ItemInfo& ItemInfo, FGIS_ItemInfo& OutItemInfo); + + /** + * Removes an item from the collection. + * 从集合中移除道具。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @return The item that was removed, or empty if nothing was removed. 移除的道具,如果未移除则为空。 + */ + virtual FGIS_ItemInfo RemoveItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Removes all items from the collection. + * 从集合中移除所有道具。 + */ + virtual void RemoveAll(); + +#pragma endregion Add&Remove + +#pragma region GetReference + /** + * Gets item information by stack ID. + * 通过栈ID获取道具信息。 + * @param InStackId The stack ID to query. 要查询的栈ID。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if item information was found, false otherwise. 如果找到道具信息则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + bool GetItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Finds item information by stack ID. + * 通过栈ID查找道具信息。 + * @param InStackId The stack ID to query. 要查询的栈ID。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if item information was found, false otherwise. 如果找到道具信息则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemCollection", meta=(ExpandBoolAsExecs="ReturnValue")) + bool FindItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Retrieves information about a specific item instance. + * 获取指定道具实例的信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if information was found, false otherwise. 如果找到信息则返回true,否则返回false。 + */ + virtual bool GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Gets item information by item definition. + * 通过道具定义获取道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if information was found, false otherwise. 如果找到信息则返回true,否则返回false。 + */ + bool GetItemInfoByDefinition(const TSoftObjectPtr& ItemDefinition, FGIS_ItemInfo& OutItemInfo); + + /** + * Gets all item information for a specific item definition. + * 获取指定道具定义的所有道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfos The array of item information (output). 道具信息数组(输出)。 + * @return True if information was found, false otherwise. 如果找到信息则返回true,否则返回false。 + */ + bool GetItemInfosByDefinition(const TSoftObjectPtr& ItemDefinition, TArray& OutItemInfos); + + /** + * Gets the amount of a specific item in the collection. + * 获取集合中指定道具的数量。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param SimilarItem Whether to count similar items or exact matches. 是否计数相似道具或精确匹配。 + * @return The amount of the item in the collection. 集合中的道具数量。 + */ + int32 GetItemAmount(const UGIS_ItemInstance* Item, bool SimilarItem = true) const; + + /** + * Gets the amount of items with a specific definition in the collection. + * 获取集合中具有指定定义的道具数量。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param CountStacks Whether to count the number of stacks instead of total amount. 是否计数栈数量而不是总数量。 + * @return The number of items or stacks in the collection. 集合中的道具或栈数量。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemCollection") + int32 GetItemAmount(TSoftObjectPtr ItemDefinition, bool CountStacks = false) const; + + /** + * Gets all item information in the collection. + * 获取集合中的所有道具信息。 + * @return Array of item information. 道具信息数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + TArray GetAllItemInfos() const; + + /** + * Gets all item instances in the collection. + * 获取集合中的所有道具实例。 + * @return Array of item instances. 道具实例数组。 + */ + TArray GetAllItems() const; + +#pragma endregion GetReference + +#pragma region Giver + /** + * Gives an item to another collection. + * 将道具给予另一个集合。 + * @param ItemInfo The item information to give. 要给予的道具信息。 + * @param ItemCollection The target collection to receive the item. 接收道具的目标集合。 + * @return The item information that was given. 给予的道具信息。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + virtual FGIS_ItemInfo GiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection); + + /** + * Server function to give an item to another collection. + * 服务器函数,将道具给予另一个集合。 + * @param ItemInfo The item information to give. 要给予的道具信息。 + * @param ItemCollection The target collection to receive the item. 接收道具的目标集合。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|ItemCollection") + void ServerGiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection); + + /** + * Implementation of ServerGiveItem. + * ServerGiveItem 的实现。 + * @param ItemInfo The item information to give. 要给予的道具信息。 + * @param ItemCollection The target collection to receive the item. 接收道具的目标集合。 + */ + virtual void ServerGiveItem_Implementation(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection); + + /** + * Gives all items in this collection to another collection. + * 将此集合中的所有道具给予另一个集合。 + * @param OtherItemCollection The target collection to receive the items. 接收道具的目标集合。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + virtual void GiveAllItems(UGIS_ItemCollection* OtherItemCollection); + + /** + * Server function to give all items to another collection. + * 服务器函数,将所有道具给予另一个集合。 + * @param OtherItemCollection The target collection to receive the items. 接收道具的目标集合。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|ItemCollection") + void ServerGiveAllItems(UGIS_ItemCollection* OtherItemCollection); + + /** + * Calculates how many items can fit given a limited number of additional stacks. + * 计算在有限额外栈数下可以容纳的道具数量。 + * @param ItemInfo The item information to check. 要检查的道具信息。 + * @param AvailableAdditionalStacks The number of additional stacks allowed. 允许的额外栈数。 + * @return The number of items that can fit. 可容纳的道具数量。 + */ + virtual int32 GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const; + +#pragma endregion Giver + + /** + * Event triggered when the item collection is updated. + * 道具集合更新时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Collection_UpdateSignature OnItemCollectionUpdate; + +#pragma region ItemStacks + /** + * Gets all item stacks in the collection. + * 获取集合中的所有道具栈。 + * @return Array of item stacks. 道具栈数组。 + */ + const TArray& GetAllItemStacks() const; + + /** + * Gets the number of item stacks in the collection. + * 获取集合中的道具栈数量。 + * @return The number of item stacks. 道具栈数量。 + */ + int32 GetItemStacksNum() const; + +protected: + /** + * Adds an item stack to the collection. + * 将道具栈添加到集合。 + * @param Stack The item stack to add. 要添加的道具栈。 + */ + virtual void AddItemStack(const FGIS_ItemStack& Stack); + + /** + * Removes an item stack at a specific index. + * 在指定索引移除道具栈。 + * @param Idx The index of the stack to remove. 要移除的栈索引。 + * @param bRemoveFromCollection Whether to remove the item instance from the collection. 是否从集合中移除道具实例。 + */ + virtual void RemoveItemStackAtIndex(int32 Idx, bool bRemoveFromCollection = true); + + /** + * Updates the amount of an item stack at a specific index. + * 更新指定索引的道具栈数量。 + * @param Idx The index of the stack to update. 要更新的栈索引。 + * @param NewAmount The new amount for the stack. 栈的新数量。 + */ + virtual void UpdateItemStackAmountAtIndex(int32 Idx, int32 NewAmount); + + /** + * Called before an item stack is added (server-side only). + * 在添加道具栈之前调用(仅限服务器端)。 + * @param Stack The item stack to add. 要添加的道具栈。 + * @param Idx The index where the stack will be added. 栈将添加的索引。 + */ + virtual void OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx); + + /** + * Called when an item stack is added (client and server). + * 道具栈添加时调用(客户端和服务器)。 + * @param Stack The added item stack. 添加的道具栈。 + */ + virtual void OnItemStackAdded(const FGIS_ItemStack& Stack); + + /** + * Called when an item stack is removed (client and server). + * 道具栈移除时调用(客户端和服务器)。 + * @param Stack The removed item stack. 移除的道具栈。 + */ + virtual void OnItemStackRemoved(const FGIS_ItemStack& Stack); + + /** + * Called when an item stack is updated (client and server). + * 道具栈更新时调用(客户端和服务器)。 + * @param Stack The updated item stack. 更新的道具栈。 + */ + virtual void OnItemStackUpdated(const FGIS_ItemStack& Stack); + + /** + * Processes pending item stacks. + * 处理待处理的道具栈。 + */ + void ProcessPendingItemStacks(); + + /** + * Store the stack id and position mapping within this collection. + * 存储在此集合中Stack id到位置的映射关系。 + */ + UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient, meta=(ForceInlineRow)) + TMap StackToIdxMap; + + /** + * Store the stack id and position mapping within this collection. + * 存储在此集合中Stack id到位置的映射关系。 + */ + // UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient) + // TMap IdxToStackMap; + +private: + /** + * Temporary storage for pending item stacks. + * 待处理道具栈的临时存储。 + */ + UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient) + TMap PendingItemStacks; + + +#pragma endregion + +protected: + /** + * Internal function to add an item to the collection. + * 内部函数,将道具添加到集合。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @return The item that was actually added. 实际添加的道具。 + */ + virtual FGIS_ItemInfo AddInternal(const FGIS_ItemInfo& ItemInfo); + + /** + * Handles overflow when an item cannot be fully added. + * 处理道具无法完全添加时的溢出。 + * @param OriginalItemInfo The original item information. 原始道具信息。 + * @param ItemInfoAdded The item information that was added. 已添加的道具信息。 + */ + virtual void HandleItemOverflow(const FGIS_ItemInfo& OriginalItemInfo, const FGIS_ItemInfo& ItemInfoAdded); + + /** + * Internal function to remove an item from the collection. + * 内部函数,从集合中移除道具。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @return The item that was removed. 移除的道具。 + */ + virtual FGIS_ItemInfo RemoveInternal(const FGIS_ItemInfo& ItemInfo); + + /** + * Simplifies internal item removal logic. + * 简化内部道具移除逻辑。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @param AlreadyRemoved The amount already removed (modified). 已移除的数量(可修改)。 + * @param StackIndex The stack index to remove from. 要移除的栈索引。 + * @return The item stack was actually removed. 实际被移除的道具栈。 + */ + virtual FGIS_ItemStack SimpleInternalItemRemove(const FGIS_ItemInfo& ItemInfo, int32& AlreadyRemoved, int32 StackIndex); + + /** + * Indicates whether the collection is initialized. + * 指示集合是否已初始化。 + */ + UPROPERTY(Transient) + bool bInitialized = false; + + /** + * Counter for locking notifications. + * 用于锁定通知的计数器。 + */ + int32 NotifyLocker = 0; + + /** + * Container for item stacks. + * 道具栈的容器。 + */ + UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Replicated, meta=(ShowOnlyInnerProperties)) + FGIS_ItemStackContainer Container; + + /** + * The unique tag of the item collection for querying in the inventory. + * 道具集合的唯一标签,用于在库存中查询。 + */ + UPROPERTY(Transient) + FGameplayTag CollectionTag; + + /** + * The collection definition. + * 集合定义。 + */ + UPROPERTY(Transient) + TObjectPtr Definition{nullptr}; + + /** + * The unique ID of the collection. + * 集合的唯一ID。 + */ + UPROPERTY(Transient) + FGuid CollectionId; + + /** + * The inventory that owns this collection. + * 拥有此集合的库存。 + */ + UPROPERTY() + TObjectPtr OwningInventory; + + /** + * Sets the collection definition. + * 设置集合定义。 + * @param NewDefinition The new collection definition. 新的集合定义。 + */ + virtual void SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition); + + /** + * Sets the unique tag of the collection. + * 设置集合的唯一标签。 + * @param NewTag The new collection tag. 新的集合标签。 + */ + void SetCollectionTag(FGameplayTag NewTag); + + /** + * Sets the unique ID of the collection. + * 设置集合的唯一ID。 + * @param NewId The new collection ID. 新的集合ID。 + */ + void SetCollectionId(FGuid NewId); + + /** + * Sets the owning inventory for the collection. + * 设置集合的所属库存。 + * @param NewInventory The new owning inventory. 新的所属库存。 + */ + void SetInventory(UGIS_InventorySystemComponent* NewInventory) { OwningInventory = NewInventory; }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemMultiStackCollection.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemMultiStackCollection.h new file mode 100644 index 0000000..a3bfa2b --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemMultiStackCollection.h @@ -0,0 +1,133 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemMultiStackCollection.generated.h" + +class UGIS_ItemMultiStackCollection; + +/** + * Definition for a multi-stack item collection. + * 多栈道具集合的定义。 + */ +UCLASS(BlueprintType) +class UGIS_ItemMultiStackCollectionDefinition : public UGIS_ItemCollectionDefinition +{ + GENERATED_BODY() + +public: + /** + * Constructor for the multi-stack collection definition. + * 多栈集合定义的构造函数。 + */ + UGIS_ItemMultiStackCollectionDefinition(); + + /** + * Gets the class for instantiating the collection. + * 获取用于实例化集合的类。 + * @return The collection instance class. 集合实例类。 + */ + virtual TSubclassOf GetCollectionInstanceClass() const override; + + /** + * Default stack size limit for items without a StackSizeLimitAttribute. + * 没有StackSizeLimitAttribute的道具的默认栈大小限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StackSettings") + int32 DefaultStackSizeLimit = 99; + + /** + * The integer attribute in the item definition to determine stack size (optional). + * 道具定义中用于确定栈大小的整型属性(可选)。 + * @attention This is optional. + * @注意 这是可选的。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StackSettings") + FGameplayTag StackSizeLimitAttribute; +}; + +/** + * An item collection that supports multiple stacks of the same item. + * 支持相同道具多栈的道具集合。 + */ +UCLASS(DisplayName="GIS Item Collection (Multi Stack)") +class GENERICINVENTORYSYSTEM_API UGIS_ItemMultiStackCollection : public UGIS_ItemCollection +{ + GENERATED_BODY() + +public: + /** + * Constructor for the multi-stack item collection. + * 多栈道具集合的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_ItemMultiStackCollection(const FObjectInitializer& ObjectInitializer); + + /** + * Retrieves information about an item instance in the collection. + * 获取集合中道具实例的信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if information was found, false otherwise. 如果找到信息则返回true,否则返回false。 + */ + virtual bool GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const override; + +protected: + /** + * Calculates how many items can fit given a limited number of additional stacks. + * 计算在有限额外栈数下可以容纳的道具数量。 + * @param ItemInfo The item information to check. 要检查的道具信息。 + * @param AvailableAdditionalStacks The number of additional stacks allowed. 允许的额外栈数。 + * @return The number of items that can fit. 可容纳的道具数量。 + */ + virtual int32 GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const override; + + /** + * Internal function to add an item to the collection. + * 内部函数,将道具添加到集合。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @return The item that was actually added. 实际添加的道具。 + */ + virtual FGIS_ItemInfo AddInternal(const FGIS_ItemInfo& ItemInfo) override; + + /** + * Gets the maximum stack size for an item. + * 获取道具的最大栈大小。 + * @param Item The item instance to query. 要查询的道具实例。 + * @return The maximum stack size for the item. 道具的最大栈大小。 + */ + int32 GetMaxStackSize(UGIS_ItemInstance* Item) const; + +private: + /** + * Internal function to remove an item from the collection. + * 内部函数,从集合中移除道具。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @return The item that was removed. 移除的道具。 + */ + virtual FGIS_ItemInfo RemoveInternal(const FGIS_ItemInfo& ItemInfo) override; + + /** + * Removes items from a specific stack. + * 从指定栈移除道具。 + * @param Index The index of the stack to remove from. 要移除的栈索引。 + * @param PrevStackIndexWithSameItem The previous stack index with the same item. 具有相同道具的前一个栈索引。 + * @param MaxStackSize The maximum stack size. 最大栈大小。 + * @param AmountToRemove The amount to remove (modified). 要移除的数量(可修改)。 + * @param AlreadyRemoved The amount already removed (modified). 已移除的数量(可修改)。 + * @return The stack index with the item. 包含道具的栈索引。 + */ + int32 RemoveItemFromStack(int32 Index, int32 PrevStackIndexWithSameItem, int32 MaxStackSize, int32& AmountToRemove, int32& AlreadyRemoved); + + /** + * Increases the amount in a specific stack. + * 增加指定栈中的数量。 + * @param StackIdx The stack index to increase. 要增加的栈索引。 + * @param MaxStackSize The maximum stack size. 最大栈大小。 + * @param AmountToAdd The amount to add (modified). 要添加的数量(可修改)。 + * @return The amount added to the stack. 添加到栈的数量。 + */ + int32 IncreaseStackAmount(int32 StackIdx, int32 MaxStackSize, int32& AmountToAdd); +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction.h new file mode 100644 index 0000000..8cd1805 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction.h @@ -0,0 +1,120 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Items/GIS_ItemInfo.h" +#include "GIS_ItemRestriction.generated.h" + +/** + * Base class for item restrictions used to limit item collection operations. + * 用于限制道具集合操作的道具限制基类。 + * @details Subclasses can extend this to implement custom restrictions. + * @细节 子类可以扩展此类以实现自定义限制。 + */ +UCLASS(Blueprintable, BlueprintType, DefaultToInstanced, EditInlineNew, CollapseCategories, Abstract, Const) +class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction : public UObject +{ + GENERATED_BODY() + +public: + /** + * Checks if an item can be added to the collection. + * 检查道具是否可以添加到集合。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @param ReceivingCollection The collection to add the item to. 要添加到的集合。 + * @return True if any valid item can be added, false otherwise. 如果可以添加任何有效道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category=ItemRestriction) + bool CanAddItem(UPARAM(ref) FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const; + + /** + * Checks if an item can be removed from the collection. + * 检查道具是否可以从集合中移除。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @return True if any valid item can be removed, false otherwise. 如果可以移除任何有效道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category=ItemRestriction) + bool CanRemoveItem(UPARAM(ref) FGIS_ItemInfo& ItemInfo) const; + + /** + * Gets the reason for rejecting an add operation. + * 获取拒绝添加操作的原因。 + * @return The rejection reason text. 拒绝原因文本。 + */ + UFUNCTION(BlueprintPure, BlueprintNativeEvent, Category=ItemRestriction) + FText GetRejectAddReason() const; + + /** + * Implementation of GetRejectAddReason. + * GetRejectAddReason 的实现。 + * @return The rejection reason text. 拒绝原因文本。 + */ + virtual FText GetRejectAddReason_Implementation() const { return RejectAddReason; } + + /** + * Gets the reason for rejecting a remove operation. + * 获取拒绝移除操作的原因。 + * @return The rejection reason text. 拒绝原因文本。 + */ + UFUNCTION(BlueprintPure, BlueprintNativeEvent, Category=ItemRestriction) + FText GetRejectRemoveReason() const; + + /** + * Implementation of GetRejectRemoveReason. + * GetRejectRemoveReason 的实现。 + * @return The rejection reason text. 拒绝原因文本。 + */ + virtual FText GetRejectRemoveReason_Implementation() const { return RejectAddReason; } + +protected: + /** + * Internal check for adding an item to the collection. + * 检查道具是否可以添加到集合的内部函数。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @param ReceivingCollection The collection to add the item to. 要添加到的集合。 + * @return True if the item can be added, false otherwise. 如果道具可以添加则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category=ItemRestriction, meta=(DisplayName=CanAddItem)) + bool CanAddItemInternal(UPARAM(ref) FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const; + + /** + * Implementation of CanAddItemInternal. + * CanAddItemInternal 的实现。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @param ReceivingCollection The collection to add the item to. 要添加到的集合。 + * @return True if the item can be added, false otherwise. 如果道具可以添加则返回true,否则返回false。 + */ + virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const; + + /** + * Internal check for removing an item from the collection. + * 检查道具是否可以从集合移除的内部函数。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category=ItemRestriction, meta=(DisplayName=CanRemoveItem)) + bool CanRemoveItemInternal(UPARAM(ref) FGIS_ItemInfo& ItemInfo) const; + + /** + * Implementation of CanRemoveItemInternal. + * CanRemoveItemInternal 的实现。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true,否则返回false。 + */ + virtual bool CanRemoveItemInternal_Implementation(FGIS_ItemInfo& ItemInfo) const; + + /** + * The reason for rejecting an add operation. + * 拒绝添加操作的原因。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=ItemRestriction) + FText RejectAddReason; + + /** + * The reason for rejecting a remove operation. + * 拒绝移除操作的原因。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=ItemRestriction) + FText RejectRemoveReason; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StackSizeLimit.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StackSizeLimit.h new file mode 100644 index 0000000..616fd70 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StackSizeLimit.h @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemRestriction.h" +#include "GIS_ItemRestriction_StackSizeLimit.generated.h" + +/** + * Restricts the maximum amount of an item within a specified collection. + * 限制指定集合中道具的最大数量。 + * @attention Not suitable for MultiStackItemCollection. + * @注意 不适用于MultiStackItemCollection。 + */ +UCLASS(NotBlueprintable, DisplayName=ItemRestriction_StackSizeLimit) +class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_StackSizeLimit final : public UGIS_ItemRestriction +{ + GENERATED_BODY() + +public: + /** + * Constructor for the stack size limit restriction. + * 栈大小限制的构造函数。 + */ + UGIS_ItemRestriction_StackSizeLimit(); + +protected: + /** + * Internal check for adding an item to the collection. + * 检查道具是否可以添加到集合的内部函数。 + * @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。 + * @param ReceivingCollection The collection to add the item to. 要添加到的集合。 + * @return True if the item can be added within the stack size limit, false otherwise. 如果道具可以在栈大小限制内添加则返回true,否则返回false。 + */ + virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override; + + /** + * Gets the stack size limit for a specific item. + * 获取指定道具的栈大小限制。 + * @param Item The item instance to query. 要查询的道具实例。 + * @return The stack size limit for the item. 道具的栈大小限制。 + */ + int32 GetStackSizeLimit(const UGIS_ItemInstance* Item) const; + + /** + * The default stack size limit for any items. + * 针对所有道具的默认栈大小限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction) + int32 DefaultStackSizeLimit; + + /** + * The integer attribute in the item definition used to limit the stack size for non-unique items (unique items cannot stack). + * 道具定义中用于限制非唯一道具栈大小的整型属性(唯一道具无法堆叠)。 + * @attention This is optional. + * @注意 这是可选的。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction) + FGameplayTag StackSizeLimitAttributeTag; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StacksNumLimit.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StacksNumLimit.h new file mode 100644 index 0000000..2379aaa --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_StacksNumLimit.h @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemRestriction.h" +#include "GIS_ItemRestriction_StacksNumLimit.generated.h" + +/** + * Restricts the total number of stacks for an item collection. + * 限制道具集合的总堆叠数量。 + */ +UCLASS(NotBlueprintable, DisplayName=ItemRestriction_StacksNumLimit) +class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_StacksNumLimit final : public UGIS_ItemRestriction +{ + GENERATED_BODY() + +protected: + /** + * Checks if an item can be added to the collection based on stack limits. + * 检查是否可以根据堆叠限制将道具添加到集合。 + * @param ItemInfo Information about the item to add. 要添加的道具信息。 + * @param ReceivingCollection The collection receiving the item. 接收道具的集合。 + * @return True if the item can be added, false otherwise. 如果可以添加道具则返回true,否则返回false。 + */ + virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override; + + /** + * Maximum number of stacks allowed in the collection. + * 集合中允许的最大堆叠数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction) + int32 MaxStacksNum = 30; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_TagRequirements.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_TagRequirements.h new file mode 100644 index 0000000..0455282 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_TagRequirements.h @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemRestriction.h" +#include "GIS_ItemRestriction_TagRequirements.generated.h" + +/** + * Restricts items to those that satisfy a specific tag query for addition to a collection. + * 限制只有满足特定标签查询的道具可以添加到集合。 + */ +UCLASS(NotBlueprintable, DisplayName=ItemRestriction_TagRequirements) +class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_TagRequirements final : public UGIS_ItemRestriction +{ + GENERATED_BODY() + +protected: + /** + * Tag query that items must satisfy to be added to the collection. + * 道具必须满足的标签查询才能添加到集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction) + FGameplayTagQuery TagQuery; + + /** + * Checks if an item satisfies the tag query for addition to the collection. + * 检查道具是否满足标签查询以添加到集合。 + * @param ItemInfo Information about the item to add. 要添加的道具信息。 + * @param ReceivingCollection The collection receiving the item. 接收道具的集合。 + * @return True if the item can be added, false otherwise. 如果可以添加道具则返回true,否则返回false。 + */ + virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_UniqueOnly.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_UniqueOnly.h new file mode 100644 index 0000000..3f163e6 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemRestriction_UniqueOnly.h @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemRestriction.h" +#include "GIS_ItemRestriction_UniqueOnly.generated.h" + +/** + * Restricts a collection to only accept unique items. + * 限制集合仅接受唯一道具。 + * @details Prevents duplicate items from being added to the collection. + * @细节 防止重复道具被添加到集合。 + */ +UCLASS(NotBlueprintable, DisplayName=ItemRestriction_UniqueOnly) +class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_UniqueOnly final : public UGIS_ItemRestriction +{ + GENERATED_BODY() + +protected: + /** + * Checks if an item is unique before adding it to the collection. + * 在将道具添加到集合之前检查其是否唯一。 + * @param ItemInfo Information about the item to add. 要添加的道具信息。 + * @param ReceivingCollection The collection receiving the item. 接收道具的集合。 + * @return True if the item is unique and can be added, false otherwise. 如果道具唯一且可添加则返回true,否则返回false。 + */ + virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemSlotCollection.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemSlotCollection.h new file mode 100644 index 0000000..c9fd248 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Collections/GIS_ItemSlotCollection.h @@ -0,0 +1,418 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemSlotCollection.generated.h" + +/** + * A slot-based item collection definition, suitable for equipment or skill bars. + * 基于槽的道具集合定义,适合用于装备或技能栏。 + * @details Stores items in a slot-to-item style, ideal for equipment, skill bars, etc. + * @细节 以槽对应道具的方式存储,适合装备、技能栏等场景。 + */ +UCLASS(BlueprintType) +class GENERICINVENTORYSYSTEM_API UGIS_ItemSlotCollectionDefinition : public UGIS_ItemCollectionDefinition +{ + GENERATED_BODY() + +public: + /** + * Gets the class for instantiating the collection. + * 获取用于实例化集合的类。 + * @return The collection instance class. 集合实例类。 + */ + virtual TSubclassOf GetCollectionInstanceClass() const override; + + /** + * Checks if the specified slot index is valid within SlotDefinitions. + * 检查指定槽索引在SlotDefinitions中是否有效。 + * @param SlotIndex The slot index to check. 要检查的槽索引。 + * @return True if the slot index is valid, false otherwise. 如果槽索引有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition") + virtual bool IsValidSlotIndex(int32 SlotIndex) const; + + /** + * Gets the index of a slot based on its tag within SlotDefinitions. + * 根据槽标签获取SlotDefinitions中的槽索引。 + * @param SlotName The slot tag to query. 要查询的槽标签。 + * @return The index of the slot, or INDEX_NONE if not found. 槽的索引,如果未找到则返回INDEX_NONE。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition") + virtual int32 GetIndexOfSlot(UPARAM(meta=(Categories="GIS.Slots")) + const FGameplayTag& SlotName) const; + + /** + * Gets the slot tag for a given slot index within SlotDefinitions. + * 根据槽索引获取SlotDefinitions中的槽标签。 + * @param SlotIndex The slot index to query. 要查询的槽索引。 + * @return The slot tag, or invalid tag if not found. 槽标签,如果未找到则返回无效标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition") + virtual FGameplayTag GetSlotOfIndex(int32 SlotIndex) const; + + /** + * Gets all slot definitions within this collection definition. + * 获取此集合定义中的所有槽定义。 + * @return Array of slot definitions. 槽定义数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition") + const TArray& GetSlotDefinitions() const; + + /** + * Gets the slot definition for a given slot index. + * 获取指定槽索引的槽定义。 + * @param SlotIndex The slot index to query. 要查询的槽索引。 + * @param OutDefinition The slot definition (output). 槽定义(输出)。 + * @return True if the slot definition was found, false otherwise. 如果找到槽定义则返回true,否则返回false。 + */ + bool GetSlotDefinition(int32 SlotIndex, FGIS_ItemSlotDefinition& OutDefinition) const; + + /** + * Gets the slot definition for a given slot tag. + * 获取指定槽标签的槽定义。 + * @param SlotName The slot tag to query. 要查询的槽标签。 + * @param OutDefinition The slot definition (output). 槽定义(输出)。 + * @return True if the slot definition was found, false otherwise. 如果找到槽定义则返回true,否则返回false。 + */ + bool GetSlotDefinition(const FGameplayTag& SlotName, FGIS_ItemSlotDefinition& OutDefinition) const; + + /** + * Gets the index of a slot within a specified group. + * 获取指定槽在指定槽组内的索引。 + * @param GroupTag The group tag to query. 要查询的组标签。 + * @param SlotTag The slot tag to query. 要查询的槽标签。 + * @return The index of the slot within the group, or -1 if not found. 槽在组内的索引,如果未找到则返回-1。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition") + int32 GetSlotIndexWithinGroup(UPARAM(meta=(Categories="GIS.Slots")) + FGameplayTag GroupTag, FGameplayTag SlotTag) const; + + /** + * Whether newly added items replace existing items if they are not stackable. + * 新添加的道具如果无法堆叠,是否替换现有道具。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", meta=(DisplayAfter="OverflowOptions")) + bool bNewItemPriority = true; + + /** + * Whether replaced items are returned to the source collection of the new item. + * 被替换的道具是否返回到新道具的源集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", meta=(EditCondition="bNewItemPriority")) + bool bTryGivePrevItemToNewItemCollection = true; + + /** + * Defines all available item slots in the collection. + * 定义集合中的所有可用道具槽。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="SlotSettings", meta=(TitleProperty="SlotName:{Name}")) + TArray SlotDefinitions; + + /** + * List of parent slot tags for grouping slots; only one slot per group can be active at a time. + * 用于分组的父级槽标签列表,每个组同时只能激活一个槽。 + * @attention Used by the equipment system. + * @注意 由装备系统使用。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="SlotSettings", meta=(AllowPrivateAccess=True, Categories="GIS.Slots")) + TArray SlotGroups; + + /** + * Cached mapping of slot indices to slot tags for faster access, updated on save in the editor. + * 槽索引到槽标签的缓存映射,用于快速访问,在编辑器保存时更新。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Advanced", meta=(ForceInlineRow)) + TMap IndexToTagMap; + + /** + * Cached mapping of slot tags to slot indices for faster access, updated on save in the editor. + * 槽标签到槽索引的缓存映射,用于快速访问,在编辑器保存时更新。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Advanced", meta=(ForceInlineRow)) + TMap TagToIndexMap; + + /** + * Cached grouping of slot definitions by slot groups, updated on save in the editor. + * 按槽组分组的槽定义缓存,在编辑器保存时更新。 + */ + UPROPERTY(VisibleAnywhere, Category="Advanced", meta=(ForceInlineRow)) + TMap SlotGroupMap; + +#if WITH_EDITOR + /** + * Called before saving the object in the editor. + * 在编辑器中保存对象前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + +/** + * A slot-based item collection that organizes items in slots, each holding an item stack. + * 基于槽的道具集合,按槽组织道具,每个槽可持有道具栈。 + */ +UCLASS(BlueprintType, EditInlineNew, CollapseCategories, DisplayName="GIS Item Collection (Slotted)") +class GENERICINVENTORYSYSTEM_API UGIS_ItemSlotCollection : public UGIS_ItemCollection +{ + GENERATED_BODY() + + friend UGIS_ItemSlotCollectionDefinition; + +public: + /** + * Constructor for the slot-based item collection. + * 基于槽的道具集合的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_ItemSlotCollection(const FObjectInitializer& ObjectInitializer); + + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the definition of this slot-based collection. + * 获取此基于槽的集合的定义。 + * @return The collection definition, or nullptr if not set. 集合定义,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + const UGIS_ItemSlotCollectionDefinition* GetMyDefinition() const; + + /** + * Adds an item to the collection, typically for unique items. + * 将道具添加到集合,通常用于唯一道具。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @return The item that was not added, the previously equipped item if replaced, or empty if added successfully. 未添加的道具、替换的先前道具或成功添加时为空。 + */ + virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo) override; + + /** + * Adds an item to the specified slot (authority only). + * 将道具添加到指定槽(仅限权限)。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。 + * @return The item that was actually added. 实际添加的道具。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex); + + /** + * Server function to add an item to the specified slot. + * 服务器函数,将道具添加到指定槽。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + void ServerAddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex); + + /** + * Adds an item to the slot with the specified tag (authority only). + * 将道具添加到指定标签的槽(仅限权限)。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @param SlotName The tag of the slot to add the item to. 要添加到的槽标签。 + * @return The item that was actually added. 实际添加的道具。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + FGIS_ItemInfo AddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName); + + /** + * Server function to add an item to the slot with the specified tag. + * 服务器函数,将道具添加到指定标签的槽。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @param SlotName The tag of the slot to add the item to. 要添加到的槽标签。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection") + void ServerAddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName); + + /** + * Removes an item from the collection. + * 从集合中移除道具。 + * @param ItemInfo The item information to remove. 要移除的道具信息。 + * @return The item that was removed, or empty if nothing was removed. 移除的道具,如果未移除则为空。 + */ + virtual FGIS_ItemInfo RemoveItem(const FGIS_ItemInfo& ItemInfo) override; + + /** + * Removes an item from the specified slot. + * 从指定槽移除道具。 + * @param SlotIndex The index of the slot to remove the item from. 要移除道具的槽索引。 + * @param Amount The amount to remove (-1 to remove all). 要移除的数量(-1表示全部移除)。 + * @return The item information that was removed. 移除的道具信息。 + */ + virtual FGIS_ItemInfo RemoveItem(int32 SlotIndex, int32 Amount = -1); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + bool IsItemFitWithSlot(const UGIS_ItemInstance* Item, int32 SlotIndex) const; + + /** + * Gets a suitable slot index for an incoming item. + * 获取适合传入道具的槽索引。 + * @param Item The item instance to check. 要检查的道具实例。 + * @return A valid slot index, or INDEX_NONE if none is suitable. 有效槽索引,如果没有合适的槽则返回INDEX_NONE。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + int32 GetTargetSlotIndex(const UGIS_ItemInstance* Item) const; + + /** + * Gets the item information at a specific slot by tag. + * 通过槽标签获取指定槽的道具信息。 + * @param SlotTag The slot tag to query. 要查询的槽标签。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if item information was found, false otherwise. 如果找到道具信息则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + virtual bool GetItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Finds the item information at a specific slot by tag. + * 通过槽标签查找指定槽的道具信息。 + * @param SlotTag The slot tag to query. 要查询的槽标签。 + * @param OutItemInfo The item information (output). 道具信息(输出)。 + * @return True if item information was found, false otherwise. 如果找到道具信息则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemCollection", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool FindItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Gets the item stack at a specific slot by tag (commented out). + * 通过槽标签获取指定槽的道具栈(已注释)。 + */ + virtual bool GetItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const; + + /** + * Finds the item stack at a specific slot by tag (commented out). + * 通过槽标签查找指定槽的道具栈(已注释)。 + */ + virtual bool FindItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const; + + /** + * Gets the item information at a specific slot by index. + * 通过槽索引获取指定槽的道具信息。 + * @param SlotIndex The slot index to query. 要查询的槽索引。 + * @return The item information at the slot. 槽中的道具信息。 + */ + FGIS_ItemInfo GetItemInfoAtSlot(int32 SlotIndex) const; + + /** + * Gets the item stack at a specific slot by index. + * 通过槽索引获取指定槽的道具栈。 + * @param SlotIndex The slot index to query. 要查询的槽索引。 + * @return The item stack at the slot. 槽中的道具栈。 + */ + FGIS_ItemStack GetItemStackAtSlot(int32 SlotIndex) const; + + /** + * Gets the slot tag where an item is equipped. + * 获取道具装备的槽标签。 + * @param Item The item instance to query. 要查询的道具实例。 + * @return The slot tag, or invalid if not equipped. 槽标签,如果未装备则返回无效标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + FGameplayTag GetItemSlotName(const UGIS_ItemInstance* Item) const; + + /** + * Gets the slot index where an item is equipped. + * 获取道具装备的槽索引。 + * @param Item The item instance to query. 要查询的道具实例。 + * @return The slot index, or -1 if not equipped. 槽索引,如果未装备则返回-1。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + int32 GetItemSlotIndex(const UGIS_ItemInstance* Item) const; + + /** + * Internal function to add an item to a specific slot. + * 内部函数,将道具添加到指定槽。 + * @param ItemInfo The item information to add. 要添加的道具信息。 + * @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。 + * @return The item that was actually added. 实际添加的道具。 + */ + virtual FGIS_ItemInfo AddItemInternal(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex); + + /** + * Converts a stack ID to a slot index. + * 将栈ID转换为槽索引。 + * @param InStackId The stack ID to query. 要查询的栈ID。 + * @return The corresponding slot index, or -1 if not found. 对应的槽索引,如果未找到则返回-1。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + int32 StackIdToSlotIndex(FGuid InStackId) const; + + /** + * Converts a slot index to a stack index. + * 将槽索引转换为栈索引。 + * @param InSlotIndex The slot index to query. 要查询的槽索引。 + * @return The corresponding stack index, or -1 if not found. 对应的栈索引,如果未找到则返回-1。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + int32 SlotIndexToStackIndex(int32 InSlotIndex) const; + + /** + * Converts a slot index to a stack ID. + * 将槽索引转换为栈ID。 + * @param InSlotIndex The slot index to query. 要查询的槽索引。 + * @return The corresponding stack ID, or invalid if not found. 对应的栈ID,如果未找到则返回无效ID。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection") + FGuid SlotIndexToStackId(int32 InSlotIndex) const; + + /** + * Mapping of slot indices to stack IDs for equipped items. + * 槽索引到装备道具栈ID的映射。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="ItemCollection", ReplicatedUsing=OnRep_ItemsBySlot) + TArray ItemBySlots; + + UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient) + TMap SlotToStackMap; + + /** + * The definition of this slot-based collection. + * 此基于槽的集合的定义。 + */ + UPROPERTY() + TObjectPtr MyDefinition; + + /** + * Called when the ItemBySlots array is replicated. + * ItemBySlots数组复制时调用。 + */ + UFUNCTION() + void OnRep_ItemsBySlot(); + +protected: + /** + * Sets the collection definition. + * 设置集合定义。 + * @param NewDefinition The new collection definition. 新的集合定义。 + */ + virtual void SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition) override; + + /** + * Called before an item stack is added to the collection. + * 在集合添加道具栈之前调用。 + * @param Stack The item stack to add. 要添加的道具栈。 + * @param Idx The index where the stack will be added. 栈将添加的索引。 + */ + virtual void OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx) override; + + virtual void OnItemStackAdded(const FGIS_ItemStack& Stack) override; + virtual void OnItemStackRemoved(const FGIS_ItemStack& Stack) override; + + /** + * Sets the amount for an item in a specific slot. + * 在指定槽中设置道具数量。 + * @param ItemInfo The item information to set. 要设置的道具信息。 + * @param SlotIndex The slot index to set the item in. 要设置的槽索引。 + * @param RemovePreviousItem Whether to replace the previous item if the slot is at capacity. 如果槽已满,是否替换之前的道具。 + * @param ItemInfoAdded The item information that was set (output). 已设置的道具信息(输出)。 + * @return True if the item was set successfully, false otherwise. 如果道具设置成功则返回true,否则返回false。 + */ + virtual bool SetItemAmount(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex, bool RemovePreviousItem, FGIS_ItemInfo& ItemInfoAdded); +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment.h new file mode 100644 index 0000000..620dda9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment.h @@ -0,0 +1,103 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_MixinTargetInterface.h" +#include "UObject/Object.h" +#include "GIS_ItemFragment.generated.h" + +class UGIS_ItemInstance; + +/** + * Base class for item fragments, which are data objects attached to item definitions. + * 道具片段,即道具定义上附加的数据对象的基类。 + * @details Allows users to create custom item fragment classes for extending item functionality. + * @细节 允许用户创建自定义道具片段类以扩展道具功能。 + */ +UCLASS(BlueprintType, Blueprintable, DefaultToInstanced, EditInlineNew, Abstract, CollapseCategories, HideDropdown, Const) +class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment : public UObject, public IGIS_MixinTargetInterface +{ + GENERATED_BODY() + +public: + /** + * Called when an item instance is created, allowing fragment-specific initialization. + * 在道具实例创建时调用,允许特定片段的初始化。 + * @param ItemInstance The item instance being created. 被创建的道具实例。 + */ + virtual void OnInstanceCreated(UGIS_ItemInstance* ItemInstance) const + { + Bp_OnInstancedCrated(ItemInstance); + } + + /** + * Determines if the fragment's runtime data can be serialized. + * 确定片段的运行时数据是否可以序列化。 + * @return True if the data is serializable, false otherwise. 如果数据可序列化则返回true,否则返回false。 + */ + virtual bool IsMixinDataSerializable() const override; + + /** + * Retrieves the compatible data structure for serialization or replication. + * 获取用于序列化或复制的兼容数据结构。 + * @return The script struct compatible with this fragment. 与此片段兼容的脚本结构。 + */ + virtual TObjectPtr GetCompatibleMixinDataType() const override; + + /** + * Generate default runtime data for compatible data type. + * 生成兼容的混合数据类型的默认值。 + * @param DefaultState The default value of compatible data. 兼容的数据结构默认值。 + * @return If has default value. 是否具备默认值? + */ + virtual bool MakeDefaultMixinData(FInstancedStruct& DefaultState) const override; + +protected: + /** + * Blueprint event for handling instance creation logic. + * 处理实例创建逻辑的蓝图事件。 + * @param ItemInstance The item instance being created. 被创建的道具实例。 + */ + UFUNCTION(BlueprintImplementableEvent, Category="ItemFragment", meta=(DisplayName="On Instance Created")) + void Bp_OnInstancedCrated(UGIS_ItemInstance* ItemInstance) const; + + /** + * Returns the struct type for runtime data serialization or replication. + * 返回运行时数据序列化或复制的结构类型。 + * @return The compatible runtime data struct type. 兼容的运行时数据结构类型。 + */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment") + const UScriptStruct* GetCompatibleStateType() const; + + /** + * Provide default values for fragment's runtime data type. + * 针对运行时数据类型提供默认值。 + * @param DefaultState The default data. 默认数据 + * @return If has default value. 是否具备默认值。 + */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment") + bool MakeDefaultState(FInstancedStruct& DefaultState) const; + + /** + * Determines if the fragment's runtime data supports serialization to disk. + * 确定片段的运行时数据是否支持序列化到磁盘。 + * @details Runtime data is replicated over the network, but serialization to disk is optional. + * @细节 运行时数据通过网络复制,但序列化到磁盘是可选的。 + * @return True if the data supports serialization, false otherwise. 如果数据支持序列化则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment") + bool IsStateSerializable() const; + +public: +#if WITH_EDITOR + /** + * Override this to add your custom data validation logic on save. + * @note Only called on editor, not runtime. + * @param OutMessage The output message. + * @return return true if no any data validation errors. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment") + bool FragmentDataValidation(FText& OutMessage) const; +#endif +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_DynamicAttributes.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_DynamicAttributes.h new file mode 100644 index 0000000..ab1c76a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_DynamicAttributes.h @@ -0,0 +1,89 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemFragment.h" +#include "Attributes/GIS_GameplayTagFloat.h" +#include "Attributes/GIS_GameplayTagInteger.h" +#include "GIS_ItemFragment_DynamicAttributes.generated.h" + +/** + * Item fragment for adding dynamic attributes to an item instance. + * 为道具实例添加动态属性的道具片段。 + * @details Initial attributes are applied to the item instance upon creation. + * @细节 初始属性在道具实例创建时应用。 + * @attention Use static attributes in the item definition for attributes that don't change at runtime. + * @注意 对于运行时不更改的属性,请使用道具定义中的静态属性。 + */ +UCLASS(DisplayName="Dynamic Attribute Settings", Category="BuiltIn") +class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_DynamicAttributes : public UGIS_ItemFragment +{ + GENERATED_BODY() + +protected: + /** + * Array of initial float attributes applied to– + +System: the item instance. + * 应用于道具实例的初始浮点属性数组。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Attribute, meta=(TitleProperty="{Tag} -> {Value}")) + TArray InitialFloatAttributes; + + /** + * Array of initial integer attributes applied to the item instance. + * 应用于道具实例的初始整型属性数组。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Attribute, meta=(TitleProperty="{Tag} -> {Value}")) + TArray InitialIntegerAttributes; + + /** + * Cached map for faster access to integer attributes. + * 用于快速访问整型属性的缓存映射。 + */ + UPROPERTY() + TMap IntegerAttributeMap; + + /** + * Cached map for faster access to float attributes. + * 用于快速访问浮点属性的缓存映射。 + */ + UPROPERTY() + TMap FloatAttributeMap; + +public: + /** + * Initializes the item instance with dynamic attributes upon creation. + * 在道具实例创建时使用动态属性进行初始化。 + * @param Instance The item instance to initialize. 要初始化的道具实例。 + */ + virtual void OnInstanceCreated(UGIS_ItemInstance* Instance) const override; + + /** + * Retrieves the default value of a float attribute by tag. + * 通过标签获取浮点属性的默认值。 + * @param AttributeTag The tag identifying the attribute. 标识属性的标签。 + * @return The default value of the float attribute. 浮点属性的默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemFragment|DynamicAttributes") + float GetFloatAttributeDefault(FGameplayTag AttributeTag) const; + + /** + * Retrieves the default value of an integer attribute by tag. + * 通过标签获取整型属性的默认值。 + * @param AttributeTag The tag identifying the attribute. 标识属性的标签。 + * @return The default value of the integer attribute. 整型属性的默认值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemFragment|DynamicAttributes") + int32 GetIntegerAttributeDefault(FGameplayTag AttributeTag) const; + +#if WITH_EDITOR + /** + * Called before saving the object to perform editor-specific operations. + * 在保存对象之前调用以执行编辑器特定的操作。 + * @param SaveContext The context for the save operation. 保存操作的上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Equippable.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Equippable.h new file mode 100644 index 0000000..9b40db4 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Equippable.h @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_EquipmentStructLibrary.h" +#include "GIS_ItemFragment.h" +#include "GIS_ItemFragment_Equippable.generated.h" + +class UGIS_EquipmentInstance; + +/** + * Item fragment for defining equippable item behavior. + * 定义可装备道具行为的道具片段。 + * @details Configures equipment instance and spawning behavior for equipped items. + * @细节 配置装备实例和已装备道具的生成行为。 + */ +UCLASS(DisplayName="Equippable Settings", Category="BuiltIn") +class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_Equippable : public UGIS_ItemFragment +{ + GENERATED_BODY() + +public: + /** + * Constructor for the equippable item fragment. + * 可装备道具片段的构造函数。 + */ + UGIS_ItemFragment_Equippable(); + + /** + * Specifies the equipment instance class handling equipment logic. + * 指定处理装备逻辑的装备实例类。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, NoClear, meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false")) + TSoftClassPtr InstanceType; + + /** + * Indicates if the equipment instance is actor-based, set automatically based on InstanceType. + * 指示装备实例是否基于Actor,根据InstanceType自动设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, meta=(EditCondition="false")) + bool bActorBased{false}; + + /** + * Determines if the equipment instance is automatically activated upon equipping. + * 确定装备实例在装备时是否自动激活。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment) + bool bAutoActivate{false}; + + /** + * List of actors to spawn on the pawn when the item is equipped, such as weapons or armor. + * 在道具装备时在Pawn上生成的Actor列表,如武器或盔甲。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, meta=(EditCondition="!bActorBased", EditConditionHides)) + TArray ActorsToSpawn; + +#if WITH_EDITOR + /** + * Called before saving the object to perform editor-specific operations. + * 在保存对象之前调用以执行编辑器特定的操作。 + * @param SaveContext The context for the save operation. 保存操作的上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Shoppable.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Shoppable.h new file mode 100644 index 0000000..183a57f --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Fragments/GIS_ItemFragment_Shoppable.h @@ -0,0 +1,42 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CurrencyEntry.h" +#include "GIS_ItemFragment.h" +#include "GIS_ItemFragment_Shoppable.generated.h" + +/** + * Item fragment for defining shop-related properties of an item. + * 定义道具商店相关属性的道具片段。 + * @details Specifies buy and sell prices for the item in various currencies. + * @细节 指定道具在不同货币中的购买和出售价格。 + */ +UCLASS(DisplayName="Shoppable Settings", Category="BuiltIn") +class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_Shoppable : public UGIS_ItemFragment +{ + GENERATED_BODY() + +public: + /** + * Initializes shop-related data for the item instance upon creation. + * 在道具实例创建时初始化商店相关数据。 + * @param Instance The item instance to initialize. 要初始化的道具实例。 + */ + virtual void OnInstanceCreated(UGIS_ItemInstance* Instance) const override; + + /** + * List of currencies and amounts required to purchase the item. + * 购买道具所需的货币和金额列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Shoppable, meta=(TitleProperty="Definition")) + TArray BuyCurrencyAmounts; + + /** + * List of currencies and amounts received when selling the item. + * 出售道具时获得的货币和金额列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Shoppable, meta=(TitleProperty="Definition")) + TArray SellCurrencyAmounts; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_CoreStructLibray.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_CoreStructLibray.h new file mode 100644 index 0000000..3de3905 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_CoreStructLibray.h @@ -0,0 +1,188 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/Texture2D.h" +#include "Styling/SlateBrush.h" +#include "GameplayTagContainer.h" +#include "UObject/Object.h" +#include "GIS_CoreStructLibray.generated.h" + +class UGIS_ItemInstance; +class UGIS_ItemDefinition; + +/** + * Structure representing an item definition with an associated amount. + * 表示道具定义及其关联数量的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_ItemDefinitionAmount +{ + GENERATED_BODY() + + FGIS_ItemDefinitionAmount(); + + + FGIS_ItemDefinitionAmount(TSoftObjectPtr InDefinition, int32 InAmount); + + /** + * The item definition. + * 道具定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TSoftObjectPtr Definition; + + /** + * The amount of the item. + * 道具数量。 + * @attention Minimum value is 1. + * @注意 最小值为1。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(ClampMin="1")) + int32 Amount = 1; + +#if WITH_EDITORONLY_DATA + + /** + * Friendly name for displaying in the editor. + * 在编辑器中显示的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Structure representing a default loadout with a tag and associated items. + * 表示默认装备配置的结构体,包含标签和关联的道具。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_DefaultLoadout +{ + GENERATED_BODY() + + /** + * The gameplay tag for the loadout. + * 装备配置的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGameplayTag Tag; + + /** + * List of default items for the loadout. + * 装备配置的默认道具列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="EditorFriendlyName")) + TArray DefaultItems; +}; + +/** + * Structure defining options for handling item overflow in an inventory. + * 定义库存中道具溢出处理选项的结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemOverflowOptions +{ + GENERATED_BODY() + + /** + * Whether to return overflow items to their source (if specified) when the inventory is full. + * 当库存满时,是否将溢出的道具返回其来源(如果指定)。 + */ + UPROPERTY(EditAnywhere, Category=Overflow) + bool bReturnOverflow = true; + + /** + * Whether to send a rejection message when items cannot be added due to overflow. + * 当道具因溢出无法添加时,是否发送拒绝消息。 + */ + UPROPERTY(EditAnywhere, Category=Overflow) + bool bSendRejectedMessage = true; +}; + +/** + * Structure representing an item slot definition, primarily used in item slot collections. + * 表示道具槽定义的结构体,主要用于道具槽集合。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_ItemSlotDefinition +{ + GENERATED_BODY() + + /** + * The gameplay tag for the item slot. + * 道具槽的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot, meta=(Categories="GIS.Slots")) + FGameplayTag Tag; + + /** + * The icon for the item slot. + * 道具槽的图标。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot) + TSoftObjectPtr Icon; + + /** + * The Slate brush for the item slot icon. + * 道具槽图标的Slate画刷。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot) + FSlateBrush IconBrush; + + /** + * The display name of the item slot. + * 道具槽的显示名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot) + FText Name; + + /** + * The description of the item slot. + * 道具槽的描述。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot) + FText Desc; + + /** + * Gameplay tag query that item tags must match to be equipped in this slot. + * 道具标签必须匹配的游戏标签查询,才能装备到此槽。 + * @attention If empty, no items can be equipped in this slot. + * @注意 如果为空,则无法将任何道具装备到此槽。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot) + FGameplayTagQuery TagQuery; + + /** + * Checks if an item can be equipped in this slot based on its tags. + * 检查道具是否可以根据其标签装备到此槽。 + * @param Item The item instance to check. 要检查的道具实例。 + * @return True if the item matches the slot's tag query, false otherwise. 如果道具匹配槽的标签查询则返回true,否则返回false。 + */ + bool MatchItem(const UGIS_ItemInstance* Item) const; +}; + +/** + * Structure representing a group of item slots with index-to-slot and slot-to-index mappings. + * 表示一组道具槽的结构体,包含索引到槽和槽到索引的映射。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemSlotGroup +{ + GENERATED_BODY() + + /** + * Mapping of indices to slot tags in the group. + * 组内索引到槽标签的映射。 + */ + UPROPERTY(VisibleAnywhere, Category=ItemSlot, meta=(ForceInlineRow)) + TMap IndexToSlotMap; + + /** + * Mapping of slot tags to indices in the group. + * 组内槽标签到索引的映射。 + */ + UPROPERTY(VisibleAnywhere, Category=ItemSlot, meta=(ForceInlineRow)) + TMap SlotToIndexMap; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinContainer.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinContainer.h new file mode 100644 index 0000000..8b88d50 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinContainer.h @@ -0,0 +1,383 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "Templates/SubclassOf.h" +#include "UObject/Object.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GIS_MixinContainer.generated.h" + +/** + * Record of mixin for serialization. + * 用于序列化的混合数据记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_MixinRecord +{ + GENERATED_BODY() + + /** + * The asset path of target object. + * 目标对象的资产路径。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FString TargetPath; + + /** + * The runtime state of this fragment. + * 片段的运行时数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FInstancedStruct Data; + + /** + * Equality operator to compare mixin records. + * 比较混合数据记录的相等性运算符。 + * @param Other The another mixin record to compare with. 要比较的其他Mixin记录。 + * @return True if the record's target path are equal, false otherwise. 如果目标路径相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_MixinRecord& Other) const; + + /** + * Checks if the mixin record is valid. + * 检查Mixin记录是否有效。 + * @return True if the target path and data are valid, false otherwise. 如果片段类型和状态有效则返回true,否则返回false。 + */ + bool IsValid() const; +}; + + +/** + * Stores the mixed-in data of a const target object (attaches additional runtime data to a const object). + * 针对常量目标对象存储其混合数据(将额外的运行时数据附加到常量对象)。 + * @details A wrapper of InstancedStruct, allowing storage of any struct data. For save games, ensure struct fields are serializable (marked with SaveGame). + * @细节 这是实例化结构的包装,允许存储任意结构体数据。对于存档,应确保结构体字段标记为SaveGame并受虚幻序列化系统支持。 + */ +USTRUCT(BlueprintType) +struct FGIS_Mixin : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * The target object to which the data is attached. + * 数据附加的目标对象。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin") + TObjectPtr Target; + + /** + * The mixed-in data attached to the target object. + * 附加到目标对象的混合数据。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin") + FInstancedStruct Data; + + /** + * Timestamp of the last data change. + * 数据最后更改的时间戳。 + */ + UPROPERTY() + float Timestamp; + + /** + * Timestamp of the last replication (not replicated). + * 最后一次复制的时间戳(不复制)。 + */ + UPROPERTY(NotReplicated) + float LastReplicatedTimestamp; + + /** + * Default constructor for mixin. + * 混合数据的默认构造函数。 + */ + FGIS_Mixin() + : FGIS_Mixin(nullptr, FInstancedStruct()) + { + } + + /** + * Constructor for mixin with target class and data. + * 使用目标类和数据构造混合数据。 + * @param InClass The target class for the mixin. 混合数据的目标类。 + * @param InData The instanced struct data to mix in. 要混合的实例化结构数据。 + */ + FGIS_Mixin(const TSubclassOf& InClass, const FInstancedStruct& InData) + : Target(InClass), Data(InData) + { + Timestamp = 0.f; + LastReplicatedTimestamp = 0.f; + } + + /** + * Destructor for mixin. + * 混合数据的析构函数。 + */ + ~FGIS_Mixin() + { + Reset(); + } + + /** + * Checks if the mixin is valid. + * 检查混合数据是否有效。 + * @return True if the mixin is valid (target and data are set), false otherwise. 如果混合数据有效(目标和数据已设置)则返回true,否则返回false。 + */ + bool IsValid() const + { + return Target != nullptr && Data.IsValid(); + } + + /** + * Resets the mixin to its default state. + * 将混合数据重置为默认状态。 + */ + void Reset() + { + Target = nullptr; + Data.Reset(); + } + + /** + * Computes the hash value for the mixin entry. + * 计算混合数据条目的哈希值。 + * @param Entry The mixin entry to hash. 要哈希的混合数据条目。 + * @return The hash value. 哈希值。 + */ + friend uint32 GetTypeHash(const FGIS_Mixin& Entry); +}; + +/** + * Template specialization to enable copying for FGIS_Mixin. + * 为 FGIS_Mixin 启用复制的模板特化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithCopy = true + }; +}; + +/** + * Container for storing runtime data for various const objects within the owning object. + * 用于存储拥有对象中多个常量对象的运行时数据的容器。 + * @details For example, ItemInstance uses this container to store runtime data for each fragment, as fragments themselves are const (cannot be modified at runtime). Only necessary runtime data is serialized/replicated instead of the entire fragments. + * @细节 例如,ItemInstance 使用此容器存储每个片段的运行时数据,因为片段本身是常量(运行时无法修改)。仅序列化/复制必要的运行时数据,而不是整个片段。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_MixinContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for mixin container. + * 混合数据容器的默认构造函数。 + */ + FGIS_MixinContainer() + : OwningObject(nullptr) + { + } + + /** + * Constructor for mixin container with an owning object. + * 使用所属对象构造混合数据容器。 + * @param NewObject The object that owns this container. 拥有此容器的对象。 + */ + explicit FGIS_MixinContainer(UObject* NewObject) + : OwningObject(NewObject) + { + } + + /** + * Retrieves data for a target class. + * 获取目标类的数据。 + * @param TargetClass The class of the target object. 目标对象的类。 + * @param OutData The retrieved instanced struct data (output). 检索到的实例化结构数据(输出)。 + * @return True if data was found, false otherwise. 如果找到数据则返回true,否则返回false。 + */ + // bool GetDataByTargetClass(const TSubclassOf& TargetClass, FInstancedStruct& OutData) const; + + /** + * Retrieves data for a target object. + * 获取目标对象的数据。 + * @param Target The target object which the data attached to. 数据所附加到的目标对象。 + * @param OutData The retrieved instanced struct data (output). 检索到的实例化结构数据(输出)。 + * @return True if data was found, false otherwise. 如果找到数据则返回true,否则返回false。 + */ + bool GetDataByTarget(const UObject* Target, FInstancedStruct& OutData) const; + + /** + * Finds the index of a target object in the container. + * 查找容器中目标对象的索引。 + * @param Target The target object to find. 要查找的目标对象。 + * @return The index of the target object, or INDEX_NONE if not found. 目标对象的索引,如果未找到则返回INDEX_NONE。 + */ + int32 IndexOfTarget(const UObject* Target) const; + + /** + * Finds the index of a target class in the container. + * 查找容器中目标类的索引。 + * @param TargetClass The class of the target object. 目标对象的类。 + * @return The index of the target class, or INDEX_NONE if not found. 目标类的索引,如果未找到则返回INDEX_NONE。 + */ + int32 IndexOfTargetByClass(const TSubclassOf& TargetClass) const; + + /** + * Sets data for a target object. + * 为目标对象设置数据。 + * @param Target The target object. 目标对象。 + * @param Data The instanced struct data to set. 要设置的实例化结构数据。 + * @return The index of the updated or added entry. 更新或添加条目的索引。 + */ + int32 SetDataForTarget(const TObjectPtr& Target, const FInstancedStruct& Data); + + bool IsObjectLoadedFromDisk(const UObject* Object) const; + + /** + * Updates data for a target class. + * 更新目标类的数据。 + * @param TargetClass The class of the target object. 目标对象的类。 + * @param Data The instanced struct data to update. 要更新的实例化结构数据。 + * @return The index of the updated entry, or INDEX_NONE if not found. 更新条目的索引,如果未找到则返回INDEX_NONE。 + */ + int32 UpdateDataByTargetClass(const TSubclassOf& TargetClass, const FInstancedStruct& Data); + + /** + * Removes data associated with a target class. + * 移除与目标类关联的数据。 + * @param TargetClass The class of the target object. 目标对象的类。 + */ + void RemoveDataByTargetClass(const TSubclassOf& TargetClass); + + /** + * Checks if the data is compatible with the target object. + * 检查数据是否与目标对象兼容。 + * @param Target The target object. 目标对象。 + * @param Data The instanced struct data to check. 要检查的实例化结构数据。 + * @return True if the data is compatible, false otherwise. 如果数据兼容则返回true,否则返回false。 + */ + bool CheckCompatibility(const UObject* Target, const FInstancedStruct& Data) const; + + /** + * Retrieves all data stored in the container. + * 获取容器中存储的所有数据。 + * @return Array of instanced struct data. 实例化结构数据的数组。 + */ + TArray GetAllData() const; + + /** + * Retrieves all serializable mixin in the container. + * 获取容器中所有可序列化的mixin + * @return Array of serializable mixin. 可序列化的Mixin数组 + */ + TArray GetSerializableMixins() const; + + TArray GetSerializableMixinRecords() const; + + void RestoreFromRecords(const TArray& Records); + + static TArray ConvertRecordsToMixins(const TArray& Records); + + /** + * Retrieves all serializable data stored in the container. + * 获取容器中存储的所有可序列化数据。 + * @return Array of serializable instanced struct data. 可序列化实例化结构数据的数组。 + */ + TArray GetAllSerializableData() const; + + // -- Begin FFastArraySerializer implementation + /** + * Called after items are added during replication. + * 复制期间在添加项目后调用。 + * @param AddedIndices The indices of added items. 添加项目的索引。 + * @param FinalSize The final size of the array after addition. 添加后数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after items are changed during replication. + * 复制期间在项目更改后调用。 + * @param ChangedIndices The indices of changed items. 更改项目的索引。 + * @param FinalSize The final size of the array after changes. 更改后数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + + /** + * Called before items are removed during replication. + * 复制期间在移除项目前调用。 + * @param RemovedIndices The indices of removed items. 移除项目的索引。 + * @param FinalSize The final size of the array after removal. 移除后数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Handles delta serialization for the mixin container. + * 处理混合数据容器的增量序列化。 + * @param DeltaParams The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams); + // -- End FFastArraySerializer implementation + +protected: + /** + * Caches mixin data for faster access. + * 缓存混合数据以加速访问。 + */ + void CacheMixins(); + + /** + * Updates data at a specific index. + * 在指定索引更新数据。 + * @param Idx The index of the entry to update. 要更新的条目索引。 + * @param Data The instanced struct data to set. 要设置的实例化结构数据。 + * @return The index of the updated entry. 更新条目的索引。 + */ + int32 UpdateDataAt(int32 Idx, const FInstancedStruct& Data); + + /** + * The object that owns this container. + * 拥有此容器的对象。 + */ + UPROPERTY() + TObjectPtr OwningObject; + + /** + * List of a target object to data pairs. + * 目标对象到数据的键值对列表。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin") + TArray Mixins; + + /** + * Cached map for faster access to mixin data (index may vary between client and server). + * 用于加速访问混合数据的缓存映射(客户端和服务器的索引可能不同)。 + */ + UPROPERTY(NotReplicated) + TMap, int32> AcceleratedMap; + + /** + * Hash of the last cached state. + * 最后缓存状态的哈希值。 + */ + UPROPERTY() + uint32 LastCachedHash = INDEX_NONE; +}; + +/** + * Template specialization to enable network delta serialization for the mixin container. + * 为混合数据容器启用网络增量序列化的模板特化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum { WithNetDeltaSerializer = true }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinOwnerInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinOwnerInterface.h new file mode 100644 index 0000000..e574a84 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinOwnerInterface.h @@ -0,0 +1,59 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "UObject/Interface.h" +#include "GIS_MixinOwnerInterface.generated.h" + +/** + * Interface class for objects that own a mixin container. + * 拥有混合数据容器的对象的接口类。 + */ +UINTERFACE() +class GENERICINVENTORYSYSTEM_API UGIS_MixinOwnerInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for objects that own a mixin container, to be notified of mixin data changes. + * 拥有混合数据容器的对象应实现的接口,用于接收混合数据变更通知。 + * @details For example, an item instance will implement this interface to get notified when data attached to different fragment classes is added, changed, or removed. + * @细节 例如,道具实例将实现此接口,以在附加到不同片段类的数据被添加、更改或移除时收到通知。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_MixinOwnerInterface +{ + GENERATED_BODY() + +public: + /** + * Called when mixin data is added to the owning object. + * 当混合数据被添加到拥有对象时调用。 + * @param Target The target object to which the data is attached. 数据附加到的对象。 + * @param Data The added instanced struct data. 添加的实例化结构数据。 + */ + virtual void OnMixinDataAdded(const TObjectPtr& Target, const FInstancedStruct& Data) = 0; + + /** + * Called when mixin data is updated in the owning object. + * 当拥有对象中的混合数据被更新时调用。 + * @param Target The target object to which the data is attached. 数据附加到的对象。 + * @param Data The updated instanced struct data. 更新的实例化结构数据。 + */ + virtual void OnMixinDataUpdated(const TObjectPtr& Target, const FInstancedStruct& Data) = 0; + + /** + * Called when mixin data is removed from the owning object. + * 当从拥有对象中移除混合数据时调用。 + * @param Target The target object to which the data was attached. 数据附加到的对象。 + * @param Data The removed instanced struct data. 移除的实例化结构数据。 + */ + virtual void OnMixinDataRemoved(const TObjectPtr& Target, const FInstancedStruct& Data) = 0; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinTargetInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinTargetInterface.h new file mode 100644 index 0000000..64bebf6 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/GIS_MixinTargetInterface.h @@ -0,0 +1,53 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "UObject/Interface.h" +#include "GIS_MixinTargetInterface.generated.h" + +UINTERFACE(meta=(CannotImplementInterfaceInBlueprint)) +class UGIS_MixinTargetInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for objects to handle data mixin functionality. + * 处理数据混合功能的接口。 + * @details Defines methods for serialization and compatibility of mixin data. + * @细节 定义了混合数据的序列化和兼容性方法。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_MixinTargetInterface +{ + GENERATED_BODY() + +public: + /** + * Determines if the mixin data can be serialized. + * 确定混合数据是否可以序列化。 + * @return True if the data is serializable, false otherwise. 如果数据可序列化则返回true,否则返回false。 + */ + virtual bool IsMixinDataSerializable() const = 0; + + /** + * Retrieves the compatible data structure for the mixin. + * 获取混合数据的兼容数据结构。 + * @return The script struct compatible with the mixin data. 与混合数据兼容的脚本结构。 + */ + virtual TObjectPtr GetCompatibleMixinDataType() const = 0; + + /** + * Generate default runtime data for compatible data type. + * 生成兼容的混合数据类型的默认值。 + * @param DefaultState The default value of compatible data. 兼容的数据结构默认值。 + * @return If has default value. 是否具备默认值? + */ + virtual bool MakeDefaultMixinData(FInstancedStruct& DefaultState) const = 0; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinition.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinition.h new file mode 100644 index 0000000..48a4dcd --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinition.h @@ -0,0 +1,188 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_GameplayTagFloat.h" +#include "GIS_GameplayTagInteger.h" +#include "Engine/DataAsset.h" +#include "GIS_ItemDefinition.generated.h" + +class UGIS_ItemDefinitionSchema; +class UGIS_ItemFragment; +class UGIS_ItemInstance; +class UTexture2D; + +/** + * An item definition is a data asset. Creating a new item definition is equivalent to creating a new instance of the item definition class. + * Essentially, item definitions are static data that can incorporate item data fragments to achieve complex data structures. + * 道具定义是一个数据资产,创建一个新道具定义相当于新建一个类型为道具定义的资产。道具定义本质上是静态数据,可以添加道具数据片段,以实现复杂的数据结构。 + */ +UCLASS(BlueprintType, Const) +class GENERICINVENTORYSYSTEM_API UGIS_ItemDefinition : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /** + * Constructor for the item definition. + * 道具定义的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_ItemDefinition(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Display name of the item on the UI. + * 道具在UI上的显示名称。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display) + FText DisplayName; + + /** + * Description of the item on the UI. + * 道具在UI上的描述。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display) + FText Description; + + /** + * Icon of the item on the UI. + * 道具在UI上的图标。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display) + TObjectPtr Icon; + + /** + * Indicates whether the item is unique, preventing it from being stacked. + * 表示道具是否唯一,唯一道具不可堆叠。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Common) + bool bUnique; + + /** + * Gameplay tags associated with the item for querying, categorization, and validation. + * 与道具关联的游戏标签,用于查询、分类和验证。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Common, meta=(Categories="GIS.Item")) + FGameplayTagContainer ItemTags; + + /** + * Static tag-to-float attributes for the item definition. + * 道具定义的静态标签到浮点数的属性映射。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute")) + TArray StaticFloatAttributes; + + /** + * Static tag-to-integer attributes for the item definition. + * 道具定义的静态标签到整数的属性映射。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute")) + TArray StaticIntegerAttributes; + + /** + * Array of item data fragments to form simple or complex data structures. No duplicate fragment types are allowed. + * 数据片段数组,用于构成简单或复杂的道具数据结构。不允许重复的片段类型。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, Instanced, meta=(NoElementDuplicate)) + TArray> Fragments; + + /** + * Gets a fragment by its class. + * 通过类获取数据片段。 + * @param FragmentClass The class of the fragment to retrieve. 要检索的片段类。 + * @return The fragment instance, or nullptr if not found. 片段实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition") + const UGIS_ItemFragment* GetFragment(TSubclassOf FragmentClass) const; + + /** + * Checks if the item definition has a specific float attribute. + * 检查道具定义是否具有特定的浮点属性。 + * @param AttributeTag The tag of the attribute to check. 要检查的属性标签。 + * @return True if the attribute exists, false otherwise. 如果属性存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition") + bool HasFloatAttribute(FGameplayTag AttributeTag) const; + + /** + * Gets the value of a static float attribute. + * 获取静态浮点属性的值。 + * @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。 + * @return The value of the float attribute, or 0 if not found. 浮点属性的值,如果未找到则返回0。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition") + float GetFloatAttribute(FGameplayTag AttributeTag) const; + + /** + * Checks if the item definition has a specific integer attribute. + * 检查道具定义是否具有特定的整数属性。 + * @param AttributeTag The tag of the attribute to check. 要检查的属性标签。 + * @return True if the attribute exists, false otherwise. 如果属性存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition") + bool HasIntegerAttribute(FGameplayTag AttributeTag) const; + + /** + * Gets the value of a static integer attribute. + * 获取静态整数属性的值。 + * @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。 + * @return The value of the integer attribute, or 0 if not found. 整数属性的值,如果未找到则返回0。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition") + int32 GetIntegerAttribute(FGameplayTag AttributeTag) const; + + /** + * Template function to find a fragment by its class. + * 模板函数,通过类查找数据片段。 + * @param ResultClass The class of the fragment to find. 要查找的片段类。 + * @return The fragment instance cast to the specified class, or nullptr if not found. 转换为指定类的片段实例,如果未找到则返回nullptr。 + */ + template + const ResultClass* FindFragment() const + { + return static_cast(GetFragment(ResultClass::StaticClass())); + } + + /** + * Cached map for faster access to integer attributes. + * 用于快速访问整数属性的缓存映射。 + */ + UPROPERTY() + TMap IntegerAttributeMap; + + /** + * Cached map for faster access to float attributes. + * 用于快速访问浮点属性的缓存映射。 + */ + UPROPERTY() + TMap FloatAttributeMap; + + /** + * Finds a fragment in an item definition by its class. + * 在道具定义中通过类查找数据片段。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param FragmentClass The class of the fragment to find. 要查找的片段类。 + * @return The fragment instance, or nullptr if not found. 片段实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemDefinition", meta=(DeterminesOutputType=FragmentClass)) + static const UGIS_ItemFragment* FindFragmentOfItemDefinition(TSoftObjectPtr ItemDefinition, TSubclassOf FragmentClass); + +#if WITH_EDITOR + /** + * Called before the item definition is saved in the editor. + * 编辑器中道具定义保存前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates the item definition's data in the editor. + * 在编辑器中验证道具定义的数据。 + * @param Context The validation context. 验证上下文。 + * @return The result of the data validation. 数据验证的结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinitionSchema.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinitionSchema.h new file mode 100644 index 0000000..9aea5c1 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemDefinitionSchema.h @@ -0,0 +1,171 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_GameplayTagFloat.h" +#include "GIS_GameplayTagInteger.h" +#include "Engine/DataAsset.h" +#include "GIS_ItemDefinitionSchema.generated.h" + +class UGIS_ItemInstance; +class UGIS_ItemDefinition; +class UGIS_ItemFragment; + +/** + * Structure representing a validation entry for item definitions. + * 表示道具定义验证条目的结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemDefinitionValidationEntry +{ + GENERATED_BODY() + + /** + * Gameplay tag query to match items for this validation. + * 用于匹配此验证的游戏标签查询。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(Categories="GIS.Item")) + FGameplayTagQuery ItemTagQuery; + + /** + * The required item instance class for matching items. + * 匹配道具所需的道具实例类。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TSoftClassPtr InstanceClass; + + /** + * List of required fragment classes for the item definition. + * 道具定义所需的片段类列表。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TArray> RequiredFragments; + + /** + * List of forbidden fragment classes for the item definition. + * 道具定义禁止的片段类列表。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TArray> ForbiddenFragments; + + /** + * List of required float attributes with default values. + * 必需的浮点属性列表,包含默认值。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(TitleProperty="{Tag} -> {Value}")) + TArray RequiredFloatAttributes; + + /** + * List of required integer attributes with default values. + * 必需的整数属性列表,包含默认值。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(TitleProperty="{Tag} -> {Value}")) + TArray RequiredIntegerAttributes; + + /** + * Whether to enforce the bUnique property. + * 是否强制执行 bUnique 属性。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + bool bEnforceUnique = false; + + /** + * The required value for bUnique if enforced. + * 如果强制执行 bUnique,则指定其值。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(EditCondition="bEnforceUnique")) + bool RequiredUniqueValue = false; +}; + +/** + * The item definition schema is a data asset used in the editor for item definition data validation. + * 道具定义模式是一种数据资产,用于在编辑器中进行道具定义的数据验证。 + */ +UCLASS(NotBlueprintable) +class GENERICINVENTORYSYSTEM_API UGIS_ItemDefinitionSchema : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Validates an item definition against the schema. + * 针对模式验证道具定义。 + * @param Definition The item definition to validate. 要验证的道具定义。 + * @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。 + * @return True if the validation passes, false otherwise. 如果验证通过则返回true,否则返回false。 + */ + static bool TryValidateItemDefinition(const UGIS_ItemDefinition* Definition, FText& OutError); + + /** + * Performs pre-save validation and auto-fixes for an item definition. + * 对道具定义进行保存前验证和自动修复。 + * @param Definition The item definition to validate and fix. 要验证和修复的道具定义。 + * @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。 + */ + static void TryPreSaveItemDefinition(UGIS_ItemDefinition* Definition, FText& OutError); + + /** + * Validates an item definition against the schema (instance method). + * 针对模式验证道具定义(实例方法)。 + * @param Definition The item definition to validate. 要验证的道具定义。 + * @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。 + * @return True if the validation passes, false otherwise. 如果验证通过则返回true,否则返回false。 + */ + virtual bool TryValidate(const UGIS_ItemDefinition* Definition, FText& OutError) const; + + /** + * Performs pre-save validation and auto-fixes for an item definition (instance method). + * 对道具定义进行保存前验证和自动修复(实例方法)。 + * @param Definition The item definition to validate and fix. 要验证和修复的道具定义。 + * @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。 + */ + virtual void TryPreSave(UGIS_ItemDefinition* Definition, FText& OutError) const; + +#if WITH_EDITOR + /** + * Called before the schema is saved in the editor. + * 编辑器中模式保存前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates the schema's data in the editor. + * 在编辑器中验证模式的数据。 + * @param Context The validation context. 验证上下文。 + * @return The result of the data validation. 数据验证的结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif + +protected: + /** + * Array of validation entries for the schema. + * 模式的验证条目数组。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TArray ValidationEntries; + + /** + * List of required fragment classes for all item definitions. + * 所有道具定义所需的常规片段类列表。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TArray> CommonRequiredFragments; + + /** + * The required parent tag for all item definitions' ItemTags. + * 所有道具定义的 ItemTags 必须包含的父级标签。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + FGameplayTag RequiredParentTag; + + /** + * Global fragment order for all item definitions. + * 所有道具定义的全局片段排序。 + */ + UPROPERTY(EditDefaultsOnly, Category = Schema) + TArray> FragmentOrder; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInfo.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInfo.h new file mode 100644 index 0000000..b42a4fc --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInfo.h @@ -0,0 +1,187 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Items/GIS_ItemStack.h" +#include "UObject/Object.h" +#include "GIS_ItemInfo.generated.h" + +struct FGIS_ItemStack; +class UGIS_ItemCollection; +class UGIS_ItemInstance; + +/** + * Item information is a temporary structure used to pass item-related information across different operations, such as its belonging collection, amount, and corresponding item instance. + * 道具信息是一个临时的结构体,用于在不同的操作中传递道具相关信息,例如其所属集合、数量及对应的道具实例等。 + */ +USTRUCT(BlueprintType, DisplayName="GIS Item Information") +struct GENERICINVENTORYSYSTEM_API FGIS_ItemInfo +{ + GENERATED_BODY() + + /** + * Default constructor for item information. + * 道具信息的默认构造函数。 + */ + FGIS_ItemInfo(); + + /** + * Constructor for item information with item, amount, and collection. + * 使用道具、数量和集合构造道具信息。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + * @param InCollection The item collection where the item originates. 道具来源的集合。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection); + + /** + * Constructor for item information with item, amount, collection, and stack ID. + * 使用道具、数量、集合和堆栈ID构造道具信息。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + * @param InCollection The item collection where the item originates. 道具来源的集合。 + * @param InStackId The stack ID associated with the item. 与道具关联的堆栈ID。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, FGuid InStackId); + + /** + * Constructor for item information with item and amount. + * 使用道具和数量构造道具信息。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount); + + /** + * Constructor for item information with item, amount, and collection ID. + * 使用道具、数量和集合ID构造道具信息。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + * @param InCollectionId The collection ID associated with the item. 与道具关联的集合ID。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGuid InCollectionId); + + /** + * Constructor for item information with item, amount, and collection tag. + * 使用道具、数量和集合标签构造道具信息。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + * @param InCollectionTag The collection tag associated with the item. 与道具关联的集合标签。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGameplayTag InCollectionTag); + + /** + * Constructor for item information by copying another info and changing the amount. + * 通过复制其他道具信息并更改数量构造道具信息。 + * @param InAmount The new item amount. 新的道具数量。 + * @param OtherInfo The item info to copy. 要复制的道具信息。 + */ + FGIS_ItemInfo(int32 InAmount, const FGIS_ItemInfo& OtherInfo); + + FGIS_ItemInfo(int32 InAmount, int32 InIndex, const FGIS_ItemInfo& OtherInfo); + + /** + * Constructor for item information by copying another info with a new item and amount. + * 通过复制其他道具信息并指定新道具和数量构造道具信息。 + * @param InItem The new item instance. 新的道具实例。 + * @param InAmount The new item amount. 新的道具数量。 + * @param OtherInfo The item info to copy. 要复制的道具信息。 + */ + FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, const FGIS_ItemInfo& OtherInfo); + + /** + * Constructor for item information from an item stack. + * 从道具堆栈构造道具信息。 + * @param ItemStack The item stack to convert from. 要转换的道具堆栈。 + */ + FGIS_ItemInfo(const FGIS_ItemStack& ItemStack); + + /** + * Gets a debug string representation of the item information. + * 获取道具信息的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString() const; + + /** + * Compares this item info with another for equality. + * 将此道具信息与另一个比较以判断是否相等。 + * @param Other The other item info to compare with. 要比较的其他道具信息。 + * @return True if the item infos are equal, false otherwise. 如果道具信息相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_ItemInfo& Other) const; + + /** + * Checks if the item information is valid. + * 检查道具信息是否有效。 + * @return True if the item info is valid, false otherwise. 如果道具信息有效则返回true,否则返回false。 + */ + bool IsValid() const; + + /** + * Gets the inventory system component associated with the item collection. + * 获取与道具集合关联的库存系统组件。 + * @return The inventory system component, or nullptr if not found. 库存系统组件,如果未找到则返回nullptr。 + */ + UGIS_InventorySystemComponent* GetInventory() const; + + /** + * Static constant representing an invalid or empty item info. + * 表示无效或空道具信息的静态常量。 + */ + static FGIS_ItemInfo None; + + /** + * The item instance associated with this information, with context-dependent usage. + * 与此信息关联的道具实例,用途因上下文而异。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(DisplayName="Item Instance")) + TObjectPtr Item; + + /** + * The amount of the item instance in this information. + * 此信息中道具实例的数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(DisplayName="Item Amount")) + int32 Amount; + + /** + * The collection instance where the information originates, with context-dependent usage. + * 此信息的源集合,用途因上下文而异。 + * @attention For adding items, it specifies the source collection for overflow return; for retrieving items, it indicates the source collection. + * @注意 添加道具时,指定溢出返回的源集合;取回道具时,表示来源集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TObjectPtr ItemCollection; + + /** + * The stack ID associated with this information, with context-dependent usage. + * 与此信息关联的堆栈ID,用途因上下文而异。 + * @attention For adding/removing items, it determines the target stack; for retrieving items, it indicates the source stack. + * @注意 添加/移除道具时,用于确定目标堆栈;取回道具时,表示来源堆栈。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGuid StackId; + + /** + * The collection ID associated with this information, with context-dependent usage. + * 与此信息关联的集合ID,用途因上下文而异。 + * @attention For adding/removing items, it determines the target collection; for retrieving items, it is usually null. + * @注意 添加/移除道具时,用于确定目标集合;取回道具时,通常为空。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGuid CollectionId; + + /** + * The collection tag associated with this information, with context-dependent usage. + * 与此信息关联的集合标签,用途因上下文而异。 + * @attention For adding/removing items, it determines the target collection (lower priority than CollectionId); for retrieving items, it is usually null. + * @注意 添加/移除道具时,用于确定目标集合(优先级低于CollectionId);取回道具时,通常为空。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(Categories="GIS.Collection")) + FGameplayTag CollectionTag; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + int32 Index{INDEX_NONE}; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInstance.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInstance.h new file mode 100644 index 0000000..837b7b5 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInstance.h @@ -0,0 +1,579 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagAssetInterface.h" +#include "GameplayTagContainer.h" +#include "Attributes/GIS_GameplayTagFloat.h" +#include "Attributes/GIS_GameplayTagInteger.h" +#include "GIS_MixinContainer.h" +#include "GIS_MixinOwnerInterface.h" +#include "UObject/Object.h" +#include "GIS_ItemInstance.generated.h" + +class UGIS_InventorySystemComponent; +class UGIS_ItemFragment; +struct FGIS_ItemStack; +class UGIS_ItemCollection; +class UGIS_ItemDefinition; + +/** + * Delegate triggered when fragment data is added, updated, or removed. + * 当片段数据被添加、更新或移除时触发的委托。 + * @param Fragment The item fragment associated with the event. 与事件关联的道具片段。 + * @param Data The instanced struct containing the fragment data. 包含片段数据的实例化结构体。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGIS_ItemFragmentStateEventSignature, const UGIS_ItemFragment*, Fragment, const FInstancedStruct&, Data); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_ItemIntegerAttributeChangedEventSignature, const FGameplayTag&, AttributeTag, int32, OldValue, int32, NewValue); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_ItemFloatAttributeChangedEventSignature, const FGameplayTag&, AttributeTag, float, OldValue, float, NewValue); + + +/** + * The item instance created via item definition. + * 通过道具定义创建的道具实例。 + */ +UCLASS(BlueprintType, Blueprintable, CollapseCategories) +class GENERICINVENTORYSYSTEM_API UGIS_ItemInstance : public UObject, public IGameplayTagAssetInterface, public IGIS_MixinOwnerInterface, public IGIS_GameplayTagFloatContainerOwner, + public IGIS_GameplayTagIntegerContainerOwner +{ + GENERATED_BODY() + +public: + /** + * Constructor for the item instance. + * 道具实例的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_ItemInstance(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Gets the gameplay tags owned by this item instance. + * 获取此道具实例拥有的游戏标签。 + * @param TagContainer The container to store the tags. 存储标签的容器。 + */ + virtual void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const override; + + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Checks if the item instance supports networking. + * 检查道具实例是否支持网络。 + * @return True if networking is supported, false otherwise. 如果支持网络则返回true,否则返回false。 + */ + virtual bool IsSupportedForNetworking() const override { return true; }; + + /** + * Gets the unique ID of the item instance. + * 获取道具实例的唯一ID。 + * @return The item instance's unique ID. 道具实例的唯一ID。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual FGuid GetItemId() const; + + /** + * Sets the unique ID of the item instance. + * 设置道具实例的唯一ID。 + * @param NewId The new ID to set. 要设置的新ID。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual void SetItemId(FGuid NewId); + + /** + * Checks if the item instance is unique (non-stackable). + * 检查道具实例是否唯一(不可堆叠)。 + * @return True if the item is unique, false otherwise. 如果道具唯一则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual bool IsUnique() const; + + /** + * Gets the display name of the item instance. + * 获取道具实例的显示名称。 + * @return The display name of the item. 道具的显示名称。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual FText GetItemName() const; + + /** + * Gets the item definition associated with this instance. + * 获取与此实例关联的道具定义。 + * @return The item definition, or nullptr if not set. 道具定义,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + const UGIS_ItemDefinition* GetDefinition() const; + + /** + * Sets the item definition for this instance (authority only). + * 设置此实例的道具定义(仅限权限)。 + * @param NewDefinition The new item definition to set. 要设置的新道具定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void SetDefinition(const UGIS_ItemDefinition* NewDefinition); + + /** + * Gets the description of the item instance. + * 获取道具实例的描述。 + * @return The description of the item. 道具的描述。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual FText GetItemDescription() const; + + /** + * Gets the gameplay tags associated with the item instance. + * 获取与道具实例关联的游戏标签。 + * @return The gameplay tag container. 游戏标签容器。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual FGameplayTagContainer GetItemTags() const; + + /** + * Gets a fragment from the item definition by its class. + * 从道具定义中按类获取片段。 + * @param FragmentClass The class of the fragment to retrieve. 要检索的片段类。 + * @return The fragment instance, or nullptr if not found. 片段实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemInstance", meta=(DeterminesOutputType="FragmentClass", DynamicOutputParam="ReturnValue")) + const UGIS_ItemFragment* GetFragment(TSubclassOf FragmentClass) const; + + /** + * Finds a fragment in the item definition by its class, with validity check. + * 在道具定义中按类查找片段,并进行有效性检查。 + * @param FragmentClass The class of the fragment to find. 要查找的片段类。 + * @param bValid Output parameter indicating if the fragment was found. 输出参数,指示是否找到片段。 + * @return The fragment instance, or nullptr if not found. 片段实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemInstance", meta=(DeterminesOutputType="FragmentClass", DynamicOutputParam="ReturnValue", ExpandBoolAsExecs="bValid")) + const UGIS_ItemFragment* FindFragment(TSubclassOf FragmentClass, bool& bValid) const; + + /** + * Template function to find a fragment by its class. + * 按类查找片段的模板函数。 + * @param ResultClass The class of the fragment to find. 要查找的片段类。 + * @return The fragment instance cast to the specified class, or nullptr if not found. 转换为指定类的片段实例,如果未找到则返回nullptr。 + */ + template + const ResultClass* FindFragmentByClass() const + { + return static_cast(GetFragment(ResultClass::StaticClass())); + } + + /** + * Checks if the item instance has any attribute with the specified tag. + * 检查道具实例是否具有指定标签的任何属性。 + * @param AttributeTag The tag of the attribute to check. 要检查的属性标签。 + * @return True if the attribute exists, false otherwise. 如果属性存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual bool HasAnyAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag) const; + + /** + * Checks if the item instance has a float attribute with the specified tag. + * 检查道具实例是否具有指定标签的浮点属性。 + * @param AttributeTag The tag of the attribute to check. 要检查的属性标签。 + * @return True if the float attribute exists, false otherwise. 如果浮点属性存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual bool HasFloatAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag) const; + + /** + * Gets the value of a float attribute. + * 获取浮点属性的值。 + * @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。 + * @return The value of the float attribute, or 0 if not found. 浮点属性的值,如果未找到则返回0。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual float GetFloatAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag) const; + + /** + * Sets the value of a float attribute (authority only). + * 设置浮点属性的值(仅限权限)。 + * @param AttributeTag The tag of the attribute to set. 要设置的属性标签。 + * @param NewValue The new value to set. 要设置的新值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void SetFloatAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, float NewValue); + + /** + * Adds a value to a float attribute (authority only). + * 为浮点属性添加值(仅限权限)。 + * @param AttributeTag The tag of the attribute to modify. 要修改的属性标签。 + * @param Value The value to add. 要添加的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void AddFloatAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, float Value); + + /** + * Removes a value from a float attribute (authority only). + * 从浮点属性中移除值(仅限权限)。 + * @param AttributeTag The tag of the attribute to modify. 要修改的属性标签。 + * @param Value The value to remove. 要移除的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void RemoveFloatAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, float Value); + + /** + * Checks if the item instance has an integer attribute with the specified tag. + * 检查道具实例是否具有指定标签的整数属性。 + * @param AttributeTag The tag of the attribute to check. 要检查的属性标签。 + * @return True if the integer attribute exists, false otherwise. 如果整数属性存在则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual bool HasIntegerAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag) const; + + /** + * Gets the value of an integer attribute. + * 获取整数属性的值。 + * @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。 + * @return The value of the integer attribute, or 0 if not found. 整数属性的值,如果未找到则返回0。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + virtual int32 GetIntegerAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag) const; + + /** + * Sets the value of an integer attribute (authority only). + * 设置整数属性的值(仅限权限)。 + * @param AttributeTag The tag of the attribute to set. 要设置的属性标签。 + * @param NewValue The new value to set. 要设置的新值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void SetIntegerAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, int32 NewValue); + + /** + * Adds a value to an integer attribute (authority only). + * 为整数属性添加值(仅限权限)。 + * @param AttributeTag The tag of the attribute to modify. 要修改的属性标签。 + * @param Value The value to add. 要添加的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void AddIntegerAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, int32 Value); + + /** + * Removes a value from an integer attribute (authority only). + * 从整数属性中移除值(仅限权限)。 + * @param AttributeTag The tag of the attribute to modify. 要修改的属性标签。 + * @param Value The value to remove. 要移除的值。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemInstance") + virtual void RemoveIntegerAttribute(UPARAM(meta=(Categories="GIS.Attribute")) + FGameplayTag AttributeTag, int32 Value); + + /** + * Gets the collection where this item belongs to. + * 获取此道具的所属集合。 + * @attention Only available in server side. 只在服务端有效。 + * @return The owning collection, or nullptr if not set. 所属集合,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemInstance") + UGIS_ItemCollection* GetOwningCollection() const; + + /** + * Gets the inventory that owns this item instance. + * 获取拥有此道具实例的库存。 + * @return The owning inventory, or nullptr if not set. 所属库存,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemInstance") + UGIS_InventorySystemComponent* GetOwningInventory() const; + + /** + * Assigns a collection to this item instance. server only + * 为此道具实例分配集合。仅服务端 + * @param NewItemCollection The new collection to assign. 要分配的新集合。 + */ + virtual void AssignCollection(UGIS_ItemCollection* NewItemCollection); + + /** + * Unassigns a collection from this item instance. server only + * 从此道具实例中取消分配集合。仅服务端 + * @param ItemCollection The collection to unassign. 要取消分配的集合。 + */ + virtual void UnassignCollection(UGIS_ItemCollection* ItemCollection); + + /** + * Resets the owning collection of this item instance. + * 重置此道具实例的所属集合。 + */ + // void ResetCollection(); + + /** + * Checks if the item instance is valid. + * 检查道具实例是否有效。 + * @return True if the item instance is valid, false otherwise. 如果道具实例有效则返回true,否则返回false。 + */ + bool IsItemValid() const; + + /** + * Checks if this item instance is stackable equivalent to another. + * 检查此道具实例是否与另一个在堆叠上等价。 + * @param OtherItem The other item instance to compare with. 要比较的其他道具实例。 + * @return True if the items are stackable equivalent, false otherwise. 如果道具在堆叠上等价则返回true,否则返回false。 + */ + virtual bool StackableEquivalentTo(const UGIS_ItemInstance* OtherItem) const; + + /** + * Checks if this item instance is similar to another. + * 检查此道具实例是否与另一个相似。 + * @param OtherItem The other item instance to compare with. 要比较的其他道具实例。 + * @return True if the items are similar, false otherwise. 如果道具相似则返回true,否则返回false。 + */ + virtual bool SimilarTo(const UGIS_ItemInstance* OtherItem) const; + + /** + * Static function to check if two item instances are stackable equivalent. + * 静态函数,检查两个道具实例是否在堆叠上等价。 + * @param Lhs The first item instance. 第一个道具实例。 + * @param Rhs The second item instance. 第二个道具实例。 + * @return True if the items are stackable equivalent, false otherwise. 如果道具在堆叠上等价则返回true,否则返回false。 + */ + static bool AreStackableEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs); + + /** + * Static function to check if two item instances are similar (commented out). + * 静态函数,检查两个道具实例是否相似(已注释)。 + */ + // static bool AreValueEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs); + + /** + * Static function to check if two item instances are similar. + * 静态函数,检查两个道具实例是否相似。 + * @param Lhs The first item instance. 第一个道具实例。 + * @param Rhs The second item instance. 第二个道具实例。 + * @return True if the items are similar, false otherwise. 如果道具相似则返回true,否则返回false。 + */ + static bool AreSimilar(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs); + + /** + * Called when the item instance is duplicated. + * 道具实例被复制时调用。 + * @param SrcItem The source item instance being duplicated. 被复制的源道具实例。 + */ + virtual void OnItemDuplicated(const UGIS_ItemInstance* SrcItem); + +#pragma region Mixins + + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + const FGIS_MixinContainer& GetFragmentStates() const; + + /** + * Finds fragment state by its class. + * 按类查找片段数据。 + * @param FragmentClass The class of the fragment to find. 要查找的片段类。 + * @param OutState The found fragment state (output). 找到的片段状态(输出)。 + * @return True if the data was found, false otherwise. 如果找到有效数据则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemInstance", meta=(DisplayName="Find Fragment State", ExpandBoolAsExecs="ReturnValue")) + virtual bool FindFragmentStateByClass(TSubclassOf FragmentClass, FInstancedStruct& OutState) const; + + /** + * Sets fragment data by its class. + * 按类设置片段数据。 + * @param FragmentClass The class of the fragment to set. 要设置的片段类。 + * @param NewState The fragment state to set. 要设置的片段数据。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemInstance", meta=(DisplayName="Set Fragment State")) + virtual void SetFragmentStateByClass(TSubclassOf FragmentClass, UPARAM(ref) + const FInstancedStruct& NewState); + + /** + * Event triggered when fragment data is added to the item instance. + * 当片段数据添加到道具实例时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="ItemInstance") + FGIS_ItemFragmentStateEventSignature OnFragmentStateAddedEvent; + + /** + * Event triggered when fragment data is removed from the item instance. + * 当从道具实例移除片段数据时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="ItemInstance") + FGIS_ItemFragmentStateEventSignature OnFragmentStateRemovedEvent; + + /** + * Event triggered when fragment data is updated in the item instance. + * 当道具实例中的片段数据更新时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="ItemInstance") + FGIS_ItemFragmentStateEventSignature OnFragmentStateUpdatedEvent; + +protected: + /** + * Called when mixin data is added to the item instance. + * 当混合数据添加到道具实例时调用。 + * @param Target The target object for the mixin data. 混合数据的目标对象。 + * @param Data The instanced struct containing the mixin data. 包含混合数据的实例化结构体。 + */ + virtual void OnMixinDataAdded(const TObjectPtr& Target, const FInstancedStruct& Data) override final; + + /** + * Called when mixin data is updated in the item instance. + * 当道具实例中的混合数据更新时调用。 + * @param Target The target object for the mixin data. 混合数据的目标对象。 + * @param Data The instanced struct containing the updated mixin data. 包含更新混合数据的实例化结构体。 + */ + virtual void OnMixinDataUpdated(const TObjectPtr& Target, const FInstancedStruct& Data) override final; + + /** + * Called when mixin data is removed from the item instance. + * 当从道具实例中移除混合数据时调用。 + * @param Target The target object for the mixin data. 混合数据的目标对象。 + * @param Data The instanced struct containing the removed mixin data. 包含移除混合数据的实例化结构体。 + */ + virtual void OnMixinDataRemoved(const TObjectPtr& Target, const FInstancedStruct& Data) override final; + + /** + * Called when fragment data is added to the item instance. + * 当片段数据添加到道具实例时调用。 + * @param Fragment The fragment associated with the data. 与数据关联的片段。 + * @param Data The instanced struct containing the fragment data. 包含片段数据的实例化结构体。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + void OnFragmentStateAdded(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data); + + /** + * Called when fragment data is updated in the item instance. + * 当道具实例中的片段数据更新时调用。 + * @param Fragment The fragment associated with the data. 与数据关联的片段。 + * @param Data The instanced struct containing the updated fragment data. 包含更新片段数据的实例化结构体。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + void OnFragmentStateUpdated(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data); + + /** + * Called when fragment data is removed from the item instance. + * 当从道具实例移除片段数据时调用。 + * @param Fragment The fragment associated with the data. 与数据关联的片段。 + * @param Data The instanced struct containing the removed fragment data. 包含移除片段数据的实例化结构体。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") + void OnFragmentStateRemoved(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data); + +#pragma endregion + +#pragma region Containers + +public: + /** + * Event triggered when a float attribute is changed inside the item instance. + * 当道具实例中的浮点型属性变化时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="ItemInstance") + FGIS_ItemFloatAttributeChangedEventSignature OnFloatAttributeChangedEvent; + + /** + * Event triggered when an integer attribute is changed inside the item instance. + * 当道具实例中的整型属性变化时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="ItemInstance") + FGIS_ItemFloatAttributeChangedEventSignature OnIntegerAttributeChangedEvent; + + /** + * Called when a float attribute is updated. + * 浮点型属性更新时调用。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + virtual void OnTagFloatUpdate(const FGameplayTag& Tag, float OldValue, float NewValue) override final; + + /** + * Called when an integer attribute is updated. + * 整型属性更新时调用。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + virtual void OnTagIntegerUpdate(const FGameplayTag& Tag, int32 OldValue, int32 NewValue) override final; + +protected: + /** + * Blueprint event triggered when a float attribute changes. + * 浮点型属性变化时触发的蓝图事件。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + UFUNCTION(BlueprintNativeEvent, Category="ItemInstance") + void OnFloatAttributeChanged(const FGameplayTag& Tag, float OldValue, float NewValue); + + /** + * Blueprint event triggered when an integer attribute changes. + * 整型属性变化时触发的蓝图事件。 + * @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。 + * @param OldValue The previous value of the attribute. 属性之前的值。 + * @param NewValue The new value of the attribute. 属性的新值。 + */ + UFUNCTION(BlueprintNativeEvent, Category="ItemInstance") + void OnIntegerAttributeChanged(const FGameplayTag& Tag, int32 OldValue, int32 NewValue); + +#pragma endregion + + /** + * Unique ID of this item instance, assigned at creation. + * 道具实例的唯一ID,在创建时分配。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="ItemInstance", Replicated) + FGuid ItemId; + + /** + * The item definition associated with this instance. + * 与此实例关联的道具定义。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="ItemInstance", Replicated) + TObjectPtr Definition; + + /** + * Container for integer attributes of the item instance. + * 道具实例的整数属性容器。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category="ItemInstance", SaveGame, meta=(DisplayName="Attributes (Integer)", ShowOnlyInnerProperties)) + FGIS_GameplayTagIntegerContainer IntegerAttributes; + + /** + * Container for float attributes of the item instance. + * 道具实例的浮点属性容器。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category="ItemInstance", SaveGame, meta=(DisplayName="Attributes (Float)", ShowOnlyInnerProperties)) + FGIS_GameplayTagFloatContainer FloatAttributes; + + /** + * Container for each fragment's runtime state. + * 每个片段运行时状态的容器。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category="ItemInstance", meta=(DisplayName="Fragment States", ShowOnlyInnerProperties)) + FGIS_MixinContainer FragmentStates; + +private: +#if UE_WITH_IRIS + /** + * Registers replication fragments for networking (Iris-specific). + * 为网络注册复制片段(特定于Iris)。 + * @param Context The fragment registration context. 片段注册上下文。 + * @param RegistrationFlags The registration flags. 注册标志。 + */ + virtual void RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) override; +#endif // UE_WITH_IRIS + + /** + * The collection that owns this item instance. + * 拥有此道具实例的集合。 + */ + UPROPERTY(Replicated, Transient) + TObjectPtr OwningCollection{nullptr}; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInterface.h new file mode 100644 index 0000000..afc0427 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemInterface.h @@ -0,0 +1,48 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// #pragma once +// +// #include "CoreMinimal.h" +// #include "GameplayTagContainer.h" +// #include "UObject/Interface.h" +// #include "GIS_ItemInterface.generated.h" +// +// +// // This class does not need to be modified. +// UINTERFACE(meta=(CannotImplementInterfaceInBlueprint)) +// class GENERICINVENTORYSYSTEM_API UGIS_EnhancedItemInterface : public UInterface +// { +// GENERATED_BODY() +// }; +// +// /** +// * 具备数值修正的道具 +// */ +// class GENERICINVENTORYSYSTEM_API IGIS_EnhancedItemInterface +// { +// GENERATED_BODY() +// +// public: +// UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") +// virtual TMap GetAdditionAttributes() const = 0; +// // virtual TMap GetAdditionAttributes_Implementation() const = 0; +// virtual void OverrideAdditionAttribute(FGameplayTag AttributeTag, float Value) = 0; +// +// UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") +// virtual TMap GetMultiplierAttributes() const = 0; +// // virtual TMap GetMultiplierAttributes_Implementation() const =0; +// virtual void OverrideMultiplierAttribute(FGameplayTag AttributeTag, float Value) = 0; +// +// /**强化等级*/ +// UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") +// virtual int32 GetEnhancedLevel() const = 0; +// virtual void SetEnhancedLevel(int32 Value) = 0; +// /**最大强化次数*/ +// UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") +// virtual int32 GetMaxEnhancedNumber() const = 0; +// virtual void SetMaxEnhancedNumber(int32 Value) = 0; +// /**当前强化次数*/ +// UFUNCTION(BlueprintCallable, Category="GIS|ItemInstance") +// virtual int32 GetCurrentEnhancedNumber() const = 0; +// virtual void SetCurrentEnhancedNumber(int32 Value) = 0; +// }; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemStack.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemStack.h new file mode 100644 index 0000000..f01c0a1 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Core/Items/GIS_ItemStack.h @@ -0,0 +1,268 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "GIS_ItemStack.generated.h" + +class UActorComponent; +class UGIS_ItemCollection; +class UGIS_ItemSlotCollection; +class UGIS_ItemMultiStackCollection; +class UGIS_ItemDefinition; +class UGIS_ItemInstance; +class UGIS_InventorySystemComponent; + +/** + * Enum defining the types of changes to an item stack. + * 定义道具堆栈变更类型的枚举。 + */ +UENUM(BlueprintType) +enum class EGIS_ItemStackChangeType : uint8 +{ + WasAdded, // Item stack was added. 道具堆栈被添加。 + WasRemoved, // Item stack was removed. 道具堆栈被移除。 + Changed // Item stack was modified. 道具堆栈被修改。 +}; + +/** + * Structure used to store the corresponding item instance, their amount, and their position within the collection. + * 道具堆栈是一个简单的结构体,用于存储对应的道具实例、数量以及在集合中的位置。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemStack : public FFastArraySerializerItem +{ + GENERATED_BODY() + + friend struct FGIS_ItemStackContainer; + + /** + * Static constant representing an invalid stack ID. + * 表示无效堆栈ID的静态常量。 + */ + static FGuid InvalidId; + + /** + * Default constructor for item stack. + * 道具堆栈的默认构造函数。 + */ + FGIS_ItemStack(); + + /** + * Gets a debug string representation of the item stack. + * 获取道具堆栈的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString(); + + /** + * Initializes the item stack with specified parameters. + * 使用指定参数初始化道具堆栈。 + * @param InStackId The stack ID. 堆栈ID。 + * @param InItem The item instance. 道具实例。 + * @param InAmount The amount of the item. 道具数量。 + * @param InCollection The collection the stack belongs to. 堆栈所属的集合。 + */ + void Initialize(FGuid InStackId, UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, int32 InIndex = INDEX_NONE); + + /** + * Checks if the item stack is valid. + * 检查道具堆栈是否有效。 + * @return True if the stack is valid, false otherwise. 如果堆栈有效则返回true,否则返回false。 + */ + bool IsValidStack() const; + + /** + * Resets the item stack to its default state. + * 将道具堆栈重置为默认状态。 + */ + void Reset(); + + /** + * The item instance of this stack. + * 此堆栈中的道具实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="GIS", meta=(DisplayName="ItemInstance", ShowInnerProperties, DisplayPriority=0)) + TObjectPtr Item{nullptr}; + + /** + * The quantity of the item in this stack. + * 此堆栈中的道具数量。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + int32 Amount = 0; + + /** + * The stack ID. + * 堆栈ID。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + FGuid Id; + + /** + * The collection this stack belongs to. + * 此堆栈所属的集合。 + * @attention TODO: Should this really need to be replicated? + * @注意 TODO:此属性是否真的需要复制? + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + TObjectPtr Collection = nullptr; + + /** + * The collection ID (commented out). + * 集合ID(已注释)。 + */ + // UPROPERTY(VisibleAnywhere, Category="GIS") + // FGuid CollectionId; + + /** + * The last observed count of the stack (not replicated). + * 堆栈的最后观察计数(不复制)。 + */ + UPROPERTY(NotReplicated) + int32 LastObservedAmount = INDEX_NONE; + + /** + * The position of the stack within the collection. + * 堆栈在集合中的位置。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS") + int32 Index = INDEX_NONE; + + /** + * Compares this item stack with another for equality. + * 将此道具堆栈与另一个比较以判断是否相等。 + * @param Other The other item stack to compare with. 要比较的其他道具堆栈。 + * @return True if the stacks are equal, false otherwise. 如果堆栈相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_ItemStack& Other) const; + + /** + * Compares this item stack with another for inequality. + * 将此道具堆栈与另一个比较以判断是否不相等。 + * @param Other The other item stack to compare with. 要比较的其他道具堆栈。 + * @return True if the stacks are not equal, false otherwise. 如果堆栈不相等则返回true,否则返回false。 + */ + bool operator!=(const FGIS_ItemStack& Other) const; + + /** + * Compares this item stack's ID with another ID for equality. + * 将此道具堆栈的ID与另一个ID比较以判断是否相等。 + * @param OtherId The other ID to compare with. 要比较的其他ID。 + * @return True if the IDs are equal, false otherwise. 如果ID相等则返回true,否则返回false。 + */ + bool operator==(const FGuid& OtherId) const; + + /** + * Compares this item stack's ID with another ID for inequality. + * 将此道具堆栈的ID与另一个ID比较以判断是否不相等。 + * @param OtherId The other ID to compare with. 要比较的其他ID。 + * @return True if the IDs are not equal, false otherwise. 如果ID不相等则返回true,否则返回false。 + */ + bool operator!=(const FGuid& OtherId) const; +}; + +/** + * The container for item stacks, supporting fast array serialization. + * 道具堆栈的容器,支持快速数组序列化。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemStackContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + friend UGIS_ItemCollection; + friend UGIS_ItemMultiStackCollection; + friend UGIS_ItemSlotCollection; + + /** + * Default constructor for item stack container. + * 道具堆栈容器的默认构造函数。 + */ + FGIS_ItemStackContainer() : OwningCollection(nullptr) + { + } + + /** + * Constructor for item stack container with a specified collection. + * 使用指定集合构造道具堆栈容器。 + * @param InCollection The owning collection. 所属集合。 + */ + FGIS_ItemStackContainer(UGIS_ItemCollection* InCollection) : OwningCollection(InCollection) + { + } + + //~FFastArraySerializer contract + /** + * Called before items are removed during replication. + * 复制期间在移除道具前调用。 + * @param RemovedIndices The indices of removed items. 移除道具的索引。 + * @param FinalSize The final size of the array after removal. 移除后数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after items are added during replication. + * 复制期间在添加道具后调用。 + * @param AddedIndices The indices of added items. 添加道具的索引。 + * @param FinalSize The final size of the array after addition. 添加后数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after items are changed during replication. + * 复制期间在道具更改后调用。 + * @param ChangedIndices The indices of changed items. 更改道具的索引。 + * @param FinalSize The final size of the array after changes. 更改后数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for the item stack container. + * 处理道具堆栈容器的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Stacks, DeltaParms, *this); + } + + const FGIS_ItemStack* FindById(const FGuid& StackId) const; + + const FGIS_ItemStack* FindByItemId(const FGuid& ItemId) const; + + int32 IndexOfById(const FGuid& StackId) const; + + int32 IndexOfByItemId(const FGuid& ItemId) const; + + int32 IndexOfByIds(const FGuid& StackId, const FGuid& ItemId) const; + +protected: + /** + * Replicated list of item stacks. + * 复制的道具堆栈列表。 + */ + UPROPERTY(VisibleAnywhere, Category="GIS", meta=(ShowOnlyInnerProperties, DisplayName="Item Stacks")) + TArray Stacks; + + /** + * The collection that owns this container (not replicated). + * 拥有此容器的集合(不复制)。 + */ + UPROPERTY(NotReplicated) + TObjectPtr OwningCollection; +}; + +/** + * Template specialization to enable network delta serialization for the item stack container. + * 为道具堆栈容器启用网络增量序列化的模板特化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum { WithNetDeltaSerializer = true }; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingStructLibrary.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingStructLibrary.h new file mode 100644 index 0000000..a79eaec --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingStructLibrary.h @@ -0,0 +1,297 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/DataTable.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_CurrencyEntry.h" +#include "Items/GIS_ItemInfo.h" +#include "UObject/Object.h" +#include "GIS_CraftingStructLibrary.generated.h" + +/** + * Struct for querying item ingredients required for crafting. + * 用于查询合成所需道具材料的结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_ItemIngredient +{ + GENERATED_USTRUCT_BODY() + + FGIS_ItemIngredient() + : NeedAmounts(0) + { + } + + FGIS_ItemIngredient(int32 InNeedAmounts, const TArray& InHoldItemAmounts) + : NeedAmounts(InNeedAmounts) + , HoldItemAmounts(InHoldItemAmounts) + { + } + + /** + * Definition of the item required for crafting. + * 合成所需道具的定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TSoftObjectPtr ItemDefinition; + + /** + * Required amount of the item for crafting. + * 合成所需道具的数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + int32 NeedAmounts; + + /** + * List of held items and their amounts. + * 持有道具及其数量的列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TArray HoldItemAmounts; + + /** + * Calculates the total amount of held items. + * 计算持有道具的总量。 + * @return The total amount of held items. 持有道具的总量。 + */ + FORCEINLINE int32 GetHoldAmount() const + { + int32 Count = 0; + for (const auto& ItemInfo : HoldItemAmounts) + { + Count += ItemInfo.Amount; + } + return Count; + } +}; + +/** + * Struct for querying currency ingredients required for crafting. + * 用于查询合成所需货币材料的结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_CurrencyIngredient +{ + GENERATED_USTRUCT_BODY() + + FGIS_CurrencyIngredient() + : NeedAmounts(0) + { + } + + FGIS_CurrencyIngredient(TObjectPtr InCurrencyDefinition, int32 InNeedAmounts, FGIS_CurrencyEntry InHoldAmounts) + : CurrencyDefinition(InCurrencyDefinition) + , NeedAmounts(InNeedAmounts) + , HoldCurrencyInfo(InHoldAmounts) + { + } + + /** + * Definition of the currency required for crafting. + * 合成所需货币的定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TObjectPtr CurrencyDefinition; + + /** + * Required amount of the currency for crafting. + * 合成所需货币的数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + int32 NeedAmounts; + + /** + * Information about the held currency. + * 持有货币的信息。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGIS_CurrencyEntry HoldCurrencyInfo; +}; + +/** + * Struct defining the cost data for crafting or enhancing items. + * 定义合成或强化道具的消耗数据的结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_CraftItemCostData +{ + GENERATED_USTRUCT_BODY() + + FGIS_CraftItemCostData() : bCostItem(false), bCostCurrency(true), bCostTime(false), Days(1) + { + }; + + /** + * Determines if items are consumed during crafting. + * 确定合成时是否消耗道具。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Crafting") + bool bCostItem; + + /** + * List of items consumed during crafting. + * 合成时消耗的道具列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(EditCondition="bCostItem"), Category="Crafting", meta=(TitleProperty="Definition")) + TArray CostItems; + + /** + * Determines if currencies are consumed during crafting. + * 确定合成时是否消耗货币。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Crafting") + bool bCostCurrency; + + /** + * List of currencies consumed during crafting. + * 合成时消耗的货币列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(EditCondition="bCostCurrency"), Category="Crafting") + TArray CostCurrencies; + + /** + * Determines if time is consumed during crafting. + * 确定合成时是否消耗时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Crafting") + bool bCostTime; + + /** + * Time consumed for crafting, in days. + * 合成消耗的时间(以天为单位)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(EditCondition="bCostTime"), Category="Crafting") + float Days; +}; + +/** + * Data table struct for item crafting recipes. + * 道具合成配方的数据表结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_CraftingItemData : public FTableRowBase +{ + GENERATED_USTRUCT_BODY() + + /** + * Gameplay tag defining the crafting item. + * 定义合成道具的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGameplayTag CraftingItemDefinition; + + /** + * Definition of the crafting item. + * 合成道具的定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TSoftObjectPtr CraftingDefinition; + + /** + * Type of crafting operation. + * 合成操作的类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGameplayTag CraftingItemType; + + /** + * Cost data for the crafting operation. + * 合成操作的消耗数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGIS_CraftItemCostData CraftItemCost; + + /** + * Definition of the output item produced by crafting. + * 合成产出的道具定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS") + TSoftObjectPtr OutputItemDefinition; +}; + +/** + * Data table struct for item enhancement data. + * 道具强化数据的数据表结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_ItemEnhancedData : public FTableRowBase +{ + GENERATED_USTRUCT_BODY() + + FGIS_ItemEnhancedData() : EnhancedLevel(1), bDestroy(false), Downgrading(false) + { + }; + + /** + * Set of item types that can use this enhancement data. + * 可以使用此强化数据的道具类型集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGameplayTagContainer ItemTypeSet; + + /** + * Tag query for items eligible for this enhancement. + * 适用于此强化的道具标签查询。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGameplayTagQuery ItemTagQuery; + + /** + * Level of enhancement applied to the item. + * 应用于道具的强化等级。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + int32 EnhancedLevel; + + /** + * Prefix added to the item name after enhancement. + * 强化后添加到道具名称的前缀。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FName EnhancedNamePrefix; + + /** + * Cost data for the enhancement operation. + * 强化操作的消耗数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGIS_CraftItemCostData EnhancedItemCost; + + /** + * Constant attribute bonuses applied by the enhancement. + * 强化应用的常量属性加成。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS") + TMap AdditionAttributes; + + /** + * Multiplier attribute bonuses applied by the enhancement. + * 强化应用的系数属性加成。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS") + TMap MultiplierAttributes; + + /** + * Success rate of the enhancement operation. + * 强化操作的成功率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS", meta=(ClampMin = 0.f, ClampMax = 1.f)) + float SuccessRate = 1.f; + + /** + * Determines if the item is destroyed on enhancement failure (deprecated). + * 确定强化失败时道具是否销毁(已弃用)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS", meta=(DeprecatedProperty)) + bool bDestroy; + + /** + * Level reduction on enhancement failure (deprecated). + * 强化失败时的等级降低(已弃用)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GIS", meta=(EditCondition="!bDestroy", DeprecatedProperty)) + int32 Downgrading; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingSystemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingSystemComponent.h new file mode 100644 index 0000000..9291171 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_CraftingSystemComponent.h @@ -0,0 +1,146 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CraftingStructLibrary.h" +#include "Components/ActorComponent.h" +#include "GIS_CraftingSystemComponent.generated.h" + +class UGIS_ItemFragment_CraftingRecipe; +class UGIS_CurrencySystemComponent; +class UGIS_CraftingRecipe; + +/** + * Actor component for handling crafting functionality in the inventory system. + * 用于在库存系统中处理制作功能的演员组件。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_CraftingSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the crafting system component. + * 制作系统组件的构造函数。 + */ + UGIS_CraftingSystemComponent(); + + /** + * Attempts to craft items using a recipe. + * 尝试使用配方制作道具。 + * @param Recipe The item definition containing recipe data. 包含配方数据的道具定义。 + * @param Inventory The inventory that pays the crafting cost. 用于支付制作成本的库存。 + * @param Quantity The multiplier for crafting output. 制作输出的倍率。 + * @return True if crafting succeeds, false otherwise. 如果制作成功返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|CraftingSystem") + bool Craft(const UGIS_ItemDefinition* Recipe, UGIS_InventorySystemComponent* Inventory, int32 Quantity = 1); + + /** + * Checks if the parameters are valid for crafting an item. + * 检查参数是否满足制作道具的条件。 + * @param RecipeDefinition The item definition with recipe data. 包含配方数据的道具定义。 + * @param Inventory The inventory containing the items. 包含道具的库存。 + * @param Quantity The amount to craft. 要制作的数量。 + * @return True if crafting is possible, false otherwise. 如果可以制作则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CraftingSystem") + bool CanCraft(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity = 1); + +protected: + /** + * Internal function to check if crafting is possible. + * 内部函数,检查是否可以制作。 + * @param RecipeDefinition The item definition with recipe data. 包含配方数据的道具定义。 + * @param Inventory The inventory containing the items. 包含道具的库存。 + * @param Quantity The amount to craft. 要制作的数量。 + * @return True if crafting is possible, false otherwise. 如果可以制作则返回true,否则返回false。 + */ + virtual bool CanCraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity = 1); + + /** + * Internal function to perform crafting. + * 内部函数,执行制作。 + * @param RecipeDefinition The item definition with recipe data. 包含配方数据的道具定义。 + * @param Inventory The inventory that pays the crafting cost. 用于支付制作成本的库存。 + * @param Quantity The multiplier for crafting output. 制作输出的倍率。 + * @return True if crafting succeeds, false otherwise. 如果制作成功返回true,否则返回false。 + */ + virtual bool CraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity = 1); + + /** + * Produces crafting output for the inventory, adding output items by default. + * 为库存生成制作输出,默认将输出道具添加到库存。 + * @details Override to implement custom logic, such as a crafting queue. + * @细节 可覆写以实现自定义逻辑,例如制作队列(延迟获得结果)。 + * @param RecipeDefinition The item definition with recipe data. 包含配方数据的道具定义。 + * @param Inventory The inventory to receive the output. 接收输出的库存。 + * @param Quantity The multiplier for crafting output. 制作输出的倍率。 + */ + virtual void ProduceCraftingOutput(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity = 1); + + /** + * Checks if the recipe definition is valid (contains valid crafting data). + * 检查配方定义是否有效(包含有效的制作数据)。 + * @details Override to apply custom validation rules, such as checking for specific fragments. + * @细节 可覆写以应用自定义验证规则,例如检查特定道具片段是否存在。 + * @param RecipeDefinition The item definition to validate. 要验证的道具定义。 + * @return True if the recipe is valid, false otherwise. 如果配方有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|CraftingSystem") + bool IsValidRecipe(const UGIS_ItemDefinition* RecipeDefinition) const; + + /** + * Gets the recipe fragment for the default crafting implementation. + * 获取默认制作实现所需的配方片段。 + * @param RecipeDefinition The item definition to retrieve data from. 要从中获取数据的道具定义。 + * @return The crafting recipe fragment, or nullptr if not found. 制作配方片段,如果未找到则返回nullptr。 + */ + const UGIS_ItemFragment_CraftingRecipe* GetRecipeFragment(const UGIS_ItemDefinition* RecipeDefinition) const; + + /** + * Selects the maximum number of item infos for required item ingredients from the inventory. + * 从库存中选择满足道具成分需求的最大道具信息。 + * @param Inventory The inventory to select items from. 要从中选择道具的库存。 + * @param ItemIngredients The required item ingredients. 所需的道具成分。 + * @param Quantity The amount to craft. 要制作的数量。 + * @return True if sufficient items are selected, false otherwise. 如果选择了足够的道具则返回true,否则返回false。 + */ + virtual bool SelectItemForIngredients(const UGIS_InventorySystemComponent* Inventory, const TArray& ItemIngredients, int32 Quantity); + + /** + * Checks if selected items are sufficient for the required item ingredients. + * 检查已选道具是否足以满足道具成分需求。 + * @param ItemIngredients The required item ingredients. 所需的道具成分。 + * @param Quantity The amount to craft. 要制作的数量。 + * @param SelectedItems The selected items from the inventory. 从库存中选择的道具。 + * @param ItemsToIgnore Items to ignore during checking. 检查时要忽略的道具。 + * @param NumOfItemsToIgnore Number of items to ignore (output). 要忽略的道具数量(输出)。 + * @return True if the selected items are sufficient, false otherwise. 如果选择的道具足够则返回true,否则返回false。 + */ + virtual bool CheckIfEnoughItemIngredients(const TArray& ItemIngredients, + int32 Quantity, const TArray& SelectedItems, TArray& ItemsToIgnore, int32& NumOfItemsToIgnore); + + /** + * Removes item ingredients from the inventory's default collection one by one. + * 从库存的默认集合中逐一移除道具成分。 + * @param Inventory The inventory to remove items from. 要从中移除道具的库存。 + * @param ItemIngredients The item ingredients to remove. 要移除的道具成分。 + * @return True if all items were successfully removed, false otherwise. 如果所有道具移除成功则返回true,否则返回false。 + */ + virtual bool RemoveItemIngredients(UGIS_InventorySystemComponent* Inventory, const TArray& ItemIngredients); + + /** + * Cache for selected items during crafting. + * 制作过程中选择的道具缓存。 + */ + TArray SelectedItemsCache; + + /** + * Number of selected items in the cache. + * 缓存中选择的道具数量。 + */ + int32 NumOfSelectedItemsCache; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_ItemFragment_CraftingRecipe.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_ItemFragment_CraftingRecipe.h new file mode 100644 index 0000000..6c1b6fa --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Crafting/GIS_ItemFragment_CraftingRecipe.h @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_CurrencyEntry.h" +#include "GIS_ItemFragment.h" +#include "GIS_ItemFragment_CraftingRecipe.generated.h" + +/** + * Item fragment defining a crafting recipe for producing items. + * 定义用于生产道具的合成配方的道具片段。 + * @details Specifies input items, currencies, and output items for crafting. + * @细节 指定用于合成的输入道具、货币和输出道具。 + */ +UCLASS(DisplayName="Crafting Recipe Settings", Category="BuiltIn") +class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_CraftingRecipe : public UGIS_ItemFragment +{ + GENERATED_BODY() + +public: + /** + * List of required items and their quantities for crafting. + * 合成所需的道具及其数量列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Crafting", meta=(TitleProperty="{Definition}->{Amount}")) + TArray InputItems; + + /** + * List of required currencies and their amounts for crafting. + * 合成所需的货币及其数量列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Ingredients", meta=(TitleProperty="{Tag}->{Amount}")) + TArray InputCurrencies; + + /** + * List of items produced by the crafting recipe. + * 合成配方产出的道具列表。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Crafting", meta=(TitleProperty="EditorFriendlyName")) + TArray OutputItems; + +#if WITH_EDITOR + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_CurrencyDropper.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_CurrencyDropper.h new file mode 100644 index 0000000..84c1310 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_CurrencyDropper.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_DropperComponent.h" +#include "GIS_CurrencyDropper.generated.h" + +class UGIS_CurrencySystemComponent; + +/** + * Component for handling currency drop logic. + * 处理货币掉落逻辑的组件。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_CurrencyDropper : public UGIS_DropperComponent +{ + GENERATED_BODY() + +public: + /** + * Initializes the currency dropper component at the start of play. + * 在游戏开始时初始化货币掉落组件。 + */ + virtual void BeginPlay() override; + + /** + * Executes the currency drop logic. + * 执行货币掉落逻辑。 + */ + virtual void Drop() override; + +protected: + /** + * Reference to the currency system component. + * 货币系统组件的引用。 + */ + UPROPERTY() + UGIS_CurrencySystemComponent* MyCurrency; + +#if WITH_EDITOR + /** + * Validates the component's data in the editor. + * 在编辑器中验证组件的数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The result of the data validation. 数据验证的结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_DropperComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_DropperComponent.h new file mode 100644 index 0000000..b26d610 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_DropperComponent.h @@ -0,0 +1,79 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/SceneComponent.h" +#include "GIS_DropperComponent.generated.h" + +/** + * Abstract base class for a component that handles dropping items in the game world. + * 用于在游戏世界中处理道具掉落的抽象基类组件。 + */ +UCLASS(Abstract, ClassGroup=(GIS)) +class GENERICINVENTORYSYSTEM_API UGIS_DropperComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the dropper component, sets default values for properties. + * 掉落组件的构造函数,设置属性的默认值。 + */ + UGIS_DropperComponent(); + + /** + * Drops an item in the game world (authority only). + * 在游戏世界中掉落道具(仅限权限)。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|Dropper") + virtual void Drop(); + +protected: + /** + * Creates an instance of the pickup actor using the specified PickupActorClass. + * 使用指定的PickupActorClass创建拾取Actor的实例。 + * @return The spawned pickup actor instance, or nullptr if creation fails. 创建的拾取Actor实例,如果创建失败则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Dropper") + AActor* CreatePickupActorInstance(); + + /** + * Calculates the origin point for dropping the item. + * 计算道具掉落的原点位置。 + * @return The calculated drop origin as a vector. 掉落原点的向量。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Dropper") + FVector CalcDropOrigin() const; + + /** + * Calculates a random offset to apply to the drop location. + * 计算应用于掉落位置的随机偏移量。 + * @return The random offset vector. 随机偏移量向量。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Dropper") + FVector CalcDropOffset() const; + + /** + * The class of the actor to spawn as the dropped item. + * 作为掉落物生成的Actor类。 + * @attention Must implement the GIS_PickupActorInterface. + * @注意 必须实现GIS_PickupActorInterface接口。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(ExposeOnSpawn, MustImplement="/Script/GenericInventorySystem.GIS_PickupActorInterface")) + TSoftClassPtr PickupActorClass; + + /** + * Optional actor whose transform is used as the drop origin. + * 可选的Actor,其变换用作掉落原点。 + */ + UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category="Dropper") + TObjectPtr DropTransform{nullptr}; + + /** + * The radius within which the item can be dropped. + * 道具掉落的半径。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(ExposeOnSpawn)) + float DropRadius = 50.f; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_ItemDropperComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_ItemDropperComponent.h new file mode 100644 index 0000000..efc677d --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_ItemDropperComponent.h @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_DropperComponent.h" +#include "Items/GIS_ItemInfo.h" +#include "GIS_ItemDropperComponent.generated.h" + + +class UGIS_InventorySystemComponent; + +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_ItemDropperComponent : public UGIS_DropperComponent +{ + GENERATED_BODY() + +public: + /** + * Get the item infos of this dropper will drops. + * 获取要掉落的道具信息。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|Dropper") + TArray GetItemsToDrop() const; + + virtual void Drop() override; + +protected: + virtual void BeginPlay() override; + + virtual TArray GetItemsToDropInternal() const; + virtual void DropItemsInternal(const TArray& ItemInfos); + + virtual void DropInventoryPickup(const TArray& ItemInfos); + virtual void DropItemPickup(const FGIS_ItemInfo& ItemInfo); + + /** + * Target collection to drop. + * 指定要掉落库存中的哪个集合里的道具. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(Categories="GIS.Collection")) + FGameplayTag CollectionTag; + + /** + * If the drops is inventory pickup or item pickups? + * 指定以InventoryPickup的方式掉落还是以ItemPickup + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper") + bool bDropAsInventory{true}; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_RandomItemDropperComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_RandomItemDropperComponent.h new file mode 100644 index 0000000..caf0a87 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Drops/GIS_RandomItemDropperComponent.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemDropperComponent.h" +#include "GIS_RandomItemDropperComponent.generated.h" + +/** + * Component for handling random item drop logic. + * 处理随机道具掉落逻辑的组件。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_RandomItemDropperComponent : public UGIS_ItemDropperComponent +{ + GENERATED_BODY() + +protected: + /** + * Retrieves the list of items to drop randomly. + * 随机获取要掉落的道具列表。 + * @return Array of item information for dropped items. 掉落道具的信息数组。 + */ + virtual TArray GetItemsToDropInternal() const override; + + /** + * Selects a random item from the provided list based on weights. + * 根据权重从提供的列表中随机选择一个道具。 + * @param ItemInfos List of item information to choose from. 可选择的道具信息列表。 + * @param Sum Total weight sum for randomization. 用于随机化的总权重和。 + * @return Reference to the selected item information. 选中的道具信息的引用。 + */ + const FGIS_ItemInfo& GetRandomItemInfo(const TArray& ItemInfos, int32 Sum) const; + + /** + * Determines if items are dropped randomly. + * 确定是否随机掉落道具。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(DisplayAfter="bDropAsInventory")) + bool bRandomDrop{false}; + + /** + * Minimum number of items to drop when random drop is enabled. + * 启用随机掉落时掉落的最小道具数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(EditCondition="bRandomDrop", ClampMin=1)) + int32 MinAmount{1}; + + /** + * Maximum number of items to drop when random drop is enabled. + * 启用随机掉落时掉落的最大道具数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dropper", meta=(EditCondition="bRandomDrop", ClampMin=1)) + int32 MaxAmount{2}; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentActorInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentActorInterface.h new file mode 100644 index 0000000..d2502e4 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentActorInterface.h @@ -0,0 +1,45 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GIS_EquipmentActorInterface.generated.h" + +class UGIS_EquipmentInstance; + +// This class does not need to be modified. +UINTERFACE() +class UGIS_EquipmentActorInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class GENERICINVENTORYSYSTEM_API IGIS_EquipmentActorInterface +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent, Category="GIS|EquipmentActor", meta=(BlueprintProtected)) + void ReceiveSourceEquipment(UGIS_EquipmentInstance* NewEquipmentInstance, int32 Idx); + virtual void ReceiveSourceEquipment_Implementation(UGIS_EquipmentInstance* NewEquipmentInstance, int32 Idx); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|EquipmentActor") + UGIS_EquipmentInstance* GetSourceEquipment() const; + virtual UGIS_EquipmentInstance* GetSourceEquipment_Implementation() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GIS|EquipmentActor") + UPrimitiveComponent* GetPrimitiveComponent() const; + virtual UPrimitiveComponent* GetPrimitiveComponent_Implementation() const; + + UFUNCTION(BlueprintNativeEvent, Category="GIS|EquipmentActor", meta=(BlueprintProtected)) + void ReceiveEquipmentBeginPlay(); + virtual void ReceiveEquipmentBeginPlay_Implementation(); + + UFUNCTION(BlueprintNativeEvent, Category="GIS|EquipmentActor", meta=(BlueprintProtected)) + void ReceiveEquipmentEndPlay(); + virtual void ReceiveEquipmentEndPlay_Implementation(); +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInstance.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInstance.h new file mode 100644 index 0000000..e3a96a6 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInstance.h @@ -0,0 +1,247 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/TimerHandle.h" +#include "GIS_EquipmentInterface.h" +#include "GIS_EquipmentStructLibrary.h" +#include "GIS_EquipmentInstance.generated.h" + +class UGIS_EquipmentSystemComponent; +class AActor; +class APawn; +struct FFrame; + +/** + * Delegate triggered when the active state of the equipment instance changes. + * 当装备实例的激活状态改变时触发的委托。 + * @param bNewState The new active state of the equipment. 装备的新激活状态。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_ActiveStateChangedSignature, bool, bNewState); + +/** + * An equipment instance is a UObject tasked with managing the internal logic and runtime states of equipment. + * 装备实例是一个UObject,负责管理装备的内部逻辑和运行时状态。 + * @attention This is the default implementation of EquipmentInterface. You can use other types of classes as equipment. + * @注意 这是EquipmentInterface的默认实现,你可以使用其他类作为装备实例。 + */ +UCLASS(BlueprintType, Blueprintable) +class GENERICINVENTORYSYSTEM_API UGIS_EquipmentInstance : public UObject, public IGIS_EquipmentInterface +{ + GENERATED_BODY() + +public: + /** + * Constructor for the equipment instance. + * 装备实例的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_EquipmentInstance(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~UObject interface + /** + * Checks if the equipment instance supports networking. + * 检查装备实例是否支持网络。 + * @return True if networking is supported, false otherwise. 如果支持网络则返回true,否则返回false。 + */ + virtual bool IsSupportedForNetworking() const override; + + /** + * Gets the world this equipment instance belongs to. + * 获取装备实例所属的世界。 + * @return The world, or nullptr if not set. 世界,如果未设置则返回nullptr。 + */ + virtual UWorld* GetWorld() const override final; + + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + //~End of UObject interface + + // Begin IGIS_EquipmentInterface interface + virtual APawn* GetOwningPawn_Implementation() const override; + virtual UGIS_ItemInstance* GetSourceItem_Implementation() const override; + virtual bool IsEquipmentActive_Implementation() const override; + //EndIGIS_EquipmentInterface interface + + /** + * Gets the owning pawn cast to a specific type. + * 获取转换为特定类型的所属Pawn。 + * @param PawnType The desired pawn class. 期望的Pawn类。 + * @return The owning pawn cast to the specified type, or nullptr if not valid. 转换为指定类型的所属Pawn,如果无效则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|EquipmentInstance", meta=(DeterminesOutputType=PawnType)) + APawn* GetTypedOwningPawn(TSubclassOf PawnType) const; + + /** + * Determines if the equipment can be activated. Override in Blueprint for custom logic. + * 判断装备是否可以激活,可在蓝图中重写以实现自定义逻辑。 + * @return True if the equipment can be activated, false otherwise. 如果装备可以激活则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GIS|EquipmentInstance") + bool CanActivate() const; + + /** + * Gets all actors spawned by this equipment instance. + * 获取由此装备实例生成的所有Actor。 + * @return Array of spawned actors. 生成的Actor数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|EquipmentInstance") + TArray GetEquipmentActors() const { return EquipmentActors; } + + /** + * Get the index of specified equipment actor managed by this equipment instance.获取由此装备实例所管理的装备Actor的下标。 + * @param InEquipmentActor The equipment actor of this equipment instance. 此装备实例的其中一个装备Actor。 + * @return -1 if passed-in actor not created by this instance. 如果传入Actor不是该EquipmentInstance创建的。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|EquipmentInstance") + int32 GetIndexOfEquipmentActor(const AActor* InEquipmentActor) const; + + /** + * Gets the first spawned actor matching the desired class (including subclasses). + * 获取第一个匹配指定类型(包括子类)的由装备实例生成的Actor。 + * @param DesiredClass The desired actor class. 期望的Actor类。 + * @return The matching actor, or nullptr if not found. 匹配的Actor,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|EquipmentInstance", meta=(DeterminesOutputType="DesiredClass", DynamicOutputParam="ReturnValue")) + AActor* GetTypedEquipmentActor(TSubclassOf DesiredClass) const; + + /** + * Event triggered when the active state of the equipment changes. + * 装备激活状态改变时触发的事件。 + */ + UPROPERTY(BlueprintAssignable, Category="EquipmentInstance") + FGIS_ActiveStateChangedSignature OnActiveStateChangedEvent; + +protected: + // Begin IGIS_EquipmentInterface interface + virtual void ReceiveOwningPawn_Implementation(APawn* NewPawn) override; + virtual void ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem) override; + virtual void OnEquipmentBeginPlay_Implementation() override; + virtual void OnEquipmentTick_Implementation(float DeltaSeconds) override; + virtual void OnEquipmentEndPlay_Implementation() override; + virtual void OnActiveStateChanged_Implementation(bool NewActiveState) override; + //EndIGIS_EquipmentInterface interface + +protected: +#if UE_WITH_IRIS + /** + * Registers replication fragments for networking (Iris-specific). + * 为网络注册复制片段(特定于Iris)。 + * @param Context The fragment registration context. 片段注册上下文。 + * @param RegistrationFlags The registration flags. 注册标志。 + */ + virtual void RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) override; +#endif // UE_WITH_IRIS + + /** + * Gets the scene component to which spawned actors will attach. + * 获取生成Actor将附加到的场景组件。 + * @param Pawn The pawn owning this equipment instance. 拥有此装备实例的Pawn。 + * @return The scene component to attach to, or nullptr if not applicable. 要附加到的场景组件,如果不适用则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintPure, Category="GIS|EquipmentInstance") + USceneComponent* GetAttachParentForSpawnedActors(APawn* Pawn) const; + +#pragma region Equipment Actors + + /** + * Spawns and sets up actors associated with this equipment instance. + * 生成并设置与此装备实例关联的Actor。 + * @param ActorsToSpawn The actors to spawn. 要生成的Actor。 + */ + virtual void SpawnAndSetupEquipmentActors(const TArray& ActorsToSpawn); + + /** + * Destroys all actors associated with this equipment instance. + * 销毁与此装备实例关联的所有Actor。 + */ + virtual void DestroyEquipmentActors(); + + /** + * Called before an actor is spawned to allow additional setup. + * 在Actor生成前调用以允许额外设置。 + * @param SpawningActor The actor about to be spawned. 即将生成的Actor。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|EquipmentInstance") + void BeforeSpawningActor(AActor* SpawningActor) const; + + /** + * Sets up actors after they have been spawned. + * 在Actor生成后进行设置。 + * @param InActors The spawned actors to configure. 已生成的Actor,需进行配置。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|EquipmentInstance") + void SetupEquipmentActors(const TArray& InActors); + + /** + * Implementation of SetupEquipmentActors. + * SetupEquipmentActors 的实现。 + * @param InActors The spawned actors to configure. 已生成的Actor,需进行配置。 + */ + virtual void SetupEquipmentActors_Implementation(const TArray& InActors); + + /** + * Called when the equipment actors are replicated. + * 装备Actor复制时调用。 + */ + UFUNCTION() + void OnRep_EquipmentActors(); + + /** + * Checks if the specified number of equipment actors is valid. + * 检查指定数量的装备Actor是否有效。 + * @param Num The number of actors to check. 要检查的Actor数量。 + * @return True if the number of actors is valid, false otherwise. 如果Actor数量有效则返回true,否则返回false。 + */ + bool IsEquipmentActorsValid(int32 Num) const; + + /** + * Propagates initial state to all equipment actors. + * 将初始状态传播到所有装备Actor。 + * @param InActors The actors to set up. 要设置的Actor。 + */ + virtual void SetupInitialStateForEquipmentActors(const TArray& InActors); + + /** + * Propagates active state to all equipment actors. + * 将激活状态传播到所有装备Actor。 + * @param InActors The actors to set up. 要设置的Actor。 + */ + virtual void SetupActiveStateForEquipmentActors(const TArray& InActors) const; + +#pragma endregion + +protected: + /** + * The pawn that owns this equipment instance. + * 拥有此装备实例的Pawn。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="EquipmentInstance") + TObjectPtr OwningPawn; + + /** + * The source item associated with this equipment instance. + * 与此装备实例关联的源道具。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="EquipmentInstance") + TObjectPtr SourceItem; + + /** + * Indicates whether the equipment instance is currently active. + * 指示装备实例当前是否处于激活状态。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="EquipmentInstance") + bool bIsActive; + + /** + * Array of actors spawned by this equipment instance. + * 由此装备实例生成的Actor数组。 + */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="EquipmentInstance", ReplicatedUsing=OnRep_EquipmentActors) + TArray> EquipmentActors; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInterface.h new file mode 100644 index 0000000..02bbc2b --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentInterface.h @@ -0,0 +1,119 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GIS_EquipmentInterface.generated.h" + +class UGIS_ItemInstance; +class Apawn; + +/** + * Interface class for objects that can act as equipment instances (no modifications needed). + * 可作为装备实例的对象的接口类(无需修改)。 + */ +UINTERFACE() +class UGIS_EquipmentInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for objects that wish to function as equipment instances. + * 希望用作装备实例的对象应实现的接口。 + * @details Any object implementing this interface can be used as an equipment instance in the equipment system. + * @细节 实现此接口的任何对象都可在装备系统中用作装备实例。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_EquipmentInterface +{ + GENERATED_BODY() + +public: + /** + * Receives the pawn owning this equipment. + * 接收拥有此装备的Pawn。 + * @param NewPawn The pawn with the equipment system component attached. 挂载了装备系统组件的Pawn。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void ReceiveOwningPawn(APawn* NewPawn); + virtual void ReceiveOwningPawn_Implementation(APawn* NewPawn); + + /** + * Gets the pawn owning this equipment. + * 获取拥有此装备的Pawn。 + * @return The owning pawn, or nullptr if none. 所属Pawn,如果没有则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + APawn* GetOwningPawn() const; + virtual APawn* GetOwningPawn_Implementation() const; + + /** + * Receives the source item from which the equipment was created. + * 接收创建此装备的源道具。 + * @param NewItem The source item instance. 源道具实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void ReceiveSourceItem(UGIS_ItemInstance* NewItem); + virtual void ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem); + + /** + * Gets the source item from which the equipment was created. + * 获取创建此装备的源道具。 + * @return The source item instance, or nullptr if none. 源道具实例,如果没有则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + UGIS_ItemInstance* GetSourceItem() const; + virtual UGIS_ItemInstance* GetSourceItem_Implementation() const; + + /** + * Called after the equipment is added to the equipment system's equipment list. + * 在装备被添加到装备系统的装备列表后调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void OnEquipmentBeginPlay(); + virtual void OnEquipmentBeginPlay_Implementation(); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void OnEquipmentTick(float DeltaSeconds); + virtual void OnOnEquipmentTick_Implementation(float DeltaSeconds); + + /** + * Called before the equipment is removed from the equipment system's equipment list. + * 在从装备系统的装备列表移除装备之前调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void OnEquipmentEndPlay(); + virtual void OnEquipmentEndPlay_Implementation(); + + /** + * Responds to changes in the equipment's active state. + * 响应装备激活状态的变化。 + * @param bNewActiveState The new active state of the equipment. 装备的新激活状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + void OnActiveStateChanged(bool bNewActiveState); + virtual void OnActiveStateChanged_Implementation(bool bNewActiveState); + + /** + * Checks if the equipment is active. + * 检查装备是否处于激活状态。 + * @return True if the equipment is active, false otherwise. 如果装备处于激活状态则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + bool IsEquipmentActive() const; + virtual bool IsEquipmentActive_Implementation() const; + + /** + * Checks if the equipment instance's replication is managed by the equipment system. + * 检查装备实例的复制是否由装备系统管理。 + * @details By default, the equipment system manages only GIS_EquipmentInstance and its subclasses. + * @细节 默认情况下,装备系统仅管理GIS_EquipmentInstance及其子类。 + * @attention Do not override this method unless you fully understand its implications. + * @注意 除非完全了解其影响,否则不要覆写此方法。 + * @return True if replication is managed by the equipment system, false otherwise. 如果复制由装备系统管理则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|Equipment") + bool IsReplicationManaged(); + virtual bool IsReplicationManaged_Implementation(); +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentStructLibrary.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentStructLibrary.h new file mode 100644 index 0000000..7b60017 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentStructLibrary.h @@ -0,0 +1,270 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Object.h" +#include "GIS_EquipmentStructLibrary.generated.h" + +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_EquipmentActorToSpawn +{ + GENERATED_BODY() + + FGIS_EquipmentActorToSpawn() + { + } + + /** + * Actor class must implements GIS_EquipmentActorInterface! + */ + UPROPERTY(EditAnywhere, BlueprintReadonly, Category = Equipment, meta=(MustImplement="/Script/GenericInventorySystem.GIS_EquipmentActorInterface")) + TSoftClassPtr ActorToSpawn; + + /** + * If the spawned actor will attach to equipment owner? + */ + UPROPERTY(EditAnywhere, Category = Equipment) + bool bShouldAttach{true}; + + /** + * The socket to attach to. + */ + UPROPERTY(EditAnywhere, Category = Equipment, meta = (EditCondition = "bShouldAttach", EditConditionHides)) + FName AttachSocket; + + /** + * The relative transform to attach to. + */ + UPROPERTY(EditAnywhere, Category = Equipment, meta = (EditCondition = "bShouldAttach", EditConditionHides)) + FTransform AttachTransform; +}; + +class UGIS_ItemInstance; +class UGIS_EquipmentInstance; +class UGIS_EquipItemInstance; +class UGIS_ItemDefinition; +class UGIS_EquipmentSystemComponent; +struct FGIS_EquipmentContainer; + +/** + * Structure representing an equipment entry in the container. + * 表示容器中装备条目的结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_EquipmentEntry : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * Default constructor for equipment entry. + * 装备条目的默认构造函数。 + */ + FGIS_EquipmentEntry() + { + } + + /** + * Gets a debug string representation of the equipment entry. + * 获取装备条目的调试字符串表示。 + * @return The debug string. 调试字符串。 + */ + FString GetDebugString() const; + +private: + friend FGIS_EquipmentContainer; + friend UGIS_EquipmentSystemComponent; + + /** + * The equipment instance. + * 装备实例。 + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment", meta = (ShowInnerProperties)) + TObjectPtr Instance = nullptr; + + /** + * The item instance associated with this equipment. + * 与此装备关联的道具实例。 + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment") + TObjectPtr ItemInstance{nullptr}; + + /** + * The slot where the equipment is equipped. + * 装备所在的装备槽。 + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment") + FGameplayTag EquippedSlot; + + /** + * Which group the equipment belongs to. + * 此装备属于哪个组? + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment") + FGameplayTag EquippedGroup; + + /** + * Indicates whether the equipment is active. + * 指示装备是否处于激活状态。 + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment") + bool bActive{false}; + + /** + * Previous active state (not replicated). + * 上一个激活状态(不复制)。 + */ + UPROPERTY(VisibleAnywhere, Category = "Equipment", NotReplicated) + bool bPrevActive = false; + + UPROPERTY(VisibleAnywhere, Category = "Equipment", NotReplicated) + FGameplayTag PrevEquippedGroup; + + bool CheckClientDataReady() const; + + /** + * Checks if the equipment entry is valid. + * 检查装备条目是否有效。 + * @return True if the entry is valid, false otherwise. 如果条目有效则返回true,否则返回false。 + */ + bool IsValidEntry() const; +}; + +/** + * Container for a list of applied equipment. + * 存储已应用装备列表的容器。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_EquipmentContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for equipment container. + * 装备容器的默认构造函数。 + */ + FGIS_EquipmentContainer() + : OwningComponent(nullptr) + { + } + + /** + * Constructor for equipment container with an owning component. + * 使用所属组件构造装备容器。 + * @param InComponent The owning equipment system component. 所属的装备系统组件。 + */ + FGIS_EquipmentContainer(UGIS_EquipmentSystemComponent* InComponent) + : OwningComponent(InComponent) + { + } + + //~FFastArraySerializer contract + /** + * Called before equipment entries are removed during replication. + * 复制期间在移除装备条目前调用。 + * @param RemovedIndices The indices of removed entries. 移除条目的索引。 + * @param FinalSize The final size of the array after removal. 移除后数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after equipment entries are added during replication. + * 复制期间在添加装备条目后调用。 + * @param AddedIndices The indices of added entries. 添加条目的索引。 + * @param FinalSize The final size of the array after addition. 添加后数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after equipment entries are changed during replication. + * 复制期间在装备条目更改后调用。 + * @param ChangedIndices The indices of changed entries. 更改条目的索引。 + * @param FinalSize The final size of the array after changes. 更改后数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for the equipment container. + * 处理装备容器的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Entries, DeltaParms, *this); + } + + int32 IndexOfBySlot(const FGameplayTag& Slot) const; + + //check if any equipment's group matches this group. + int32 IndexOfByGroup(const FGameplayTag& Group) const; + + int32 IndexOfByInstance(const UObject* Instance) const; + + int32 IndexOfByItem(const UGIS_ItemInstance* Item) const; + + int32 IndexOfByItemId(const FGuid& ItemId) const; + + /** + * Replicated list of equipment entries. + * 复制的装备条目列表。 + */ + UPROPERTY(VisibleAnywhere, Category = "EquipmentSystem", meta = (ShowOnlyInnerProperties, DisplayName = "Equipments")) + TArray Entries; + + /** + * The equipment system component that owns this container. + * 拥有此容器的装备系统组件。 + */ + UPROPERTY() + TObjectPtr OwningComponent; +}; + +/** + * Template specialization to enable network delta serialization for the equipment container. + * 为装备容器启用网络增量序列化的模板特化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true + }; +}; + +/** + * Structure representing a group active index entry. + * 表示组激活索引条目的结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_EquipmentGroupEntry : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * The group tag. + * 组标签。 + */ + UPROPERTY(VisibleAnywhere, Category = "EquipmentGroup") + FGameplayTag GroupTag; + + /** + * The active slot within the group. + * 组内的激活槽位。 + */ + UPROPERTY(VisibleAnywhere, Category = "EquipmentGroup") + FGameplayTag ActiveSlot; + + UPROPERTY(NotReplicated) + FGameplayTag LastSlot; + + /** + * Checks if the entry is valid. + * 检查条目是否有效。 + */ + bool IsValid() const { return GroupTag.IsValid(); } +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentSystemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentSystemComponent.h new file mode 100644 index 0000000..f5d9002 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Equipping/GIS_EquipmentSystemComponent.h @@ -0,0 +1,753 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_EquipmentStructLibrary.h" +#include "GIS_InventoryMeesages.h" +#include "Items/GIS_ItemInfo.h" +#include "Components/PawnComponent.h" +#include "GIS_EquipmentSystemComponent.generated.h" + +class UGIS_ItemSlotCollectionDefinition; +class UGIS_InventorySystemComponent; +class UGIS_EquipmentInstance; +class UGIS_ItemSlotCollection; +class UGIS_EquipItemInstance; + +/** + * Delegate triggered when the equipment system is initialized. + * 装备系统初始化时触发的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGIS_Equipment_InitializedSignature); + +/** + * Delegate triggered when an equipment's state changes (equipped or unequipped). + * 装备状态更改(装备或卸下)时触发的委托。 + * @param Equipment The equipment instance. 装备实例。 + * @param SlotTag The slot tag associated with the equipment. 与装备关联的槽标签。 + * @param bEquipped Whether the equipment is equipped. 装备是否已装备。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_Equipment_StateChangedSignature, UObject *, Equipment, FGameplayTag, SlotTag, bool, bEquipped); + +/** + * Delegate triggered when an equipment's active state changes. + * 装备激活状态更改时触发的委托。 + * @param Equipment The equipment instance. 装备实例。 + * @param SlotTag The slot tag associated with the equipment. 与装备关联的槽标签。 + * @param bActive Whether the equipment is active. 装备是否激活。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_Equipment_ActiveStateChangedSignature, UObject *, Equipment, FGameplayTag, SlotTag, bool, bActive); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_Equipment_GroupStateChangedSignature, UObject *, Equipment, FGameplayTag, SlotTag, FGameplayTag, GroupTag); + +/** + * Dynamic delegate for equipment system initialization events. + * 装备系统初始化事件的动态委托。 + */ +UDELEGATE() +DECLARE_DYNAMIC_DELEGATE(FGIS_EquipmentSystem_Initialized_DynamicEvent); + +/** + * Manager of equipment instances for a pawn. + * 管理棋子装备实例的组件。 + */ +UCLASS(ClassGroup = (GIS), meta = (BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_EquipmentSystemComponent : public UPawnComponent +{ + GENERATED_BODY() + + friend FGIS_EquipmentContainer; + friend FGIS_EquipmentEntry; + +public: + /** + * Sets default values for this component's properties. + * 为组件的属性设置默认值。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_EquipmentSystemComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~UObject interface + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Replicates subobjects for this component. + * 为此组件复制子对象。 + * @param Channel The actor channel. 演员通道。 + * @param Bunch The replication data bunch. 复制数据束。 + * @param RepFlags The replication flags. 复制标志。 + * @return True if subobjects were replicated, false otherwise. 如果子对象被复制则返回true,否则返回false。 + */ + virtual bool ReplicateSubobjects(class UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags) override; + //~End of UObject interface + + //~UActorComponent interface + /** + * Called when the component is registered. + * 组件注册时调用。 + */ + virtual void OnRegister() override; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Prepares the component for replication. + * 为组件的复制做准备。 + */ + virtual void ReadyForReplication() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Updates the component each frame. + * 每帧更新组件。 + * @param DeltaTime Time since the last tick. 上次tick以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + /** + * Uninitializes the component. + * 取消初始化组件。 + */ + virtual void UninitializeComponent() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason the game ended. 游戏结束的原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~End of UActorComponent interface + +#pragma region Equipment System + /** + * Gets the equipment system component from the specified actor. + * 从指定演员获取装备系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @return The equipment system component, or nullptr if not found. 装备系统组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem", Meta = (DefaultToSelf = "Actor")) + static UGIS_EquipmentSystemComponent* GetEquipmentSystemComponent(const AActor* Actor); + + /** + * Finds the equipment system component on the specified actor. + * 在指定演员上查找装备系统组件。 + * @param Actor The actor to search for the component. 要查找组件的演员。 + * @param Component The found equipment system component (output). 找到的装备系统组件(输出)。 + * @return True if the component was found, false otherwise. 如果找到组件则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem", Meta = (DefaultToSelf = "Actor", ExpandBoolAsExecs = "ReturnValue")) + static bool FindEquipmentSystemComponent(const AActor* Actor, UGIS_EquipmentSystemComponent*& Component); + + /** + * Finds a typed equipment system component on the specified actor. + * 在指定演员上查找类型化的装备系统组件。 + * @param Actor The actor to search for the component. 要查找组件的演员。 + * @param DesiredClass The desired class of the component. 组件的期望类。 + * @param Component The found equipment system component (output). 找到的装备系统组件(输出)。 + * @return True if the component was found, false otherwise. 如果找到组件则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem", + meta = (DefaultToSelf = "Actor", DynamicOutputParam = "Component", DeterminesOutputType = "DesiredClass", ExpandBoolAsExecs = "ReturnValue")) + static bool FindTypedEquipmentSystemComponent(AActor* Actor, TSubclassOf DesiredClass, UGIS_EquipmentSystemComponent*& Component); + + /** + * Initializes the equipment system. + * 初始化装备系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void InitializeEquipmentSystem(); + + /** + * Initializes the equipment system with a specified inventory system. + * 使用指定的库存系统初始化装备系统。 + * @param InventorySystem The inventory system to associate with. 要关联的库存系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void InitializeEquipmentSystemWithInventory(UGIS_InventorySystemComponent* InventorySystem); + + /** + * Resets the equipment system. + * 重置装备系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void ResetEquipmentSystem(); + + /** + * Gets the target collection tag for equipment monitoring. + * 获取用于装备监控的目标集合标签。 + * @return The target collection tag. 目标集合标签。 + */ + FGameplayTag GetTargetCollectionTag() const { return TargetCollectionTag; }; + + /** + * Checks if the equipment system is initialized. + * 检查装备系统是否已初始化。 + * @return True if initialized, false otherwise. 如果已初始化则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|InventorySystem") + bool IsEquipmentSystemInitialized() const; + + /** + * Binds a delegate to be called when the equipment system is initialized. + * 绑定一个委托,在装备系统初始化时调用。 + * @param Delegate The delegate to bind. 要绑定的委托。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|InventorySystem") + void BindToEquipmentSystemInitialized(FGIS_EquipmentSystem_Initialized_DynamicEvent Delegate); + + /** + * Removes all equipment instances from the system. + * 从系统中移除所有装备实例。 + */ + virtual void RemoveAllEquipments(); + + /** + * Equips an item to a specific slot. + * 将道具装备到指定槽。 + * @param Item The item instance to equip. 要装备的道具实例。 + * @param SlotTag The slot tag to equip the item to. 装备道具的槽标签。 + */ + virtual void EquipItemToSlot(UGIS_ItemInstance* Item, const FGameplayTag& SlotTag); + + /** + * Unequips an item from a specific slot. + * 从指定槽卸下装备。 + * @param SlotTag The slot tag to unequip. 要卸下的槽标签。 + */ + virtual void UnequipBySlot(FGameplayTag SlotTag); + + /** + * Unequips an item by its ID. + * 通过道具ID卸下装备。 + * @param ItemId The ID of the item to unequip. 要卸下的道具ID。 + */ + virtual void UnequipByItem(const FGuid& ItemId); + + /** + * Checks if the owning actor has authority. + * 检查拥有者演员是否具有权限。 + * @return True if the owner has authority, false otherwise. 如果拥有者有权限则返回true,否则返回false。 + */ + bool OwnerHasAuthority() const; + +protected: + /** + * Called when the target collection's item stacks change. + * 目标集合的道具堆栈更改时调用。 + * @param Message The update message containing stack details. 包含堆栈详细信息的更新消息。 + */ + UFUNCTION() + virtual void OnTargetCollectionChanged(const FGIS_InventoryStackUpdateMessage& Message); + + /** + * Called when the target collection is removed. + * 目标集合移除时调用。 + * @param Collection The removed collection. 移除的集合。 + */ + UFUNCTION() + virtual void OnTargetCollectionRemoved(UGIS_ItemCollection* Collection); + + /** + * Called when the equipment system is initialized. + * 装备系统初始化时调用。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GIS|EquipmentSystem") + void OnEquipmentSystemInitialized(); + + /** + * List of delegates for equipment system initialization. + * 装备系统初始化的委托列表。 + */ + UPROPERTY() + TArray InitializedDelegates; + +#pragma endregion + +#pragma region Equipments Query + +public: + /** + * Gets all equipment instances matching the specified conditions. + * 获取匹配指定条件的所有装备实例。 + * @param InstanceType The type of equipment instance to query. 要查询的装备实例类型。 + * @param SlotQuery The gameplay tag query for slots. 槽的游戏标签查询。 + * @return Array of matching equipment instances, or empty if none found. 匹配的装备实例数组,如果未找到则返回空。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", meta = (DeterminesOutputType = InstanceType, DynamicOutputParam = ReturnValue)) + TArray GetEquipments(UPARAM(meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false")) + TSubclassOf + InstanceType, + UPARAM(meta=(Categories="GIS.Slots")) FGameplayTagQuery SlotQuery) const; + + /** + * Gets all active equipment instances matching the specified conditions. + * 获取匹配指定条件的激活装备实例。 + * @param InstanceType The type of equipment instance to query. 要查询的装备实例类型。 + * @param SlotQuery The gameplay tag query for slots. 槽的游戏标签查询。 + * @return Array of active equipment instances, or empty if none found. 激活的装备实例数组,如果未找到则返回空。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", + meta = (DeterminesOutputType = InstanceType, DynamicOutputParam = ReturnValue)) + TArray GetActiveEquipments(UPARAM(meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false")) + TSubclassOf + InstanceType, + UPARAM(meta=(Categories="GIS.Slots")) FGameplayTagQuery SlotQuery) const; + + /** + * Gets the first equipment instance matching the specified query. + * 获取匹配指定查询的第一个装备实例。 + * @param InstanceType The type of equipment instance to query. 要查询的装备实例类型。 + * @param SlotQuery The gameplay tag query for slots. 槽的游戏标签查询。 + * @return The first matching equipment instance, or nullptr if none found. 第一个匹配的装备实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", meta = (DeterminesOutputType = InstanceType, DynamicOutputParam = ReturnValue)) + UObject* GetEquipment(UPARAM(meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false")) + TSubclassOf + InstanceType, + UPARAM(meta=(Categories="GIS.Slots")) FGameplayTagQuery SlotQuery) const; + + /** + * Gets the first active equipment instance matching the specified query. + * 获取匹配指定查询的第一个激活装备实例。 + * @param InstanceType The type of equipment instance to query. 要查询的装备实例类型。 + * @param SlotQuery The gameplay tag query for slots. 槽的游戏标签查询。 + * @return The first active equipment instance, or nullptr if none found. 第一个激活的装备实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", meta = (DeterminesOutputType = InstanceType, DynamicOutputParam = ReturnValue)) + UObject* GetActiveEquipment(UPARAM(meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false")) + TSubclassOf + InstanceType, + UPARAM(meta=(Categories="GIS.Slots")) FGameplayTagQuery SlotQuery) const; + + /** + * Gets the active equipment within specified equipment group. + * 获取指定组中的激活装备。 + * @param GroupTag The equipment group to look for. 要查询的装备组。 + * @param bExactMatch If true, the group tag has to be exactly present. 如果为真,对组标签进行绝对匹配。 + * @return active equipment in group. 组中激活的装备。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem") + UObject* GetActiveEquipmentInGroup(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag, bool bExactMatch = true) const; + + /** + * Gets the equipment instance that spawned the specified equipment actor. + * 获取生成指定装备演员的装备实例。 + * @param EquipmentActor The equipment actor to query. 要查询的装备演员。 + * @return The equipment instance, or nullptr if not found. 装备实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", meta = (DefaultToSelf = "EquipmentActor")) + UGIS_EquipmentInstance* GetEquipmentInstanceOfActor(AActor* EquipmentActor) const; + + /** + * Gets the typed equipment instance that spawned the specified equipment actor. + * 获取生成指定装备演员的类型化装备实例。 + * @param InstanceType The desired type of equipment instance. 期望的装备实例类型。 + * @param EquipmentActor The equipment actor to query. 要查询的装备演员。 + * @return The typed equipment instance, or nullptr if not found. 类型化的装备实例,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GIS|EquipmentSystem", + meta = (DefaultToSelf = "EquipmentActor", DeterminesOutputType = InstanceType, DynamicOutputParam = ReturnValue)) + UGIS_EquipmentInstance* GetTypedEquipmentInstanceOfActor(TSubclassOf InstanceType, AActor* EquipmentActor) const; + + /** + * Gets the equipment instance in a specific slot. + * 获取特定槽中的装备实例。 + * @param SlotTag The slot tag to query. 要查询的槽标签。 + * @return The equipment instance in the slot, or nullptr if none. 槽中的装备实例,如果没有则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + UObject* GetEquipmentInSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag SlotTag) const; + + /** + * Gets the equipment instance associated with a specific item instance. + * 获取与特定道具实例关联的装备实例。 + * @param Item The item instance to query. 要查询的道具实例。 + * @return The equipment instance, or nullptr if none. 装备实例,如果没有则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + UObject* GetEquipmentByItem(const UGIS_ItemInstance* Item); + +#pragma endregion + +#pragma region Equipments Activation/Deactivation + /** + * Sets the active state of equipment in a specific slot. + * 设置特定槽中装备的激活状态。 + * @param SlotTag The slot tag of the equipment. 装备的槽标签。 + * @param NewActiveState The new active state. 新的激活状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void SetEquipmentActiveState(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag SlotTag, bool NewActiveState); + + /** + * Server-side function to set the active state of an equipment in a specific slot. + * 服务器端函数,设置特定槽中装备的激活状态。 + * @param SlotTag The slot tag of the equipment. 装备的槽标签。 + * @param NewActiveState The new active state. 新的激活状态。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + void ServerSetEquipmentActiveState(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag SlotTag, bool NewActiveState); + virtual void ServerSetEquipmentActiveState_Implementation(FGameplayTag SlotTag, bool NewActiveState); + +protected: + /** + * Directly update the active state and group state for equipment entry at Idx. + * 直接设置装备的激活状态和分组。 + * @param Idx The index of the equipment entry. 装备条目索引。 + * @param NewActiveState The new active state. 新的激活状态。 + * @param NewGroup The new group state. 新的组状态。 + * + */ + virtual void UpdateEquipmentState(int32 Idx, bool NewActiveState, FGameplayTag NewGroup); + +#pragma endregion + +#pragma region Events + +public: + /** + * Event triggered when the equipment system is initialized. + * 装备系统初始化时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Equipment_InitializedSignature OnEquipmentSystemInitializedEvent; + + /** + * Event triggered when an equipment's state changes. + * 装备状态更改时触发的事件。 + * @attention Called after equipping, before unequipping. + * @attention 在装备后、卸下前调用。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Equipment_StateChangedSignature OnEquipmentStateChangedEvent; + + /** + * Event triggered when an equipment's active state changes. + * 装备激活状态更改时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Equipment_ActiveStateChangedSignature OnEquipmentActiveStateChangedEvent; + + UPROPERTY(BlueprintAssignable) + FGIS_Equipment_GroupStateChangedSignature OnEquipmentGroupStateChangedEvent; + +#pragma endregion + +#pragma region Slot Query + /** + * Checks if a specific slot is equipped. + * 检查特定槽是否已装备。 + * @param SlotTag The slot tag to check. 要检查的槽标签。 + * @return True if the slot is equipped, false otherwise. 如果槽已装备则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + bool IsSlotEquipped(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag SlotTag) const; + + /** + * Gets the slot where the equipment instance was equipped to. + * 获取特定装备实例所装备的位置。 + * @param Equipment The equipment to look for. 与要查询装备实例。 + * @return Invalid slot if not found. 如果没查到则返回无效标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + FGameplayTag GetSlotByEquipment(UObject* Equipment) const; + + /** + * Gets the slot where the equipment instance associated with a specific item instance was equipped to. + * 获取与特定物品实例关联的装备实例所装备的位置。 + * @param Item The item instance which the equipment instance was associated with. 与要查询装备实例关联的道具实例。 + * @return Invalid slot if not found. 如果没查到则返回无效标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + FGameplayTag GetSlotByItem(const UGIS_ItemInstance* Item) const; + + /** + * Converts a slot tag to the corresponding equipment entry index. + * 将槽标签转换为对应的装备条目索引。 + * @param InSlotTag The slot tag to convert. 要转换的槽标签。 + * @return The equipment entry index, or INDEX_NONE if not found. 装备条目索引,如果未找到则返回INDEX_NONE。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + int32 SlotTagToEquipmentInex(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag InSlotTag) const; + + /** + * Converts an item ID to the corresponding equipment entry index. + * 将道具ID转换为对应的装备条目索引。 + * @param InItemId The item ID to convert. 要转换的道具ID。 + * @return The equipment entry index, or INDEX_NONE if not found. 装备条目索引,如果未找到则返回INDEX_NONE。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GIS|EquipmentSystem") + int32 ItemIdToEquipmentInex(FGuid InItemId) const; + +#pragma endregion + +#pragma region Equipment Entries + +protected: + /** + * Adds an equipment entry to the system. + * 将装备条目添加到系统中。 + * @param NewEntry The new equipment entry to add. 要添加的新装备条目。 + */ + virtual void AddEquipmentEntry(const FGIS_EquipmentEntry& NewEntry); + + /** + * Ticks all the equipment instances in this container. + * 更新所有装备实例。 + * @param DeltaTime The time between frames. 每帧的间隔时间(秒) + */ + virtual void TickEquipmentEntries(float DeltaTime); + + /** + * Removes an equipment entry by its index. + * 通过索引移除装备条目。 + * @param Idx The index of the equipment entry to remove. 要移除的装备条目索引。 + */ + virtual void RemoveEquipmentEntry(int32 Idx); + + /** + * Creates an equipment instance for the specified item. + * 为指定道具创建装备实例。 + * @attention The returned instance must implement GIS_EquipmentInterface. + * @attention 返回的实例必须实现GIS_EquipmentInterface。 + * @param Owner The owning actor of the created equipment instance. 创建的装备实例的所属演员。 + * @param ItemInstance The item instance containing equipment-related data. 包含装备相关数据的道具实例。 + * @return The created equipment instance (UObject or AActor based on design). 创建的装备实例(根据设计为UObject或AActor)。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GIS|EquipmentSystem") + UObject* CreateEquipmentInstance(AActor* Owner, UGIS_ItemInstance* ItemInstance) const; + + /** + * Called when an equipment entry is added. + * 装备条目添加时调用。 + * @param Entry The added equipment entry. 添加的装备条目。 + * @param Idx The index of the added entry. 添加条目的索引。 + */ + virtual void OnEquipmentEntryAdded(const FGIS_EquipmentEntry& Entry, int32 Idx); + + /** + * Called when an equipment entry is changed. + * 装备条目更改时调用。 + * @param Entry The changed equipment entry. 更改的装备条目。 + * @param Idx The index of the changed entry. 更改条目的索引。 + */ + virtual void OnEquipmentEntryChanged(const FGIS_EquipmentEntry& Entry, int32 Idx); + + /** + * Called when an equipment entry is removed. + * 装备条目移除时调用。 + * @param Entry The removed equipment entry. 移除的装备条目。 + * @param Idx The index of the removed entry. 移除条目的索引。 + */ + virtual void OnEquipmentEntryRemoved(const FGIS_EquipmentEntry& Entry, int32 Idx); + + /** + * Adds a replicated equipment object to the system. + * 将复制的装备对象添加到系统中。 + * @param Instance The equipment instance to add. 要添加的装备实例。 + */ + virtual void AddReplicatedEquipmentObject(TObjectPtr Instance); + + /** + * Removes a replicated equipment object from the system. + * 从系统中移除复制的装备对象。 + * @param Instance The equipment instance to remove. 要移除的装备实例。 + */ + virtual void RemoveReplicatedEquipmentObject(TObjectPtr Instance); + + /** + * Processes pending equipment entries. + * 处理待处理的装备条目。 + */ + virtual void ProcessPendingEquipments(); + + /** + * List of pending replicated equipment objects. + * 待复制的装备对象列表。 + */ + TArray> PendingReplicatedEquipments; + + /** + * Map of pending equipment entries. + * 待处理装备条目的映射。 + */ + TMap PendingEquipmentEntries; + +#pragma endregion + +#pragma region Equipment Groups + +public: + /** + * Gets the layout of an equipment group. + * 获取装备组的布局。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @return Map of indices to slot tags in the group. 组内索引到槽标签的映射。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual TMap GetLayoutOfGroup(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag) const; + + /** + * Gets the layout of an equipment group. + * 获取装备组的布局。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @return Map of indices to slot tags in the group. 组内索引到槽标签的映射。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual TMap GetSlottedLayoutOfGroup(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag) const; + + /** + * Get the matching equipment group tag for equipment slot. 获取装备槽所在的装备组。 + * @param SlotTag The equipment slot to check. 要检查的装备槽。 + * @return The equipment group tag, none if not groupped. 装备组标签,如果没有组,返回None + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem") + FGameplayTag FindMatchingGroupForSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag SlotTag) const; + + /** + * Gets all equipment instances in an equipment group. + * 获取装备组中的所有装备实例。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @return Map of indices to equipment instances in the group. 组内索引到装备实例的映射。 + */ + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual TMap GetEquipmentsOfGroup(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag) const; + + UFUNCTION(BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual TMap GetSlottedEquipmentsOfGroup(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag) const; + + /** + * Sets the active slot within an equipment group. + * 设置装备组中的激活槽。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @param NewSlot The new active slot. 新的激活槽。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void SetGroupActiveSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag, FGameplayTag NewSlot); + + /** + * Server-side function to set the active slot within an equipment group. + * 服务器端函数,设置装备组中的激活槽位。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @param NewSlot The new active slot. 新的激活槽位。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual void ServerSetGroupActiveSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag, FGameplayTag NewSlot); + + /** + * Cycles the active index within an equipment group in the specified direction. + * 按指定方向在装备组中循环激活索引。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @param bDirection The cycle direction (true for right, false for left). 循环方向(true为右,false为左)。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GIS|EquipmentSystem") + virtual void CycleGroupActiveSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag, bool bDirection = true); + + virtual FGameplayTag CycleGroupNextSlot(FGameplayTag GroupTag, FGameplayTag PrevSlot, bool bDirection = true); + + /** + * Server-side function to cycle the active slot within an equipment group. + * 服务器端函数,按指定方向在装备组中循环激活槽位。 + * @param GroupTag The tag of the equipment group. 装备组的标签。 + * @param bDirection The cycle direction (true for right, false for left). 循环方向(true为右,false为左)。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category = "GIS|EquipmentSystem") + virtual void ServerCycleGroupActiveSlot(UPARAM(meta=(Categories="GIS.Slots")) FGameplayTag GroupTag, bool bDirection = true); + +protected: +#pragma endregion + +#pragma region Properties + /** + * Whether to initialize the equipment system automatically on BeginPlay. + * 是否在BeginPlay时自动初始化装备系统。 + */ + UPROPERTY(EditDefaultsOnly, Category = "EquipmentSystem") + bool bInitializeOnBeginPlay = false; + + /** + * Indicates if the equipment system is initialized. + * 指示装备系统是否已初始化。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "EquipmentSystem", ReplicatedUsing = OnEquipmentSystemInitialized) + bool bEquipmentSystemInitialized = false; + + /** + * The target collection tag for monitoring equipment-related items. + * 用于监控装备相关道具的目标集合标签。 + * @attention Monitors the specified item slot collection in the actor's inventory and generates/removes equipment instances based on changes. + * @attention 监听同一演员库存中的指定道具槽集合,并根据变化生成/移除装备实例。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "EquipmentSystem", meta = (AllowPrivateAccess = True, Categories = "GIS.Collection")) + FGameplayTag TargetCollectionTag; + + /** + * Container for the equipment entries. + * 装备条目的容器。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "EquipmentSystem", Replicated, meta = (ShowOnlyInnerProperties)) + FGIS_EquipmentContainer Container; + + /** + * The definition of the target collection where equipment groups are defined. + * 定义装备组的目标集合定义。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "EquipmentSystem", Replicated) + TObjectPtr TargetCollectionDefinition; + + // /** + // * Container for group active indices. + // * 组激活索引的容器。 + // */ + // UPROPERTY(VisibleInstanceOnly, Category = "EquipmentSystem", Replicated, meta = (ShowOnlyInnerProperties)) + // FGIS_EquipmentGroupContainer GroupActiveIndexContainer; + + /** + * Track which slot was equipped to which group. + */ + UPROPERTY(VisibleInstanceOnly, Category = "EquipmentSystem") + TMap GroupActiveSlots; + + + /** + * The associated inventory system component. + * 关联的库存系统组件。 + */ + UPROPERTY() + TObjectPtr Inventory; + + /** + * The target item slot collection. + * 目标道具槽集合。 + */ + UPROPERTY() + TObjectPtr TargetCollection; + + /** + * Mapping of slot tags to equipment entry indices. + * 槽标签到装备条目索引的映射。 + */ + UPROPERTY(VisibleInstanceOnly, Category = "EquipmentSystem", meta = (ForceInlineRow), Transient) + TMap> SlotToInstanceMap; + +#pragma endregion +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyContainer.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyContainer.h new file mode 100644 index 0000000..1fc1d98 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyContainer.h @@ -0,0 +1,105 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "GameplayTagContainer.h" +#include "GIS_CurrencyEntry.h" +#include "GIS_CurrencyContainer.generated.h" + +class UGIS_CurrencySystemComponent; +class UGIS_CurrencyDefinition; +struct FGIS_CurrencyContainer; + +/** + * Container for storing currency entries. + * 用于存储货币条目的容器。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_CurrencyContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + /** + * Default constructor for the currency container. + * 货币容器的默认构造函数。 + */ + FGIS_CurrencyContainer() + { + }; + + /** + * Constructor for the currency container with an owning component. + * 使用拥有组件构造货币容器。 + * @param InOwner The owning currency system component. 拥有此容器的货币系统组件。 + */ + FGIS_CurrencyContainer(UGIS_CurrencySystemComponent* InOwner) + : OwningComponent(InOwner) + { + } + + //~FFastArraySerializer contract + /** + * Called before entries are removed during replication. + * 复制期间移除条目前调用。 + * @param RemovedIndices The indices of entries to remove. 要移除的条目索引。 + * @param FinalSize The final size of the entries array after removal. 移除后条目数组的最终大小。 + */ + void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize); + + /** + * Called after entries are added during replication. + * 复制期间添加条目后调用。 + * @param AddedIndices The indices of added entries. 添加的条目索引。 + * @param FinalSize The final size of the entries array after addition. 添加后条目数组的最终大小。 + */ + void PostReplicatedAdd(const TArrayView AddedIndices, int32 FinalSize); + + /** + * Called after entries are changed during replication. + * 复制期间条目更改后调用。 + * @param ChangedIndices The indices of changed entries. 更改的条目索引。 + * @param FinalSize The final size of the entries array after change. 更改后条目数组的最终大小。 + */ + void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize); + //~End of FFastArraySerializer contract + + /** + * Handles delta serialization for network replication. + * 处理网络复制的增量序列化。 + * @param DeltaParms The serialization parameters. 序列化参数。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FastArrayDeltaSerialize(Entries, DeltaParms, *this); + } + + /** + * The owning currency system component. + * 拥有此容器的货币系统组件。 + */ + UPROPERTY() + TObjectPtr OwningComponent{nullptr}; + + /** + * Array of currency entries. + * 货币条目数组。 + */ + UPROPERTY(VisibleAnywhere, Category="Currency", meta=(DisplayName="Currencies", TitleProperty="{Definition}->{Value}")) + TArray Entries; +}; + +/** + * Traits for the currency container to enable network delta serialization. + * 货币容器的特性,用于启用网络增量序列化。 + */ +template <> +struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true, + }; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyDefinition.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyDefinition.h new file mode 100644 index 0000000..33a26d6 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyDefinition.h @@ -0,0 +1,127 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "Engine/Texture2D.h" +#include "GIS_CurrencyDefinition.generated.h" + +class UGIS_CurrencyDefinition; + +/** + * Struct to represent a currency exchange rate. + * 表示货币汇率的结构体。 + */ +struct FGIS_CurrencyExchangeRate +{ + /** + * The currency definition for the exchange rate. + * 汇率的货币定义。 + */ + TObjectPtr Currency; + + /** + * The exchange rate value. + * 汇率值。 + */ + float ExchangeRate; + + /** + * Constructor for the currency exchange rate. + * 货币汇率的构造函数。 + * @param InCurrency The currency definition. 货币定义。 + * @param InExchangeRate The exchange rate value. 汇率值。 + */ + FGIS_CurrencyExchangeRate(const UGIS_CurrencyDefinition* InCurrency, float InExchangeRate); +}; + +/** + * Defines properties for a currency type in the inventory system. + * 定义库存系统中货币类型的属性。 + */ +UCLASS(BlueprintType) +class GENERICINVENTORYSYSTEM_API UGIS_CurrencyDefinition : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * The display name of the currency for UI purposes. + * 货币的UI显示名称。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Common") + FText DisplayName; + + /** + * The description of the currency for UI purposes. + * 货币的UI描述。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Common") + FText Description; + + /** + * The icon for the currency for UI display. + * 货币的UI图标。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Common") + TSoftObjectPtr Icon; + + /** + * The maximum amount allowed for this currency (0 for unlimited). + * 货币的最大数量(0表示无限制)。 + * @details If the amount exceeds this value, it will attempt to convert to another currency. + * @细节 如果数量超过此值,将尝试转换为其他货币。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Common", meta=(ClampMin=0)) + float MaxAmount{0}; + + /** + * The parent currency used to compute exchange rates. + * 用于计算汇率的父级货币。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Exchange") + TObjectPtr ParentCurrency; + + /** + * The exchange rate to the parent currency (e.g., 100 cents = 1 dollar). + * 相对于父级货币的汇率(例如,100分=1美元)。 + * @details If this currency is a 10-unit note and the parent is a 100-unit note, set to 10 for 1:10 exchange. + * @细节 如果此货币是10元钞,父级是100元钞,设置值为10表示1个100元钞可兑换10个10元钞。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Exchange", meta=(ClampMin=1)) + float ExchangeRateToParent{1}; + + /** + * The currency to convert to when this currency exceeds MaxAmount. + * 当货币数量超过最大值时转换到的溢出货币。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Exchange") + TObjectPtr OverflowCurrency; + + /** + * The currency to convert fractional remainders to. + * 分数余数转换到的分数货币。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Exchange") + TObjectPtr FractionCurrency; + + /** + * Attempts to get the exchange rate to another currency. + * 尝试获取针对其他货币的汇率。 + * @param OtherCurrency The currency to compare with. 要比较的其他货币。 + * @param ExchangeRate The resulting exchange rate (output). 输出的汇率。 + * @return True if the exchange rate was found, false otherwise. 如果找到汇率则返回true,否则返回false。 + * @details Returns the rate as "1 this currency = ExchangeRate other currency". + * @细节 以“1个当前货币 = 汇率个其他货币”的形式返回汇率。 + */ + bool TryGetExchangeRateTo(const UGIS_CurrencyDefinition* OtherCurrency, double& ExchangeRate) const; + + /** + * Gets the exchange rate to the root currency. + * 获取相对于根货币的汇率。 + * @param AdditionalExchangeRate An external exchange rate for recursive calculations. 用于递归计算的外部汇率。 + * @return The exchange rate to the root currency. 相对于根货币的汇率。 + */ + FGIS_CurrencyExchangeRate GetRootExchangeRate(double AdditionalExchangeRate = 1) const; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyEntry.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyEntry.h new file mode 100644 index 0000000..047eeac --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencyEntry.h @@ -0,0 +1,94 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "UObject/Object.h" +#include "GIS_CurrencyEntry.generated.h" + +class UGIS_CurrencyDefinition; + +/** + * Represents a currency and its associated amount. + * 表示一种货币及其数量。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_CurrencyEntry : public FFastArraySerializerItem +{ + GENERATED_BODY() + + /** + * Default constructor for the currency entry. + * 货币条目的默认构造函数。 + */ + FGIS_CurrencyEntry(); + + /** + * Constructor for the currency entry with specified definition and amount. + * 使用指定货币定义和数量构造货币条目。 + * @param InDefinition The currency definition. 货币定义。 + * @param InAmount The amount of the currency. 货币数量。 + */ + FGIS_CurrencyEntry(const TObjectPtr& InDefinition, float InAmount); + + /** + * Constructor for the currency entry with specified amount and definition (alternative order). + * 使用指定数量和货币定义构造货币条目(参数顺序相反)。 + * @param InAmount The amount of the currency. 货币数量。 + * @param InDefinition The currency definition. 货币定义。 + */ + FGIS_CurrencyEntry(float InAmount, const TObjectPtr& InDefinition); + + /** + * Referenced currency definition. + * 引用的货币定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + TObjectPtr Definition; + + /** + * The amount of the currency. + * 货币的数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + float Amount; + + /** + * The previous amount of the currency (not replicated). + * 货币的前一数量(不复制)。 + */ + UPROPERTY(NotReplicated) + float PrevAmount = 0; + + /** + * Checks if this currency entry is equal to another. + * 检查此货币条目是否与另一个相等。 + * @param Other The other currency entry to compare with. 要比较的另一个货币条目。 + * @return True if the entries are equal, false otherwise. 如果条目相等则返回true,否则返回false。 + */ + bool Equals(const FGIS_CurrencyEntry& Other) const; + + /** + * Converts the currency entry to a string representation. + * 将货币条目转换为字符串表示。 + * @return The string representation of the currency entry. 货币条目的字符串表示。 + */ + FString ToString() const; + + /** + * Equality operator to compare two currency entries. + * 比较两个货币条目的相等性运算符。 + * @param Rhs The right-hand side currency entry to compare with. 要比较的右侧货币条目。 + * @return True if the entries are equal, false otherwise. 如果条目相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_CurrencyEntry& Rhs) const; + + /** + * Inequality operator to compare two currency entries. + * 比较两个货币条目的不等性运算符。 + * @param Rhs The right-hand side currency entry to compare with. 要比较的右侧货币条目。 + * @return True if the entries are not equal, false otherwise. 如果条目不相等则返回true,否则返回false。 + */ + bool operator!=(const FGIS_CurrencyEntry& Rhs) const; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencySystemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencySystemComponent.h new file mode 100644 index 0000000..519594d --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/GIS_CurrencySystemComponent.h @@ -0,0 +1,375 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CurrencyContainer.h" +#include "GIS_CurrencyDefinition.h" +#include "Components/ActorComponent.h" +#include "GIS_CurrencySystemComponent.generated.h" + +class UGIS_InventorySubsystem; + +/** + * Delegate triggered when a currency amount changes. + * 货币数量变化时触发的委托。 + * @param Currency The currency definition that changed. 发生变化的货币定义。 + * @param OldAmount The previous amount of the currency. 之前的货币数量。 + * @param NewAmount The new amount of the currency. 新的货币数量。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FGIS_CurrencyChangedSignature, const UGIS_CurrencyDefinition*, Currency, float, OldAmount, float, NewAmount); + +/** + * Currency system component for managing an actor's currencies. + * 管理演员货币的货币系统组件。 + * @details This component serves as a wrapper for the CurrencyContainer, handling currency-related operations. + * @细节 该组件作为CurrencyContainer的包装器,处理与货币相关的操作。 + */ +UCLASS(ClassGroup=(GIS), BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_CurrencySystemComponent : public UActorComponent +{ + GENERATED_BODY() + + friend FGIS_CurrencyContainer; + +public: + /** + * Constructor for the currency system component. + * 货币系统组件的构造函数。 + */ + UGIS_CurrencySystemComponent(); + + /** + * Gets the currency system component from an actor. + * 从一个演员获取货币系统组件。 + * @param Actor The actor to query for the currency system component. 要查询货币系统组件的演员。 + * @return The currency system component, or nullptr if not found. 货币系统组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|Inventory", Meta = (DefaultToSelf="Actor")) + static UGIS_CurrencySystemComponent* GetCurrencySystemComponent(const AActor* Actor); + + /** + * Gets the properties that should be replicated for this component. + * 获取需要为此组件复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Gets all currency information. + * 获取所有货币信息。 + * @return Array of currency entries containing currency types and amounts. 包含货币类型和数量的货币条目数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual TArray GetAllCurrencies() const; + + /** + * Sets all currency information. + * 设置所有货币信息。 + * @param InCurrencyInfos The array of currency entries to set. 要设置的货币条目数组。 + */ + virtual void SetCurrencies(const TArray& InCurrencyInfos); + + /** + * Clears all currency information. + * 清空所有货币信息。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual void EmptyCurrencies(); + + /** + * Gets information for a specific currency. + * 获取指定货币的信息。 + * @param CurrencyDefinition The currency definition to query. 要查询的货币定义。 + * @param OutCurrencyInfo The currency information (output). 货币信息(输出)。 + * @return True if the currency is found, false otherwise. 如果找到货币则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|CurrencySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool GetCurrency(TSoftObjectPtr CurrencyDefinition, FGIS_CurrencyEntry& OutCurrencyInfo) const; + + /** + * Gets information for multiple specified currencies. + * 获取多个指定货币的信息。 + * @param CurrencyDefinitions The array of currency definitions to query. 要查询的货币定义数组。 + * @param OutCurrencyInfos The array of currency information (output). 货币信息数组(输出)。 + * @return True if all specified currencies are found, false otherwise. 如果找到所有指定货币则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|CurrencySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool GetCurrencies(TArray> CurrencyDefinitions, TArray& OutCurrencyInfos) const; + + /** + * Adds a currency and its amount. + * 添加货币及其数量。 + * @param CurrencyInfo The currency information to add. 要添加的货币信息。 + * @return True if the currency was added successfully, false otherwise. 如果货币添加成功则返回true,否则返回false。 + * @details Adds the specified amount to an existing currency or creates a new entry if the currency doesn't exist. + * @细节 如果当前没有该货币,则以指定数量新增货币;如果已有,则在原始数量上累加。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool AddCurrency(FGIS_CurrencyEntry CurrencyInfo); + + /** + * Removes a currency and its amount. + * 移除指定数量的货币。 + * @param CurrencyInfo The currency information to remove. 要移除的货币信息。 + * @return True if the currency was removed successfully, false if the currency doesn't exist. 如果货币移除成功则返回true,如果货币不存在则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool RemoveCurrency(FGIS_CurrencyEntry CurrencyInfo); + + /** + * Checks if the specified currency amount is available. + * 检查是否拥有指定数量的货币。 + * @param CurrencyInfo The currency information to check. 要检查的货币信息。 + * @return True if the specified amount is available, false otherwise. 如果有足够数量的货币则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool HasCurrency(FGIS_CurrencyEntry CurrencyInfo) const; + + /** + * Checks if multiple specified currency amounts are available. + * 检查是否拥有多个指定数量的货币。 + * @param CurrencyInfos The array of currency information to check. 要检查的货币信息数组。 + * @return True if all specified amounts are available, false otherwise. 如果所有货币数量都满足则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool HasCurrencies(const TArray& CurrencyInfos); + + /** + * Adds multiple currencies. + * 添加多个货币。 + * @param CurrencyInfos The array of currency information to add. 要添加的货币信息数组。 + * @return True if all currencies were added successfully, false otherwise. 如果所有货币都添加成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool AddCurrencies(const TArray& CurrencyInfos); + + /** + * Removes multiple currencies. + * 移除多个货币。 + * @param CurrencyInfos The array of currency information to remove. 要移除的货币信息数组。 + * @return True if all currencies were removed successfully, false otherwise. 如果所有货币都移除成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|CurrencySystem") + virtual bool RemoveCurrencies(const TArray& CurrencyInfos); + + /** + * Called when the actor begins play. + * 演员开始播放时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the actor ends play. + * 演员结束播放时调用。 + * @param EndPlayReason The reason for ending play. 结束播放的原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Event triggered when a currency amount changes. + * 货币数量变化时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_CurrencyChangedSignature OnCurrencyChangedEvent; + +#pragma region Static Calculations +#if 0 + /** + * Finds the index of a currency in a list. + * 查找包含指定货币的索引。 + * @param Currency The currency definition to find. 要查找的货币定义。 + * @param List The list of currency entries to search. 要搜索的货币条目列表。 + * @return The index of the currency, or -1 if not found. 货币的索引,如果未找到则返回-1。 + */ + static int32 FindIndexWithCurrency(const UGIS_CurrencyDefinition* Currency, const TArray& List); + + /** + * Finds or creates an index for a currency in a list. + * 查找或创建包含指定货币的索引。 + * @param Currency The currency definition to find or create. 要查找或创建的货币定义。 + * @param Result The list of currency entries (modified). 货币条目列表(可修改)。 + * @return The index of the currency. 货币的索引。 + */ + static int32 FindOrCreateCurrencyIndex(const UGIS_CurrencyDefinition* Currency, TArray& Result); + + /** + * Performs discrete addition of two currency lists. + * 执行两个货币列表的离散加法。 + * @param Lhs The first currency list. 第一个货币列表。 + * @param Rhs The second currency list. 第二个货币列表。 + * @return The combined currency list. 合并后的货币列表。 + */ + static TArray DiscreteAddition(const TArray& Lhs, const TArray& Rhs); + + /** + * Adds a single currency and amount to a currency list. + * 将单一货币和数量添加到货币列表。 + * @param CurrencyAmounts The currency list to modify. 要修改的货币列表。 + * @param Currency The currency definition to add. 要添加的货币定义。 + * @param Amount The amount to add. 要添加的数量。 + * @return The updated currency list. 更新后的货币列表。 + */ + static TArray DiscreteAddition(const TArray& CurrencyAmounts, + const UGIS_CurrencyDefinition* Currency, float Amount); + + /** + * Sets the fractional part of a currency to its maximum value. + * 将货币的小数部分设置为最大值。 + * @param CurrencyAmounts The currency list to modify. 要修改的货币列表。 + * @param Currency The currency definition to adjust. 要调整的货币定义。 + * @return The updated currency list. 更新后的货币列表。 + */ + static TArray SetAllFractionToMax(const TArray& CurrencyAmounts, const UGIS_CurrencyDefinition* Currency); + + /** + * Converts an amount to discrete currency units. + * 将数量转换为离散的货币单位。 + * @param Currency The currency definition to convert. 要转换的货币定义。 + * @param Amount The amount to convert. 要转换的数量。 + * @return The discrete currency entries. 离散的货币条目。 + */ + static TArray ConvertToDiscrete(const UGIS_CurrencyDefinition* Currency, double Amount); + + /** + * Converts overflow amounts, ignoring fractional parts. + * 转换溢出金额,忽略小数部分。 + * @param Currency The currency definition to convert. 要转换的货币定义。 + * @param Amount The amount to convert. 要转换的数量。 + * @return The overflow currency entries. 溢出的货币条目。 + */ + static TArray ConvertOverflow(const UGIS_CurrencyDefinition* Currency, double Amount); + + /** + * Converts the fractional part of an amount to currency units. + * 将金额的小数部分转换为货币单位。 + * @param Currency The currency definition to convert. 要转换的货币定义。 + * @param Amount The amount to convert. 要转换的数量。 + * @return The fractional currency entries. 小数部分的货币条目。 + */ + static TArray ConvertFraction(const UGIS_CurrencyDefinition* Currency, double Amount); + + /** + * Gets the maximum amount for a currency in discrete units. + * 获取货币的最大金额(以离散单位计)。 + * @param Currency The currency definition to query. 要查询的货币定义。 + * @return The maximum currency entries. 最大货币条目。 + */ + static TArray MaxedOutAmount(const UGIS_CurrencyDefinition* Currency); +#endif +#pragma endregion + +protected: + /** + * Called when a currency entry is added. + * 货币条目添加时调用。 + * @param Entry The added currency entry. 添加的货币条目。 + * @param Idx The index of the added entry. 添加条目的索引。 + */ + virtual void OnCurrencyEntryAdded(const FGIS_CurrencyEntry& Entry, int32 Idx); + + /** + * Called when a currency entry is removed. + * 货币条目移除时调用。 + * @param Entry The removed currency entry. 移除的货币条目。 + * @param Idx The index of the removed entry. 移除条目的索引。 + */ + virtual void OnCurrencyEntryRemoved(const FGIS_CurrencyEntry& Entry, int32 Idx); + + /** + * Called when a currency entry is updated. + * 货币条目更新时调用。 + * @param Entry The updated currency entry. 更新的货币条目。 + * @param Idx The index of the updated entry. 更新条目的索引。 + * @param OldAmount The previous amount of the currency. 之前的货币数量。 + * @param NewAmount The new amount of the currency. 新的货币数量。 + */ + virtual void OnCurrencyEntryUpdated(const FGIS_CurrencyEntry& Entry, int32 Idx, float OldAmount, float NewAmount); + + /** + * Called when a currency amount changes. + * 货币数量变化时调用。 + * @param Tag The currency definition that changed. 发生变化的货币定义。 + * @param OldValue The previous amount of the currency. 之前的货币数量。 + * @param NewValue The new amount of the currency. 新的货币数量。 + */ + virtual void OnCurrencyChanged(TObjectPtr Tag, float OldValue, float NewValue); + + /** + * Adds initial currencies to the component. + * 向组件添加初始货币。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|CurrencySystem") + void AddInitialCurrencies(); + + /** + * Internal function to get information for a specific currency. + * 获取指定货币信息的内部函数。 + * @param CurrencyTag The currency definition to query. 要查询的货币定义。 + * @param OutCurrencyInfo The currency information (output). 货币信息(输出)。 + * @return True if the currency is found, false otherwise. 如果找到货币则返回true,否则返回false。 + */ + virtual bool GetCurrencyInternal(const TObjectPtr& CurrencyTag, FGIS_CurrencyEntry& OutCurrencyInfo) const; + + /** + * Internal function to get information for multiple currencies. + * 获取多个货币信息的内部函数。 + * @param Currencies The array of currency definitions to query. 要查询的货币定义数组。 + * @param OutCurrencies The array of currency information (output). 货币信息数组(输出)。 + * @return True if all currencies are found, false otherwise. 如果找到所有货币则返回true,否则返回false。 + */ + virtual bool GetCurrenciesInternal(const TArray>& Currencies, TArray& OutCurrencies) const; + + /** + * Internal function to add a currency. + * 添加货币的内部函数。 + * @param CurrencyInfo The currency information to add. 要添加的货币信息。 + * @param bNotify Whether to trigger notifications for the addition. 是否触发添加通知。 + * @return True if the currency was added successfully, false otherwise. 如果货币添加成功则返回true,否则返回false。 + */ + virtual bool AddCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify = true); + + /** + * Internal function to remove a currency. + * 移除货币的内部函数。 + * @param CurrencyInfo The currency information to remove. 要移除的货币信息。 + * @param bNotify Whether to trigger notifications for the removal. 是否触发移除通知。 + * @return True if the currency was removed successfully, false otherwise. 如果货币移除成功则返回true,否则返回false。 + */ + virtual bool RemoveCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify = true); + + /** + * Internal function to check if a currency amount is available. + * 检查货币数量是否可用的内部函数。 + * @param CurrencyInfo The currency information to check. 要检查的货币信息。 + * @return True if the specified amount is available, false otherwise. 如果有足够数量的货币则返回true,否则返回false。 + */ + virtual bool HasCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo) const; + + /** + * Default currencies to initialize the component with. + * 初始化组件的默认货币。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Currency") + TArray DefaultCurrencies; + + /** + * Container for currency entries. + * 货币条目的容器。 + */ + UPROPERTY(VisibleAnywhere, Replicated, Category="Currency", meta=(ShowOnlyInnerProperties)) + FGIS_CurrencyContainer Container; + + /** + * Local cache mapping currency definitions to their amounts. + * 货币定义到其数量的本地缓存映射。 + */ + UPROPERTY() + TMap, float> CurrencyMap; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopCondition.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopCondition.h new file mode 100644 index 0000000..b95e63a --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopCondition.h @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Items/GIS_ItemInfo.h" +#include "UObject/Interface.h" +#include "GIS_ShopCondition.generated.h" + +class UGIS_CurrencySystemComponent; +class UGIS_ShopSystemComponent; +class UGIS_InventorySystemComponent; + +// This class does not need to be modified. +/** + * Interface for defining custom buy condition checks. + * 定义自定义购买条件检查的接口。 + */ +UINTERFACE() +class UGIS_ShopBuyCondition : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for actors or components to implement custom buy condition logic. + * 供Actor或组件实现自定义购买条件逻辑的接口。 + * @details Allows checking if an item can be bought based on shop, inventory, and currency conditions. + * @细节 允许根据商店、库存和货币条件检查道具是否可以购买。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_ShopBuyCondition +{ + GENERATED_BODY() + +public: + /** + * Checks if an item can be bought. + * 检查道具是否可以购买。 + * @param Shop The shop system component. 商店系统组件。 + * @param BuyerInventory The inventory of the buyer. 购买者的库存。 + * @param CurrencySystem The currency system component. 货币系统组件。 + * @param ItemInfo Information about the item to buy. 要购买的道具信息。 + * @return True if the item can be bought, false otherwise. 如果道具可以购买则返回true,否则返回false。 + */ + virtual bool CanBuy(const UGIS_ShopSystemComponent* Shop, UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, + FGIS_ItemInfo ItemInfo) = 0; +}; + +// This class does not need to be modified. +/** + * Interface for defining custom sell condition checks. + * 定义自定义出售条件检查的接口。 + */ +UINTERFACE() +class UGIS_ShopSellCondition : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for actors or components to implement custom sell condition logic. + * 供Actor或组件实现自定义出售条件逻辑的接口。 + * @details Allows checking if an item can be sold based on shop, inventory, and currency conditions. + * @细节 允许根据商店、库存和货币条件检查道具是否可以出售。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_ShopSellCondition +{ + GENERATED_BODY() + +public: + /** + * Checks if an item can be sold. + * 检查道具是否可以出售。 + * @param Shop The shop system component. 商店系统组件。 + * @param SellerInventory The inventory of the seller. 出售者的库存。 + * @param CurrencySystem The currency system component. 货币系统组件。 + * @param ItemInfo Information about the item to sell. 要出售的道具信息。 + * @return True if the item can be sold, false otherwise. 如果道具可以出售则返回true,否则返回false。 + */ + virtual bool CanSell(const UGIS_ShopSystemComponent* Shop, UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, + FGIS_ItemInfo ItemInfo) = 0; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopSystemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopSystemComponent.h new file mode 100644 index 0000000..96a342c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Exchange/Shops/GIS_ShopSystemComponent.h @@ -0,0 +1,254 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CurrencyEntry.h" +#include "GIS_InventoryTags.h" +#include "Items/GIS_ItemInfo.h" +#include "Components/ActorComponent.h" +#include "GIS_ShopSystemComponent.generated.h" + +class UGIS_CurrencySystemComponent; +class IGIS_ShopSellCondition; +class IGIS_ShopBuyCondition; + +/** + * Component responsible for managing shop functionality, including buying and selling items. + * 负责管理商店功能的组件,包括购买和出售道具。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_ShopSystemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the shop system component. + * 商店系统组件的构造函数。 + */ + UGIS_ShopSystemComponent(); + + /** + * Gets the shop system component from an actor. + * 从一个演员获取商店系统组件。 + * @param Actor The actor to query for the shop system component. 要查询商店系统组件的演员。 + * @return The shop system component, or nullptr if not found. 商店系统组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ShopSystem", Meta = (DefaultToSelf="Actor")) + static UGIS_ShopSystemComponent* GetShopSystemComponent(const AActor* Actor); + + /** + * Gets the shop's inventory. + * 获取商店的库存。 + * @return The shop's inventory component, or nullptr if not set. 商店的库存组件,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ShopSystem") + virtual UGIS_InventorySystemComponent* GetInventory() const; + + /** + * Attempts to buy an item from the shop. + * 尝试从商店购买道具。 + * @param BuyerInventory The buyer's inventory component. 买家的库存组件。 + * @param CurrencySystem The buyer's currency system component (to deduct currency). 买家的货币系统组件(用于扣除货币)。 + * @param ItemInfo The item information to buy. 要购买的道具信息。 + * @return True if the purchase was successful, false otherwise. 如果购买成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool BuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo); + + /** + * Attempts to sell an item to the shop. + * 尝试向商店出售道具。 + * @param SellerInventory The seller's inventory component. 卖家的库存组件。 + * @param CurrencySystem The seller's currency system component (to add currency). 卖家的货币系统组件(用于添加货币)。 + * @param ItemInfo The item information to sell. 要出售的道具信息。 + * @return True if the sale was successful, false otherwise. 如果出售成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool SellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo); + + /** + * Checks if a buyer can purchase an item from the shop. + * 检查买家是否可以从商店购买道具。 + * @param BuyerInventory The buyer's inventory component. 买家的库存组件。 + * @param CurrencySystem The buyer's currency system component (to check currency availability). 买家的货币系统组件(用于检查货币是否足够)。 + * @param ItemInfo The item information to buy. 要购买的道具信息。 + * @return True if the buyer can purchase the item, false otherwise. 如果买家可以购买道具则返回true,否则返回false。 + * @details Used for custom checks such as content locking or other game mechanics. 可用于自定义检查,如内容锁定或其他游戏机制。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool CanBuyerBuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const; + + /** + * Checks if a seller can sell an item to the shop. + * 检查卖家是否可以向商店出售道具。 + * @param SellerInventory The seller's inventory component. 卖家的库存组件。 + * @param CurrencySystem The seller's currency system component. 卖家的货币系统组件。 + * @param ItemInfo The item information to sell. 要出售的道具信息。 + * @return True if the seller can sell the item, false otherwise. 如果卖家可以出售道具则返回true,否则返回false。 + * @details Used for custom checks such as content locking or other game mechanics. 可用于自定义检查,如内容锁定或其他游戏机制。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool CanSellerSellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const; + + /** + * Checks if the specified item is available for purchase from the shop. + * 检查指定道具是否可从商店购买。 + * @param ItemInfo The item information to check. 要检查的道具信息。 + * @return True if the item is buyable, false otherwise. 如果道具可购买则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool IsItemBuyable(const FGIS_ItemInfo& ItemInfo) const; + + /** + * Checks if the shop can buy the specified item from a seller. + * 检查商店是否可以从卖家购买指定道具。 + * @param ItemInfo The item information to check. 要检查的道具信息。 + * @return True if the item is sellable, false otherwise. 如果道具可出售则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|ShopSystem") + virtual bool IsItemSellable(const FGIS_ItemInfo& ItemInfo) const; + + /** + * Gets the buy price modifier for the buyer. + * 获取买家的购买价格浮动。 + * @param BuyerInventory The buyer's inventory component. 买家的库存组件。 + * @return The buy price modifier. 购买价格浮动值。 + * @details Can be overridden to implement a dynamic pricing system based on game mechanics. 可覆写以基于游戏机制实现动态价格系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|ShopSystem") + float GetBuyModifierForBuyer(UGIS_InventorySystemComponent* BuyerInventory) const; + + /** + * Gets the sell price modifier for the seller. + * 获取卖家的出售价格浮动。 + * @param SellerInventory The seller's inventory component. 卖家的库存组件。 + * @return The sell price modifier. 出售价格浮动值。 + * @details Can be overridden to implement a dynamic pricing system based on game mechanics, such as supply shortages. 可覆写以基于游戏机制(如缺货)实现动态价格系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|ShopSystem") + float GetSellModifierForSeller(UGIS_InventorySystemComponent* SellerInventory) const; + + /** + * Gets the buy value (currency cost) of an item for the buyer. + * 获取买家购买道具的货币价格。 + * @param Buyer The buyer's inventory component. 买家的库存组件。 + * @param ItemInfo The item information to buy. 要购买的道具信息。 + * @param BuyValue The required currency for the purchase (output). 购买所需的货币(输出)。 + * @return True if the buyer can afford the item, false otherwise. 如果买家买得起则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, BlueprintNativeEvent, Category="GIS|ShopSystem", meta=(ExpandBoolAsExecs="ReturnValue")) + bool TryGetBuyValueForBuyer(UGIS_InventorySystemComponent* Buyer, const FGIS_ItemInfo& ItemInfo, TArray& BuyValue) const; + + /** + * Gets the sell value (currency gained) of an item for the seller. + * 获取卖家出售道具的货币价格。 + * @param Seller The seller's inventory component. 卖家的库存组件。 + * @param ItemInfo The item information to sell. 要出售的道具信息。 + * @param SellValue The currency gained from the sale (output). 出售获得的货币(输出)。 + * @return True if the currency value is valid, false otherwise. 如果货币价格有效则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, BlueprintNativeEvent, Category="GIS|ShopSystem", meta=(ExpandBoolAsExecs="ReturnValue")) + bool TryGetSellValueForSeller(UGIS_InventorySystemComponent* Seller, const FGIS_ItemInfo& ItemInfo, TArray& SellValue) const; + + /** + * Called when the actor begins play. + * 演员开始播放时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the actor ends play. + * 演员结束播放时调用。 + * @param EndPlayReason The reason for ending play. 结束播放的原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +protected: + /** + * Internal check for whether a seller can sell an item to the shop. + * 检查卖家是否可以向商店出售道具的内部函数。 + * @param SellerInventory The seller's inventory component. 卖家的库存组件。 + * @param CurrencySystem The seller's currency system component. 卖家的货币系统组件。 + * @param ItemInfo The item information to sell. 要出售的道具信息。 + * @return True if the seller can sell the item, false otherwise. 如果卖家可以出售道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|ShopSystem") + bool CanSellerSellItemInternal(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const; + + /** + * Internal check for whether a buyer can purchase an item from the shop. + * 检查买家是否可以从商店购买道具的内部函数。 + * @param BuyerInventory The buyer's inventory component. 买家的库存组件。 + * @param CurrencySystem The buyer's currency system component. 买家的货币系统组件。 + * @param ItemInfo The item information to buy. 要购买的道具信息。 + * @return True if the buyer can purchase the item, false otherwise. 如果买家可以购买道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|ShopSystem") + bool CanBuyerBuyItemInternal(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const; + + /** + * Internal function to handle selling an item to the shop. + * 处理向商店出售道具的内部函数。 + * @param SellerInventory The seller's inventory component. 卖家的库存组件。 + * @param CurrencySystem The seller's currency system component. 卖家的货币系统组件。 + * @param ItemInfo The item information to sell. 要出售的道具信息。 + * @return True if the sale was successful, false otherwise. 如果出售成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|ShopSystem") + bool SellItemInternal(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo); + + /** + * Internal function to handle buying an item from the shop. + * 处理从商店购买道具的内部函数。 + * @param BuyerInventory The buyer's inventory component. 买家的库存组件。 + * @param CurrencySystem The buyer's currency system component. 买家的货币系统组件。 + * @param ItemInfo The item information to buy. 要购买的道具信息。 + * @return True if the purchase was successful, false otherwise. 如果购买成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|ShopSystem") + bool BuyItemInternal(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo); + + /** + * The target item collection to add items to when purchased. + * 购买时道具添加到的目标道具集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Shop", meta=(Categories="GIS.Collection")) + FGameplayTag TargetItemCollectionToAddOnBuy = GIS_CollectionTags::Main; + + /** + * Controls the buy price modifier for this shop. + * 控制此商店的购买价格浮动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Shop", meta=(ClampMin=0)) + float BuyPriceModifier = 0; + + /** + * Controls the sell price modifier for this shop. + * 控制此商店的出售价格浮动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Shop", meta=(ClampMin=0)) + float SellPriceModifer = 0; + + /** + * The inventory component associated with the shop. + * 与商店关联的库存组件。 + */ + UPROPERTY() + TObjectPtr OwningInventory; + + /** + * List of conditions for buying items from the shop. + * 商店购买道具的条件列表。 + */ + UPROPERTY() + TArray> BuyConditions; + + /** + * List of conditions for selling items to the shop. + * 向商店出售道具的条件列表。 + */ + UPROPERTY() + TArray> SellConditions; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFactory.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFactory.h new file mode 100644 index 0000000..0ea8095 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFactory.h @@ -0,0 +1,149 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_SerializationStructLibrary.h" +#include "UObject/Object.h" +#include "GIS_InventoryFactory.generated.h" + +class UGIS_ItemDefinition; +class UGIS_InventorySystemComponent; +class UGIS_ItemInstance; + +/** + * Inventory Factory class for managing low level operation in GIS. + * 库存工厂类用于管理GIS中的底层操作。 + * @details Extend this class to have full control over how serialization works. + * @细节 拓展此类以完全控制序列化的运作。 + */ +UCLASS(BlueprintType, Blueprintable) +class GENERICINVENTORYSYSTEM_API UGIS_InventoryFactory : public UObject +{ + GENERATED_BODY() + +public: + UGIS_InventoryFactory(); + + /** + * Creates a new item instance for an actor. + * 为演员创建新的道具实例。 + * @param Owner The actor that will own this item (used for network replication). 将拥有此道具的演员(用于网络复制)。 + * @param ItemDefinition The definition of the item to create. 要创建的道具定义。 + * @return The newly created item instance, or nullptr if creation failed. 新创建的道具实例,如果创建失败则返回nullptr。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + UGIS_ItemInstance* CreateItem(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition); + virtual UGIS_ItemInstance* CreateItem_Implementation(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition); + + /** + * Duplicates an existing item to create a new one. + * 复制现有道具以创建新道具。 + * @param Owner The actor that will own the new item. 将拥有新道具的演员。 + * @param SrcItem The item to duplicate from. 要复制的原始道具。 + * @param bGenerateNewId If true, will generate new id while copying. 如果为真,会在复制时,生成新的id + * @return The newly created item instance, or nullptr if duplication failed. 新创建的道具实例,如果复制失败则返回nullptr。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + UGIS_ItemInstance* DuplicateItem(AActor* Owner, UGIS_ItemInstance* SrcItem, bool bGenerateNewId = true); + virtual UGIS_ItemInstance* DuplicateItem_Implementation(AActor* Owner, UGIS_ItemInstance* SrcItem, bool bGenerateNewId = true); + + // /** + // * Create and restore an item instance from item record. + // * 从item记录录中创建并恢复道具实例。 + // * @param Owner The actor that will own the new item. 将拥有新道具的演员。 + // * @param InRecord The record of item. 道具记录 + // * @return The newly created item instance, or nullptr if restoring failed. 新创建的道具实例,如果恢复失败则返回nullptr。 + // */ + + + /** + * Serializes an item instance into a record. + * 将道具实例序列化为记录。 + * @param Item The item instance to serialize. 要序列化的道具实例。 + * @param Record The resulting item record (output). 输出的道具记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + bool SerializeItem(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record); + + /** + * Deserializes an item from an item record. + * 从道具记录反序列化道具。 + * @param Owner The actor owning the item instance (generally inventory system component's owner). 拥有此道具实例的演员,通常是库存系统组件的Owner。 + * @param Record The item record to deserialize. 要反序列化的道具记录。 + * @return The deserialized item instance, or nullptr if deserialization failed. 反序列化的道具实例,如果失败则返回nullptr。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + UGIS_ItemInstance* DeserializeItem(AActor* Owner, const FGIS_ItemRecord& Record); + + /** + * Creates a new collection instance for an actor. + * 为演员创建新的集合实例。 + * @param Owner The actor that will own this collection (used for network replication). 将拥有此集合的演员(用于网络复制)。 + * @param Definition The definition of the collection to create. 要创建的集合定义。 + * @return The newly created collection instance, or nullptr if creation failed. 新创建的集合实例,如果创建失败则返回nullptr。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + UGIS_ItemCollection* CreateCollection(AActor* Owner, const UGIS_ItemCollectionDefinition* Definition); + + /** + * Serializes an item collection into a record. + * 将道具集合序列化为记录。 + * @param Collection The item collection to serialize. 要序列化的道具集合。 + * @param Record The resulting collection record (output). 输出的集合记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + bool SerializeCollection(UGIS_ItemCollection* Collection, FGIS_CollectionRecord& Record); + + /** + * Deserializes a collection from a collection record. + * 从集合记录反序列化集合。 + * @param InventorySystem The inventory system component to associate with the collection. 与集合关联的库存系统组件。 + * @param Record The collection record to deserialize. 要反序列化的集合记录。 + * @param ItemsMap A map of item IDs to item instances (output). 道具ID到道具实例的映射(输出)。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + void DeserializeCollection(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap& ItemsMap); + + /** + * Serializes an entire inventory into a record. + * 将整个库存序列化为记录。 + * @param InventorySystem The inventory system component to serialize. 要序列化的库存系统组件。 + * @param Record The resulting inventory record. 输出的库存记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + bool SerializeInventory(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record); + + /** + * Deserializes an inventory from an inventory record. + * 从库存记录反序列化库存。 + * @param InventorySystem The inventory system component to populate. 要填充的库存系统组件。 + * @param InRecord The inventory record to deserialize. 要反序列化的库存记录。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GIS|Factory") + void DeserializeInventory(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& InRecord); + +protected: + // virtual TArray FilterSerializableFragmentStates(const UGIS_ItemInstance* ItemInstance); + // virtual TArray FilterCompatibleFragmentStateRecords(const UGIS_ItemDefinition* ItemDefinition, const FGIS_ItemRecord& Record); + + /** + * The default class used to construct item instances. + * 用于构造道具实例的默认类。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Factory", NoClear) + TSoftClassPtr DefaultItemInstanceClass; + +#if WITH_EDITOR + /** + * Validates the data for this factory in the editor. + * 在编辑器中验证工厂的数据。 + * @param Context The data validation context. 数据验证上下文。 + * @return The result of the data validation. 数据验证的结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFunctionLibrary.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFunctionLibrary.h new file mode 100644 index 0000000..ae6a06f --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryFunctionLibrary.h @@ -0,0 +1,82 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_CurrencyEntry.h" +#include "Items/GIS_ItemInfo.h" +#include "Items/GIS_ItemStack.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GIS_InventoryFunctionLibrary.generated.h" + +/** + * Blueprint function library for inventory-related utility functions. + * 用于库存相关实用功能的蓝图函数库。 + */ +UCLASS() +class GENERICINVENTORYSYSTEM_API UGIS_InventoryFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Multiplies the amounts of item definitions by a multiplier. + * 将道具定义的数量乘以一个倍数。 + * @param ItemAmounts The array of item definitions with amounts. 包含数量的道具定义数组。 + * @param Multiplier The multiplier to apply (default is 1). 要应用的倍数(默认为1)。 + * @return The modified array of item definitions with multiplied amounts. 修改后的包含乘以倍数的道具定义数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + static TArray MultiplyItemAmounts(const TArray& ItemAmounts, int32 Multiplier = 1); + + /** + * Multiplies the amounts of currencies by a multiplier. + * 将货币数量乘以一个倍数。 + * @param Currencies The array of currency entries. 货币条目数组。 + * @param Multiplier The multiplier to apply (default is 1). 要应用的倍数(默认为1)。 + * @return The modified array of currency entries with multiplied amounts. 修改后的包含乘以倍数的货币条目数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + static TArray MultiplyCurrencies(const TArray& Currencies, float Multiplier = 1.0f); + + /** + * Filters item infos based on a gameplay tag query. + * 根据游戏标签查询过滤道具信息。 + * @param ItemInfos The array of item infos to filter. 要过滤的道具信息数组。 + * @param Query The gameplay tag query to apply. 要应用的游戏标签查询。 + * @return The filtered array of item infos. 过滤后的道具信息数组。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + static TArray FilterItemInfosByTagQuery(const TArray& ItemInfos, const FGameplayTagQuery& Query); + + /** + * Filters item stacks based on a gameplay tag query. + * 根据游戏标签查询过滤道具堆栈。 + * @param ItemStacks The item stacks to filter. 要过滤的道具堆栈。 + * @param TagQuery The tag query for item tags. 用于道具标签的标签查询。 + * @return The filtered array of item stacks matching the tag query. 匹配标签查询的过滤后的道具堆栈数组。 + */ + // UFUNCTION(BlueprintPure, Category = "GIS") + static TArray FilterItemStacksByTagQuery(const TArray& ItemStacks, const FGameplayTagQuery& TagQuery); + + /** + * Filters item stacks based on an item definition. + * 根据道具定义过滤道具堆栈。 + * @param ItemStacks The item stacks to filter. 要过滤的道具堆栈。 + * @param Definition The item definition to filter by. 用于过滤的道具定义。 + * @return The filtered array of item stacks matching the definition. 匹配定义的过滤后的道具堆栈数组。 + */ + // UFUNCTION(BlueprintPure, Category = "GIS") + static TArray FilterItemStacksByDefinition(const TArray& ItemStacks, const UGIS_ItemDefinition* Definition); + + /** + * Filters item stacks based on collection tags. + * 根据集合标签过滤道具堆栈。 + * @param ItemStacks The item stacks to filter. 要过滤的道具堆栈。 + * @param CollectionTags The collection tags to search. 要搜索的集合标签。 + * @return The filtered array of item stacks matching the collection tags. 匹配集合标签的过滤后的道具堆栈数组。 + */ + // UFUNCTION(BlueprintPure, Category = "GIS") + static TArray FilterItemStacksByCollectionTags(const TArray& ItemStacks, const FGameplayTagContainer& CollectionTags); +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryMeesages.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryMeesages.h new file mode 100644 index 0000000..445eb39 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryMeesages.h @@ -0,0 +1,236 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Items/GIS_ItemInfo.h" +#include "NativeGameplayTags.h" +#include "GIS_InventoryMeesages.generated.h" + +class UGIS_InventorySystemComponent; +class UGIS_ItemCollection; +class UGIS_ItemInstance; + +/** + * Message struct for general inventory updates. + * 通用库存更新的消息结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryUpdateMessage +{ + GENERATED_BODY() + + /** + * The inventory system component associated with the update. + * 与更新关联的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; +}; + +/** + * Message struct for item stack additions or removals in the inventory. + * 库存中道具堆栈添加或移除的消息结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryStackUpdateMessage +{ + GENERATED_BODY() + + /** + * The inventory system component associated with the stack update. + * 与堆栈更新关联的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; + + /** + * The type of change to the item stack (e.g., added, removed, changed). + * 道具堆栈的变更类型(例如,添加、移除、变更)。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + EGIS_ItemStackChangeType ChangeType{EGIS_ItemStackChangeType::Changed}; + + /** + * The changed item instance. + * 变更的道具实例。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Instance = nullptr; + + /** + * The unique ID of the changed stack. + * 变更堆栈的唯一ID。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGuid StackId; + + /** + * The unique ID of the collection containing the changed stack. + * 包含变更堆栈的集合的唯一ID。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS") + FGuid CollectionId; + + /** + * The new amount of the item instance in the stack. + * 道具实例在堆栈中的当前数量。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + int32 NewCount = 0; + + /** + * The change in amount of the item instance in the stack (difference from previous amount). + * 道具实例在堆栈中的数量变化(与之前数量的差值)。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + int32 Delta = 0; +}; + +/** + * Message struct for adding item info to the inventory. + * 将道具信息添加到库存的消息结构体。 + * @details The ItemInfo is the source of the amount being added, and the ItemStack is the result. + * @细节 ItemInfo是添加数量的来源,ItemStack是添加的结果。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryAddItemInfoMessage +{ + GENERATED_BODY() + + /** + * The inventory system component associated with the item addition. + * 与道具添加关联的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; + + /** + * The item info being added. + * 正在添加的道具信息。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo ItemInfo; + + /** + * The unique ID of the stack where the item is added. + * 添加道具的堆栈的唯一ID。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGuid StackId; + + /** + * The unique ID of the collection where the item is added. + * 添加道具的集合的唯一ID。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGuid CollectionId; + + /** + * The gameplay tag of the collection where the item is added. + * 添加道具的集合的游戏标签。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGameplayTag CollectionTag; +}; + +/** + * Message struct for when item info addition is rejected by the inventory. + * 道具信息添加被库存拒绝时的消息结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryAddItemInfoRejectedMessage +{ + GENERATED_BODY() + + /** + * The inventory system component that rejected the item info. + * 拒绝道具信息的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; + + /** + * The collection that rejected the item info. + * 拒绝道具信息的集合。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Collection; + + /** + * The original item info that was attempted to be added. + * 尝试添加的原始道具信息。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo OriginalItemInfo; + + /** + * The portion of the item info that was successfully added. + * 成功添加的道具信息部分。 + * @attention May be empty if no items were added. 如果没有添加任何道具,可能为空。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo ItemInfoAdded; + + /** + * The portion of the item info that was rejected due to reasons like overflow or collection restrictions. + * 由于溢出或集合限制等原因被拒绝的道具信息部分。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo RejectedItemInfo; + + /** + * The item info returned to the incoming collection if the "return overflow" option is enabled. + * 如果启用了“返回溢出”选项,则返回到输入集合的道具信息。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo ReturnedItemInfo; +}; + +/** + * Message struct for removing item info from the inventory. + * 从库存中移除道具信息的消息结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryRemoveItemInfoMessage +{ + GENERATED_BODY() + + /** + * The inventory system component associated with the item removal. + * 与道具移除关联的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; + + /** + * The item info being removed. + * 正在移除的道具信息。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + FGIS_ItemInfo ItemInfo; +}; + +/** + * Message struct for collection updates in the inventory. + * 库存中集合更新的消息结构体。 + */ +USTRUCT(BlueprintType) +struct FGIS_InventoryCollectionUpdateMessage +{ + GENERATED_BODY() + + /** + * The inventory system component associated with the collection update. + * 与集合更新关联的库存系统组件。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Inventory; + + /** + * The collection that was updated. + * 已更新的集合。 + */ + UPROPERTY(BlueprintReadOnly, Category="GIS") + TObjectPtr Collection; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySubsystem.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySubsystem.h new file mode 100644 index 0000000..1b5250c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySubsystem.h @@ -0,0 +1,148 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_SerializationStructLibrary.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "GIS_InventorySubsystem.generated.h" + +class UGIS_InventorySystemComponent; +class UDataTable; +class UGIS_InventoryFactory; + + +/** + * Subsystem for managing inventory-related operations, such as item creation. + * 用于管理库存相关操作(如道具创建)的子系统。 + */ +UCLASS(Config=Game, DefaultConfig) +class GENERICINVENTORYSYSTEM_API UGIS_InventorySubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + /** + * Gets the inventory subsystem instance from a world context object. + * 从世界上下文对象获取库存子系统实例。 + * @param WorldContextObject The object providing the world context. 提供世界上下文的对象。 + * @return The inventory subsystem instance, or nullptr if not found. 库存子系统实例,如果未找到则返回nullptr。 + */ + static UGIS_InventorySubsystem* Get(const UObject* WorldContextObject); + + /** + * Initializes the subsystem. + * 初始化子系统。 + * @param Collection The subsystem collection. 子系统集合。 + */ + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + /** + * Deinitializes the subsystem. + * 反初始化子系统。 + */ + virtual void Deinitialize() override; + + /** + * Creates an item instance from a definition. + * 从道具定义创建道具实例。 + * @param Owner The actor that will own this item (required for network replication). 将拥有此道具的演员(网络复制所需)。 + * @param ItemDefinition The item definition to create the instance from. 用于创建实例的道具定义。 + * @return The newly created item instance, or nullptr if creation failed. 新创建的道具实例,如果创建失败则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + UGIS_ItemInstance* CreateItem(AActor* Owner, TSoftObjectPtr ItemDefinition); + + /** + * Creates an item instance from a definition (C++ version). + * 从道具定义创建道具实例(C++版本)。 + * @param Owner The actor that will own this item. 将拥有此道具的演员。 + * @param ItemDefinition The item definition to create the instance from. 用于创建实例的道具定义。 + * @return The newly created item instance, or nullptr if creation failed. 新创建的道具实例,如果创建失败则返回nullptr。 + */ + UGIS_ItemInstance* CreateItem(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition); + + /** + * Creates a new item by duplicating an existing item. + * 通过复制现有道具创建新道具。 + * @param Owner The actor that will own the duplicated item. 将拥有复制道具的演员。 + * @param FromItem The original item to duplicate. 要复制的原始道具。 + * @attention The duplicated item will have a new unique ID. 复制的道具将具有新的唯一ID。 + * @return The duplicated item instance, or nullptr if duplication failed. 复制的道具实例,如果复制失败则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + UGIS_ItemInstance* DuplicateItem(AActor* Owner, UGIS_ItemInstance* FromItem, bool bGenerateNewId = true); + + /** + * Serializes an item instance into a record. + * 将道具实例序列化为记录。 + * @param Item The item instance to serialize. 要序列化的道具实例。 + * @param Record The resulting item record (output). 输出的道具记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + bool SerializeItem(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record); + + /** + * Deserializes an item from an item record. + * 从道具记录反序列化道具。 + * @param Owner The actor owning the item instance (generally inventory system component's owner). 拥有此道具实例的演员,通常是库存系统组件的Owner。 + * @param Record The item record to deserialize. 要反序列化的道具记录。 + * @return The deserialized item instance, or nullptr if deserialization failed. 反序列化的道具实例,如果失败则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + UGIS_ItemInstance* DeserializeItem(AActor* Owner, const FGIS_ItemRecord& Record); + + /** + * Serializes an item collection into a record. + * 将道具集合序列化为记录。 + * @param ItemCollection The item collection to serialize. 要序列化的道具集合。 + * @param Record The resulting collection record (output). 输出的集合记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + bool SerializeCollection(UGIS_ItemCollection* ItemCollection, FGIS_CollectionRecord& Record); + + /** + * Deserializes a collection from a collection record. + * 从集合记录反序列化集合。 + * @param InventorySystem The inventory system component to associate with the collection. 与集合关联的库存系统组件。 + * @param Record The collection record to deserialize. 要反序列化的集合记录。 + * @param ItemsMap A map of item IDs to item instances (output). 道具ID到道具实例的映射(输出)。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + void DeserializeCollection(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap& ItemsMap); + + /** + * Serializes an entire inventory into a record. + * 将整个库存序列化为记录。 + * @param InventorySystem The inventory system component to serialize. 要序列化的库存系统组件。 + * @param Record The resulting inventory record. 输出的库存记录。 + * @return True if serialization was successful, false otherwise. 如果序列化成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + bool SerializeInventory(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record); + + /** + * Deserializes an inventory from an inventory record. + * 从库存记录反序列化库存。 + * @param InventorySystem The inventory system component to populate. 要填充的库存系统组件。 + * @param Record The inventory record to deserialize. 要反序列化的库存记录。 + */ + UFUNCTION(BlueprintCallable, Category="GIS") + void DeserializeInventory(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& Record); + +protected: + /** + * Initializes the item factory for the subsystem. + * 初始化子系统的道具工厂。 + */ + virtual void InitializeFactory(); + + /** + * The item factory used to create item instances. + * 用于创建道具实例的道具工厂。 + */ + UPROPERTY() + TObjectPtr Factory; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemComponent.h new file mode 100644 index 0000000..8da2b0e --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemComponent.h @@ -0,0 +1,916 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CollectionContainer.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_InventoryMeesages.h" +#include "Items/GIS_ItemInfo.h" +#include "GIS_SerializationStructLibrary.h" +#include "Components/ActorComponent.h" +#include "GIS_InventorySystemComponent.generated.h" + +class UGIS_CurrencySystemComponent; +class UGIS_ItemCollectionDefinition; +class UGIS_ItemCollection; + + +/** + * Delegate triggered when the inventory system is initialized. + * 库存系统初始化时触发的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGIS_Inventory_InitializedSignature); + +/** + * Delegate triggered when an item stack in the inventory is updated. + * 库存中的道具堆栈更新时触发的委托。 + * @param Message The update message containing stack details. 包含堆栈详细信息的更新消息。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Inventory_StackUpdateSignature, const FGIS_InventoryStackUpdateMessage&, Message); + +/** + * Delegate triggered when an item is added to the inventory (server-side only). + * 道具添加到库存时触发的委托(仅限服务器端)。 + * @param Message The message containing details of the added item. 包含添加道具详细信息的消息。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Inventory_AddItemInfoSignature, const FGIS_InventoryAddItemInfoMessage&, Message); + +/** + * Delegate triggered when an item addition is rejected (server-side only). + * 道具添加被拒绝时触发的委托(仅限服务器端)。 + * @param Message The message containing details of the rejected item. 包含拒绝道具详细信息的消息。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Inventory_AddItemInfoRejectedSignature, const FGIS_InventoryAddItemInfoRejectedMessage&, Message); + +/** + * Delegate triggered when an item is removed from the inventory (server-side only). + * 道具从库存移除时触发的委托(仅限服务器端)。 + * @param Message The message containing details of the removed item. 包含移除道具详细信息的消息。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Inventory_RemoveItemInfoSignature, const FGIS_InventoryRemoveItemInfoMessage&, Message); + +/** + * Delegate triggered when a collection is added or removed from the inventory. + * 集合添加或移除时触发的委托。 + * @param Collection The item collection involved. 涉及的道具集合。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Inventory_CollectionSignature, UGIS_ItemCollection*, Collection); + +/** + * Dynamic delegate for inventory system initialization events. + * 库存系统初始化事件的动态委托。 + */ +UDELEGATE() +DECLARE_DYNAMIC_DELEGATE(FGIS_InventorySystem_Initialized_DynamicEvent); + +/** + * Inventory system component for managing items and collections. + * 管理道具和集合的库存系统组件。 + */ +UCLASS(ClassGroup=(GIS), BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_InventorySystemComponent : public UActorComponent /*, public IGameFrameworkInitStateInterface*/ +{ + GENERATED_BODY() + + friend FGIS_ItemStackContainer; + friend FGIS_CollectionContainer; + +public: + /** + * Sets default values for this component's properties. + * 为组件的属性设置默认值。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_InventorySystemComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~UObject interface + /** + * Gets the properties that should be replicated for this object. + * 获取需要为此对象复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Replicates subobjects for this component. + * 为此组件复制子对象。 + * @param Channel The actor channel. 演员通道。 + * @param Bunch The replication data bunch. 复制数据束。 + * @param RepFlags The replication flags. 复制标志。 + * @return True if subobjects were replicated, false otherwise. 如果子对象被复制则返回true,否则返回false。 + */ + virtual bool ReplicateSubobjects(class UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags) override; + //~End of UObject interface + + //~UActorComponent interface + /** + * Called when the component is registered. + * 组件注册时调用。 + */ + virtual void OnRegister() override; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Prepares the component for replication. + * 为组件的复制做准备。 + */ + virtual void ReadyForReplication() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Updates the component each frame. + * 每帧更新组件。 + * @param DeltaTime Time since the last tick. 上次tick以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + //~End of UActorComponent interface + +#pragma region Inventory + /** + * Finds the inventory system component on the specified actor. + * 在指定演员上查找库存系统组件。 + * @param Actor The actor to search for the component. 要查找组件的演员。 + * @param Inventory The found inventory component (output). 找到的库存组件(输出)。 + * @return True if the component was found, false otherwise. 如果找到组件则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem", Meta = (DefaultToSelf="Actor", ExpandBoolAsExecs = "ReturnValue")) + static bool FindInventorySystemComponent(const AActor* Actor, UGIS_InventorySystemComponent*& Inventory); + + /** + * Gets the inventory system component from the specified actor. + * 从指定演员获取库存系统组件。 + * @param Actor The actor to query. 要查询的演员。 + * @return The inventory system component, or nullptr if not found. 库存系统组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem", Meta = (DefaultToSelf="Actor")) + static UGIS_InventorySystemComponent* GetInventorySystemComponent(const AActor* Actor); + + /** + * Static helper to find the inventory system component on an actor. + * 在演员上查找库存系统组件的静态辅助函数。 + * @param Actor The actor to query. 要查询的演员。 + * @return The inventory system component, or nullptr if not found. 库存系统组件,如果未找到则返回nullptr。 + */ + static UGIS_InventorySystemComponent* FindInventorySystemComponent(const AActor* Actor); + + /** + * Initializes the inventory system. + * 初始化库存系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual void InitializeInventorySystem(); + + /** + * Initializes the inventory system with a specific record. + * 使用特定记录初始化库存系统。 + * @param InventoryRecord The inventory record to initialize with. 初始化使用的库存记录。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual void InitializeInventorySystemWithRecord(const FGIS_InventoryRecord& InventoryRecord); + + /** + * Resets the inventory system. + * 重置库存系统。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual void ResetInventorySystem(); + + /** + * Checks if the inventory system is initialized. + * 检查库存系统是否已初始化。 + * @return True if initialized, false otherwise. 如果已初始化则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + bool IsInventoryInitialized() const; + + /** + * Binds a delegate to be called when the inventory system is initialized. + * 绑定一个委托,在库存系统初始化时调用。 + * @param Delegate The delegate to bind. 要绑定的委托。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + void BindToInventorySystemInitialized(FGIS_InventorySystem_Initialized_DynamicEvent Delegate); + + /** + * Gets the associated currency system of this inventory. + * 获取库存关联的货币系统。 + * @return The currency system component. 货币系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + UGIS_CurrencySystemComponent* GetCurrencySystem() const; + + /** + * Loads the default loadouts for the inventory. + * 为库存加载默认装备。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual void LoadDefaultLoadouts(); + + /** + * Server-side function to load default loadouts. + * 服务器端函数,用于加载默认装备。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|InventorySystem") + virtual void ServerLoadDefaultLoadouts(); + +protected: + /** + * Called when the inventory system is initialized. + * 库存系统初始化时调用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GIS|InventorySystem") + void OnInventorySystemInitialized(); + + /** + * List of delegates for inventory system initialization. + * 库存系统初始化的委托列表。 + */ + UPROPERTY() + TArray InitializedDelegates; + +#pragma endregion + +#pragma region InitState + + // /** + // * The name of this overall feature. + // * 此功能的整体名称。 + // */ + // static const FName NAME_ActorFeatureName; + // + // /** + // * Gets the feature name for the init state interface. + // * 获取初始化状态接口的功能名称。 + // * @return The feature name. 功能名称。 + // */ + // virtual FName GetFeatureName() const override; + // + // /** + // * Determines if the component can change its initialization state. + // * 确定组件是否可以更改其初始化状态。 + // * @param Manager The component manager. 组件管理器。 + // * @param CurrentState The current state. 当前状态。 + // * @param DesiredState The desired state. 期望状态。 + // * @return True if the state change is allowed, false otherwise. 如果允许状态更改则返回true,否则返回false。 + // */ + // virtual bool CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const override; + // + // /** + // * Handles a change in initialization state. + // * 处理初始化状态的更改。 + // * @param Manager The component manager. 组件管理器。 + // * @param CurrentState The current state. 当前状态。 + // * @param DesiredState The desired state. 期望状态。 + // */ + // virtual void HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) override; + // + // /** + // * Called when the actor's initialization state changes. + // * 演员的初始化状态更改时调用。 + // * @param Params The state change parameters. 状态更改参数。 + // */ + // virtual void OnActorInitStateChanged(const FActorInitStateChangedParams& Params) override; + // + // /** + // * Checks if the component has reached a specific initialization state. + // * 检查组件是否已达到特定初始化状态。 + // * @param State The state to check. 要检查的状态。 + // * @return True if the state has been reached, false otherwise. 如果已达到状态则返回true,否则返回false。 + // */ + // virtual bool HasReachedInitState(FGameplayTag State) const override; + // + // /** + // * Checks the default initialization state. + // * 检查默认初始化状态。 + // */ + // virtual void CheckDefaultInitialization() override; + // + // /** + // * Checks the initialization state of the inventory system. + // * 检查库存系统的初始化状态。 + // */ + // UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + // virtual void CheckInventoryInitialization(); + // + // /** + // * The current initialization state of the component. + // * 组件的当前初始化状态。 + // */ + // UPROPERTY(VisibleAnywhere, Category="InventorySystem") + // FGameplayTag CurrentInitState{FGameplayTag::EmptyTag}; + +private: +#pragma endregion + +#pragma region Events + +public: + /** + * Event triggered when the inventory system is initialized. + * 库存系统初始化时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_InitializedSignature OnInventorySystemInitializedEvent; + + /** + * Event triggered when any item stack in collections changes (both server and client). + * 集合中的道具堆栈更改时触发的事件(服务器和客户端均触发)。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_StackUpdateSignature OnInventoryStackUpdate; + + /** + * Event triggered when a collection is added to the inventory. + * 集合添加到库存时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_CollectionSignature OnCollectionAddedEvent; + + /** + * Event triggered when a collection is removed from the inventory. + * 集合从库存移除时触发的事件。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_CollectionSignature OnCollectionRemovedEvent; + + /** + * Event triggered when an item is added to the inventory (server-side only). + * 道具添加到库存时触发的事件(仅限服务器端)。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_AddItemInfoSignature OnInventoryAddItemInfo; + + /** + * Event triggered when an item addition is rejected (server-side only). + * 道具添加被拒绝时触发的事件(仅限服务器端)。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_AddItemInfoRejectedSignature OnInventoryAddItemInfo_Rejected; + + /** + * Event triggered when an item is removed from the inventory (server-side only). + * 道具从库存移除时触发的事件(仅限服务器端)。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_Inventory_RemoveItemInfoSignature OnInventoryRemoveItemInfo; + +#pragma endregion + +#pragma region Items + /** + * Checks if an item can be added to the inventory. + * 检查道具是否可以添加到库存。 + * @param InItemInfo The item info to check. 要检查的道具信息。 + * @param OutItemInfo The resulting item info (output). 结果道具信息(输出)。 + * @return True if the item can be added, false otherwise. 如果道具可以添加则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual bool CanAddItem(const FGIS_ItemInfo& InItemInfo, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Adds an item to the inventory. + * 将道具添加到库存。 + * @param ItemInfo The item info specifying the item, collection, and stack details. 指定道具、集合和堆栈详细信息的道具信息。 + * @return The number of items added, or 0 if none were added. 添加的道具数量,如果未添加则返回0。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem", meta=(AutoCreateRefTerm="ItemInfo")) + virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Adds a group of items to the inventory. + * 将一组道具添加到库存。 + * @param ItemInfos The array of item infos to add. 要添加的道具信息数组。 + * @return The array of actually added item infos. 实际添加的道具信息数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual TArray AddItems(TArray ItemInfos); + + /** + * Adds an item to a specific collection by its definition. + * 通过道具定义将道具添加到指定集合。 + * @param CollectionTag The tag of the target collection. 目标集合的标签。 + * @param ItemDefinition The item definition to add. 要添加的道具定义。 + * @param NewAmount The amount of the item to add. 要添加的道具数量。 + * @return The actually added item info. 实际添加的道具信息。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual FGIS_ItemInfo AddItemByDefinition(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, TSoftObjectPtr ItemDefinition, const int32 NewAmount); + + /** + * Server-side function to add an item to a specific collection by its definition. + * 服务器端函数,通过道具定义将道具添加到指定集合。 + * @param CollectionTag The tag of the target collection. 目标集合的标签。 + * @param ItemDefinition The item definition to add. 要添加的道具定义。 + * @param NewAmount The amount of the item to add. 要添加的道具数量。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|InventorySystem") + void ServerAddItemByDefinition(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, const TSoftObjectPtr& ItemDefinition, const int32 NewAmount); + + /** + * Checks if an item can be moved within the inventory. + * 检查道具是否可以在库存内移动。 + * @param ItemInfo The item info specifying the source and target collection. 指定源集合和目标集合的道具信息。 + * @return True if the item can be moved, false otherwise. 如果道具可以移动则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem", meta=(AutoCreateRefTerm="ItemInfo")) + virtual bool CanMoveItem(const FGIS_ItemInfo& ItemInfo) const; + + /** + * Moves an item within the inventory. + * 在库存内移动道具。 + * @param ItemInfo The item info specifying the source and target collection. 指定源集合和目标集合的道具信息。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem", meta=(AutoCreateRefTerm="ItemInfo")) + virtual void MoveItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Server-side function to move an item within the inventory. + * 服务器端函数,在库存内移动道具。 + * @param ItemInfo The item info specifying the source and target collection. 指定源集合和目标集合的道具信息。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|InventorySystem") + virtual void ServerMoveItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Checks if an item can be removed from the inventory. + * 检查道具是否可以从库存移除。 + * @param ItemInfo The item info to check. 要检查的道具信息。 + * @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + bool CanRemoveItem(const FGIS_ItemInfo& ItemInfo) const; + + /** + * Removes an item from the inventory. + * 从库存移除道具。 + * @param ItemInfo The item info specifying the item and amount to remove. 指定要移除的道具和数量的道具信息。 + * @return The item info of the actually removed items. 实际移除的道具信息。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + FGIS_ItemInfo RemoveItem(const FGIS_ItemInfo& ItemInfo); + + /** + * Server-side function to remove an item from the inventory. + * 服务器端函数,从库存移除道具。 + * @param ItemInfo The item info specifying the item and amount to remove. 指定要移除的道具和数量的道具信息。 + */ + UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|InventorySystem") + void ServerRemoveItem(FGIS_ItemInfo ItemInfo); + + /** + * Implementation of the server-side item removal. + * 服务器端道具移除的实现。 + * @param ItemInfo The item info specifying the item and amount to remove. 指定要移除的道具和数量的道具信息。 + */ + virtual void ServerRemoveItem_Implementation(FGIS_ItemInfo ItemInfo); + + /** + * Removes an item from the inventory by its definition. + * 通过道具定义从库存移除道具。 + * @param ItemDefinition The item definition to remove. 要移除的道具定义。 + * @param Amount The amount to remove. 要移除的数量。 + * @return The item info of the actually removed items. 实际移除的道具信息。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual FGIS_ItemInfo RemoveItemByDefinition(const TSoftObjectPtr ItemDefinition, const int32 Amount); + + /** + * Removes all items from the inventory. + * 从库存移除所有道具。 + * @param RemoveItemsFromIgnoredCollections Whether to remove items from ignored collections. 是否移除忽略集合中的道具。 + * @param DisableEventsWhileRemoving Whether to disable events during removal. 是否在移除期间禁用事件。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual void RemoveAllItems(bool RemoveItemsFromIgnoredCollections = false, bool DisableEventsWhileRemoving = true); + +#pragma endregion + +#pragma region Item Queries + /** + * Gets the amount of a specific item in the inventory. + * 获取库存中特定道具的数量。 + * @param Item The item instance to check. 要检查的道具实例。 + * @param SimilarItem Whether to count similar items or exact matches. 是否统计相似道具或精确匹配。 + * @return The amount of the item in the inventory. 库存中的道具数量。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual int32 GetItemAmount(UGIS_ItemInstance* Item, bool SimilarItem = true) const; + + /** + * Gets the total amount of items with the specified definition in all collections. + * 获取所有集合中具有指定定义的道具总数。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param Unique Whether to count unique items or total amounts. 是否统计唯一道具或总数量。 + * @return The number of items with the specified definition. 具有指定定义的道具数量。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual int32 GetItemAmountByDefinition(TSoftObjectPtr ItemDefinition, bool Unique) const; + + /** + * Gets item info for a specific item in a collection. + * 获取集合中特定道具的道具信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param OutItemInfo The item info (output). 道具信息(输出)。 + * @return True if the item was found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + virtual bool GetItemInfoInCollection(UGIS_ItemInstance* Item, UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Finds item info for a specific item in a collection. + * 在集合中查找特定道具的道具信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param OutItemInfo The item info (output). 道具信息(输出)。 + * @return True if the item was found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|InventorySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool FindItemInfoInCollection(UGIS_ItemInstance* Item, UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Gets all item infos in a specified collection. + * 获取指定集合中的所有道具信息。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param OutItemInfos The array of item infos (output). 道具信息数组(输出)。 + * @return True if any items were found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + virtual bool GetAllItemInfosInCollection(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, TArray& OutItemInfos) const; + + /** + * Finds all item infos in a specified collection. + * 查找指定集合中的所有道具信息。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param OutItemInfos The array of item infos (output). 道具信息数组(输出)。 + * @return True if any items were found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|InventorySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool FindAllItemInfosInCollection(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, TArray& OutItemInfos) const; + + /** + * Retrieves information about an item instance in the inventory. + * 检索库存中指定道具实例的信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param ItemInfo The item info (output). 道具信息(输出)。 + * @return True if the item was found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + virtual bool GetItemInfo(UGIS_ItemInstance* Item, FGIS_ItemInfo& ItemInfo) const; + + /** + * Finds information about an item instance in the inventory. + * 查找库存中指定道具实例的信息。 + * @param Item The item instance to query. 要查询的道具实例。 + * @param ItemInfo The item info (output). 道具信息(输出)。 + * @return True if the item was found, false otherwise. 如果找到道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|InventorySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool FindItemInfo(UGIS_ItemInstance* Item, FGIS_ItemInfo& ItemInfo) const; + + /** + * Retrieves the first item info matching the specified definition. + * 检索匹配指定定义的第一个道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfo The first matching item info (output). 匹配的第一个道具信息(输出)。 + * @return True if a matching item was found, false otherwise. 如果找到匹配的道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + virtual bool GetItemInfoByDefinition(const TSoftObjectPtr ItemDefinition, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Finds the first item info matching the specified definition. + * 查找匹配指定定义的第一个道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfo The first matching item info (output). 匹配的第一个道具信息(输出)。 + * @return True if a matching item was found, false otherwise. 如果找到匹配的道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|InventorySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + bool FindItemInfoByDefinition(const TSoftObjectPtr ItemDefinition, FGIS_ItemInfo& OutItemInfo) const; + + /** + * Retrieves all item infos matching the specified definition across all collections. + * 检索所有集合中匹配指定定义的道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfos The array of matching item infos (output). 匹配的道具信息数组(输出)。 + * @return True if any matching items were found, false otherwise. 如果找到匹配的道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem") + virtual bool GetItemInfosByDefinition(const TSoftObjectPtr ItemDefinition, TArray& OutItemInfos) const; + + /** + * Finds all item infos matching the specified definition across all collections. + * 查找所有集合中匹配指定定义的道具信息。 + * @param ItemDefinition The item definition to query. 要查询的道具定义。 + * @param OutItemInfos The array of matching item infos (output). 匹配的道具信息数组(输出)。 + * @return True if any matching items were found, false otherwise. 如果找到匹配的道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|InventorySystem", meta=(ExpandBoolAsExecs="ReturnValue")) + virtual bool FindItemInfosByDefinition(const TSoftObjectPtr ItemDefinition, TArray& OutItemInfos) const; + + /** + * Gets all item infos from the inventory. + * 获取库存中的所有道具信息。 + * @return The array of all item infos in the inventory. 库存中的所有道具信息数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual TArray GetItemInfos() const; + + /** + * Checks if the inventory has enough items of a specific definition. + * 检查库存中是否有足够多的指定道具。 + * @param ItemDefinition The item definition to check. 要检查的道具定义。 + * @param Amount The required amount. 所需数量。 + * @return True if there are enough items, false otherwise. 如果道具数量足够则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual bool HasEnoughItem(const TSoftObjectPtr ItemDefinition, int32 Amount) const; + +#pragma endregion + +#pragma region Collections + /** + * Gets all collections in the inventory. + * 获取库存中的所有集合。 + * @return The array of item collections. 道具集合数组。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual TArray GetItemCollections() const; + + /** + * Checks if all default collections have been created. + * 检查是否已创建所有默认集合。 + * @return True if all default collections are created, false otherwise. 如果所有默认集合都已创建则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual bool IsDefaultCollectionCreated() const; + + /** + * Determines the target collection for an item. + * 确定道具的目标集合。 + * @param ItemInfo The item info to determine the collection for. 要确定集合的道具信息。 + * @return The target item collection. 目标道具集合。 + */ + UGIS_ItemCollection* DetermineTargetCollection(const FGIS_ItemInfo& ItemInfo) const; + + /** + * Gets the default item collection. + * 获取默认道具集合。 + * @return The default item collection. 默认道具集合。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual UGIS_ItemCollection* GetDefaultCollection() const; + + /** + * Gets the number of collections in the inventory. + * 获取库存中的集合数量。 + * @return The number of collections. 集合数量。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + int32 GetCollectionCount() const; + + /** + * Gets a collection by its tag. + * 通过标签获取集合。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @return The collection with the specified tag. 具有指定标签的集合。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + UGIS_ItemCollection* GetCollectionByTag(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag) const; + + /** + * Gets a collection by matching tags. + * 通过匹配标签获取集合。 + * @param Tags The tags to match. 要匹配的标签。 + * @return The collection matching the tags. 匹配标签的集合。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + virtual UGIS_ItemCollection* GetCollectionByTags(FGameplayTagContainer Tags); + + /** + * Gets a collection by its ID. + * 通过ID获取集合。 + * @param CollectionId The ID of the collection. 集合的ID。 + * @return The collection with the specified ID. 具有指定ID的集合。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|InventorySystem") + UGIS_ItemCollection* GetCollectionById(FGuid CollectionId) const; + + /** + * Gets a typed collection by its tag. + * 通过标签获取类型化的集合。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param DesiredClass The desired class of the collection. 集合的期望类。 + * @return The typed collection with the specified tag. 具有指定标签的类型化集合。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem", meta=(DeterminesOutputType="DesiredClass", DynamicOutputParam="ReturnValue")) + UGIS_ItemCollection* GetTypedCollectionByTag(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, TSubclassOf DesiredClass) const; + + /** + * Finds a typed collection by its tag. + * 通过标签查找类型化的集合。 + * @param CollectionTag The tag of the collection. 集合的标签。 + * @param DesiredClass The desired class of the collection. 集合的期望类。 + * @param OutCollection The found collection (output). 找到的集合(输出)。 + * @return True if the collection was found, false otherwise. 如果找到集合则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|InventorySystem", meta=(DeterminesOutputType="DesiredClass", DynamicOutputParam="OutCollection", ExpandBoolAsExecs="ReturnValue")) + bool FindTypedCollectionByTag(UPARAM(meta=(Categories="GIS.Collection")) + const FGameplayTag CollectionTag, TSubclassOf DesiredClass, UGIS_ItemCollection*& OutCollection); + + /** + * Adds a collection to the inventory by its definition. + * 通过定义将集合添加到库存。 + * @param CollectionDefinition The collection definition to add. 要添加的集合定义。 + * @return The added collection instance. 添加的集合实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|InventorySystem") + virtual UGIS_ItemCollection* AddCollectionByDefinition(TSoftObjectPtr CollectionDefinition); + + /** + * Removes a collection entry by its index. + * 通过索引移除集合条目。 + * @param Idx The index of the collection entry to remove. 要移除的集合条目索引。 + */ + virtual void RemoveCollectionEntry(int32 Idx); + + /** + * Adds a collection entry to the inventory. + * 将集合条目添加到库存。 + * @param NewEntry The new collection entry to add. 要添加的新集合条目。 + * @return True if the entry was added, false otherwise. 如果条目被添加则返回true,否则返回false。 + */ + virtual bool AddCollectionEntry(const FGIS_CollectionEntry& NewEntry); + + /** + * Creates and initializes a new item collection from a definition. + * 从定义创建并初始化新的道具集合。 + * @param CollectionDefinition The collection definition. 集合定义。 + * @return The newly created uninitialized item collection instance. 新创建的未初始化道具集合实例。 + */ + virtual UGIS_ItemCollection* CreateCollectionInstance(const UGIS_ItemCollectionDefinition* CollectionDefinition); + + /** + * Checks if a collection should be ignored when searching for items. + * 检查集合在搜索道具时是否应被忽略。 + * @param ItemCollection The collection to check. 要检查的集合。 + * @return True if the collection should be ignored, false otherwise. 如果集合应被忽略则返回true,否则返回false。 + */ + virtual bool IsIgnoredCollection(UGIS_ItemCollection* ItemCollection) const; + +protected: + /** + * Called when a collection is added to the inventory. + * 集合添加到库存时调用。 + * @param Entry The added collection entry. 添加的集合条目。 + */ + virtual void OnCollectionAdded(const FGIS_CollectionEntry& Entry); + + /** + * Called when a collection is removed from the inventory. + * 集合从库存移除时调用。 + * @param Entry The removed collection entry. 移除的集合条目。 + */ + virtual void OnCollectionRemoved(const FGIS_CollectionEntry& Entry); + + /** + * Called when a collection is updated. + * 集合更新时调用。 + * @param Entry The updated collection entry. 更新的集合条目。 + */ + virtual void OnCollectionUpdated(const FGIS_CollectionEntry& Entry); + + /** + * Processes pending collections in the inventory. + * 处理库存中的待处理集合。 + */ + virtual void ProcessPendingCollections(); + + /** + * Map of pending collection entries. + * 待处理集合条目的映射。 + */ + UPROPERTY(VisibleAnywhere, Category="InventorySystem", Transient) + TMap PendingCollections; + +#pragma endregion + +#pragma region Properties + /** + * Predefined collections for the inventory. + * 库存的预定义集合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="InventorySystem") + TArray> CollectionDefinitions; + + /** + * Default items for initial collections. + * 默认集合的默认道具。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="InventorySystem", meta=(TitleProperty="Tag")) + TArray DefaultLoadouts; + + /** + * Container for the inventory's collections. + * 库存集合的容器。 + */ + UPROPERTY(VisibleAnywhere, Replicated, Category="InventorySystem", meta=(ShowOnlyInnerProperties)) + FGIS_CollectionContainer CollectionContainer; + + /** + * Cached map for O(1) collection lookups by ID. + * 按ID进行O(1)集合查找的缓存映射。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="InventorySystem", Transient, meta=(ForceInlineRow)) + TMap> CollectionIdToInstanceMap; + + /** + * Cached map for O(1) collection lookups by tag (commented out). + * 按标签进行O(1)集合查找的缓存映射(已注释)。 + */ + // UPROPERTY() + // TMap> CollectionTagToInstanceMap; + + /** + * Whether to use the initialization state chain. + * 是否使用初始化状态链。 + */ + // UPROPERTY(EditAnywhere, Category="InventorySystem") + // bool bUseInitStateChain = false; + + /** + * Whether to initialize the inventory on BeginPlay. + * 是否在BeginPlay时初始化库存。 + */ + UPROPERTY(EditAnywhere, Category="InventorySystem") + bool bInitializeOnBeginplay = false; + + /** + * Indicates if the inventory system is initialized. + * 指示库存系统是否已初始化。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="InventorySystem", ReplicatedUsing=OnInventorySystemInitialized) + bool bInventorySystemInitialized{false}; + + /** + * Collections with these tags will be ignored when searching for items. + * 搜索道具时将忽略具有这些标签的集合。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="InventorySystem") + FGameplayTagContainer IgnoredCollections; + + /** + * The associated currency system component. + * 关联的货币系统组件。 + */ + UPROPERTY() + UGIS_CurrencySystemComponent* CurrencySystem; + +#pragma endregion + +#pragma region Editor +#if WITH_EDITOR + /** + * Called after the component is loaded in the editor. + * 编辑器中组件加载后调用。 + */ + virtual void PostLoad() override; + + /** + * Called before the component is saved in the editor. + * 编辑器中组件保存前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates the component's data in the editor. + * 在编辑器中验证组件的数据。 + * @param Context The validation context. 验证上下文。 + * @return The result of the data validation. 数据验证的结果。 + */ + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; +#endif +#pragma endregion +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemSettings.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemSettings.h new file mode 100644 index 0000000..0a43352 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventorySystemSettings.h @@ -0,0 +1,96 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "GIS_InventorySystemSettings.generated.h" + +class UGIS_InventoryFactory; + +/** + * Structure representing a mapping of asset path prefix to item definition schema. + * 表示资产路径前缀到道具定义模式的映射结构体。 + */ +USTRUCT() +struct GENERICINVENTORYSYSTEM_API FGIS_ItemDefinitionSchemaEntry +{ + GENERATED_BODY() + + /** + * The path prefix for assets to apply this schema (e.g., "/Game/ActionRPG"). + * 应用此模式的资产路径前缀(例如,"/Game/ActionRPG")。 + */ + UPROPERTY(config, EditAnywhere, Category="Settings", meta=(ContentDir)) + FString PathPrefix; + + /** + * The schema to apply for assets under this path prefix. + * 对此路径前缀下的资产应用的模式。 + */ + UPROPERTY(config, EditAnywhere, Category="Settings", meta=(AllowedClasses="/Script/GenericInventorySystem.GIS_ItemDefinitionSchema")) + FSoftObjectPath Schema; +}; + +/** + * Settings for the Generic Inventory System. + * 通用库存系统的设置。 + */ +UCLASS(Config=Game, defaultconfig) +class GENERICINVENTORYSYSTEM_API UGIS_InventorySystemSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Constructor for the inventory system settings. + * 库存系统设置的构造函数。 + */ + UGIS_InventorySystemSettings(); + + /** + * Gets the category name for these settings. + * 获取这些设置的类别名称。 + * @return The category name. 类别名称。 + */ + virtual FName GetCategoryName() const override; + + /** + * Gets the inventory system settings instance. + * 获取库存系统设置实例。 + * @return The inventory system settings. 库存系统设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|Settings", meta=(DisplayName="Get Inventory System Settings")) + static const UGIS_InventorySystemSettings* Get(); + + /** + * Gets the item definition schema for a given asset path. + * 获取给定资产路径的道具定义模式。 + * @param AssetPath The path of the asset to find a schema for. 要查找模式的资产路径。 + * @return The schema to use, or nullptr if none found. 要使用的模式,如果未找到则返回nullptr。 + */ + const class UGIS_ItemDefinitionSchema* GetItemDefinitionSchemaForAsset(const FString& AssetPath) const; + + /** + * Inventory Factory class for managing low level operation in GIS. + * 库存工厂类用于管理GIS中的底层操作。 + */ + UPROPERTY(config, Category="Settings", EditDefaultsOnly, NoClear) + TSoftClassPtr InventoryFactoryClass; + + /** + * Array of path-to-schema mappings for item definition validation. + * 用于道具定义验证的路径到模式的映射数组。 + * @details Assets under the specified path prefix will use the corresponding schema. If no match is found, the default schema is used. + * @细节 指定路径前缀下的资产将使用对应的模式。如果未找到匹配,则使用默认模式。 + */ + UPROPERTY(config, EditAnywhere, Category="Settings") + TArray ItemDefinitionSchemaMap; + + /** + * The default schema to enforce item definition layout consistency when no path-specific schema is found. + * 当未找到路径特定模式时,用于强制道具定义布局一致性的默认模式。 + */ + UPROPERTY(config, EditAnywhere, Category="Settings", meta=(AllowedClasses="/Script/GenericInventorySystem.GIS_ItemDefinitionSchema")) + FSoftObjectPath DefaultItemDefinitionSchema; +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryTags.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryTags.h new file mode 100644 index 0000000..e6b0ab8 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_InventoryTags.h @@ -0,0 +1,72 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once +#include "NativeGameplayTags.h" + +/** + * Namespace for collection-related gameplay tags. + * 集合相关游戏标签的命名空间。 + */ +namespace GIS_CollectionTags +{ + /** Main inventory collection tag. 主要库存集合标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Main) + /** Hidden inventory collection tag. 隐藏库存集合标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Hidden) + /** Equipped items collection tag. 已装备道具集合标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Equipped) + /** Quick bar collection tag. 快捷栏集合标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(QuickBar); +} + +/** + * Namespace for inventory initialization state tags. + * 库存初始化状态标签的命名空间。 + */ +namespace GIS_InventoryInitState +{ + /** Tag indicating the inventory has been spawned. 指示库存已生成的标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Spawned); + /** Tag indicating data is available for the inventory. 指示库存数据可用的标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DataAvailable); + /** Tag indicating the inventory data is initialized. 指示库存数据已初始化的标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DataInitialized); + /** Tag indicating the inventory is ready for gameplay. 指示库存已准备好用于游戏的标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(GameplayReady); +} + +// namespace GIS_MessageTags +// { +// /** Tag for item stack updates. 道具堆叠更新标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(ItemStackUpdate); +// /** Tag for inventory updates. 库存更新标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InventoryUpdate); +// /** Tag for collection updates. 集合更新标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(CollectionUpdate); +// /** Tag for adding item information to the inventory. 添加道具信息到库存的标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InventoryAddItemInfo); +// /** Tag for rejected item addition to the inventory. 拒绝添加道具到库存的标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InventoryAddItemInfoRejected) +// /** Tag for removing item information from the inventory. 从库存移除道具信息的标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InventoryRemoveItemInfo); +// /** Tag for quick bar slots changes. 快捷栏槽位更改标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(QuickBarSlotsChanged); +// /** Tag for quick bar active index changes. 快捷栏激活索引更改标签。 */ +// GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(QuickBarActiveIndexChanged); +// } + +/** + * Namespace for attribute-related gameplay tags. + * 属性相关游戏标签的命名空间。 + */ +namespace GIS_AttributeTags +{ + /** Tag for the testing attribute. 用于测试属性的标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Dummy); + // /** Tag for the current enhancement level of an item. 道具当前强化等级标签。 */ + // GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(EnhancedLevel); + // /** Tag for the maximum enhancement level of an item. 道具最大强化等级标签。 */ + // GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MaxEnhancedLevel); + /** Tag for the stack size limit of an item. 道具堆叠数量限制标签。 */ + GENERICINVENTORYSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StackSizeLimit); +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_LogChannels.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_LogChannels.h new file mode 100644 index 0000000..827accd --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GIS_LogChannels.h @@ -0,0 +1,52 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + +DECLARE_STATS_GROUP(TEXT("GIS"), STATGROUP_GIS, STATCAT_Advanced) + +/** + * Gets the context string for logging purposes. + * 获取用于日志记录的上下文字符串。 + * @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。 + * @return The context string. 上下文字符串。 + */ +GENERICINVENTORYSYSTEM_API FString GetGISLogContextString(const UObject* ContextObject = nullptr); + +/** + * Log category for general inventory system messages. + * 通用库存系统消息的日志类别。 + */ +GENERICINVENTORYSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGIS, Log, All); + +/** + * Macro for logging inventory system messages. + * 用于记录库存系统消息的宏。 + * @details Logs messages with function name and formatted message. 记录包含函数名和格式化消息的日志。 + */ +#define GIS_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging for inventory system. + * 用于库存系统的基于上下文的日志记录宏。 + * @details Logs messages with function name, context, and formatted message. 记录包含函数名、上下文和格式化消息的日志。 + */ +#define GIS_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGISLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +/** + * Macro for context-based logging with an explicit owner. + * 使用显式拥有者进行基于上下文的日志记录宏。 + * @details Logs messages with function name, owner context, and formatted message. 记录包含函数名、拥有者上下文和格式化消息的日志。 + */ +#define GIS_OWNED_CLOG(LogOwner, Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGIS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGISLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/GenericInventorySystem.h b/Plugins/GIS/Source/GenericInventorySystem/Public/GenericInventorySystem.h new file mode 100644 index 0000000..bfc3f68 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/GenericInventorySystem.h @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +/** + * Module class for the Generic Inventory System. + * 通用库存系统的模块类。 + * @details Implements the module interface for initializing and shutting down the inventory system. + * @细节 实现模块接口以初始化和关闭库存系统。 + */ +class FGenericInventorySystemModule : public IModuleInterface +{ +public: + /** + * Called when the module is loaded into memory. + * 模块加载到内存时调用。 + */ + virtual void StartupModule() override; + + /** + * Called when the module is unloaded from memory. + * 模块从内存中卸载时调用。 + */ + virtual void ShutdownModule() override; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_CurrencyPickupComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_CurrencyPickupComponent.h new file mode 100644 index 0000000..b9ac7c8 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_CurrencyPickupComponent.h @@ -0,0 +1,42 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_PickupComponent.h" +#include "GIS_CurrencyPickupComponent.generated.h" + +class UGIS_CurrencySystemComponent; + +/** + * Component for picking up currencies into the currency system. + * 用于将货币拾取到货币系统的组件。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_CurrencyPickupComponent : public UGIS_PickupComponent +{ + GENERATED_BODY() + +public: + /** + * Called when the game starts to initialize the component. + * 游戏开始时调用以初始化组件。 + */ + virtual void BeginPlay() override; + + /** + * Performs the pickup logic, adding currencies to the picker's currency system. + * 执行拾取逻辑,将货币添加到拾取者的货币系统。 + * @param Picker The inventory system component of the actor performing the pickup. 执行拾取的演员的库存系统组件。 + * @return True if the pickup was successful, false otherwise. 如果拾取成功则返回true,否则返回false。 + */ + virtual bool Pickup(UGIS_InventorySystemComponent* Picker) override; + +protected: + /** + * The currency system component associated with this pickup. + * 与此拾取关联的货币系统组件。 + */ + UPROPERTY() + UGIS_CurrencySystemComponent* OwningCurrencySystem; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_InventoryPickupComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_InventoryPickupComponent.h new file mode 100644 index 0000000..11d6317 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_InventoryPickupComponent.h @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_PickupComponent.h" +#include "GIS_InventoryPickupComponent.generated.h" + +class UGIS_ItemCollection; + +/** + * Component for picking up an entire inventory into a specified collection. + * 用于将整个库存拾取到指定集合的组件。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_InventoryPickupComponent : public UGIS_PickupComponent +{ + GENERATED_BODY() + +public: + /** + * Called when the game starts to initialize the component. + * 游戏开始时调用以初始化组件。 + */ + virtual void BeginPlay() override; + + /** + * Performs the pickup logic, transferring the inventory to the picker's collection. + * 执行拾取逻辑,将库存转移到拾取者的集合。 + * @param Picker The inventory system component of the actor picking up the inventory. 拾取库存的演员的库存系统组件。 + * @return True if the pickup was successful, false otherwise. 如果拾取成功则返回true,否则返回false。 + */ + virtual bool Pickup(UGIS_InventorySystemComponent* Picker) override; + + /** + * Gets the owning inventory system component. + * 获取拥有的库存系统组件。 + * @return The inventory system component, or nullptr if not set. 库存系统组件,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|Pickup") + UGIS_InventorySystemComponent* GetOwningInventory() const; + +protected: + /** + * Adds the pickup inventory to the specified destination collection. + * 将拾取的库存添加到指定的目标集合。 + * @param DestCollection The destination collection to add the inventory to. 要添加库存的目标集合。 + * @return True if the addition was successful, false otherwise. 如果添加成功则返回true,否则返回false。 + */ + bool AddPickupToCollection(UGIS_ItemCollection* DestCollection); + + /** + * Specifies the collection in the picker's inventory to add the items to (defaults to Item.Collection.Main if not set). + * 指定拾取者库存中要添加道具的集合(如果未设置,默认为Item.Collection.Main)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup", meta=(Categories="GIS.Collection")) + FGameplayTag CollectionTag; + + /** + * The inventory system component associated with this pickup. + * 与此拾取关联的库存系统组件。 + */ + UPROPERTY() + TObjectPtr Inventory; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_ItemPickupComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_ItemPickupComponent.h new file mode 100644 index 0000000..0d2418c --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_ItemPickupComponent.h @@ -0,0 +1,75 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GIS_PickupComponent.h" +#include "GIS_ItemPickupComponent.generated.h" + +class UGIS_InventorySystemComponent; +class UGIS_WorldItemComponent; +class UGIS_ItemCollection; + +/** + * Component for picking up a single item, requiring a WorldItemComponent on the same actor. + * 用于拾取单个道具的组件,需要与GIS_WorldItemComponent共存于同一演员。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_ItemPickupComponent : public UGIS_PickupComponent +{ + GENERATED_BODY() + +public: + /** + * Performs the pickup logic, adding the item to the picker's inventory. + * 执行拾取逻辑,将道具添加到拾取者的库存。 + * @param Picker The inventory system component of the actor performing the pickup. 执行拾取的演员的库存系统组件。 + * @return True if the pickup was successful, false otherwise. 如果拾取成功则返回true,否则返回false。 + */ + virtual bool Pickup(UGIS_InventorySystemComponent* Picker) override; + + /** + * Gets the associated world item component. + * 获取关联的世界道具组件。 + * @return The world item component, or nullptr if not found. 世界道具组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|Pickup") + UGIS_WorldItemComponent* GetWorldItem() const; + +protected: + /** + * Specifies the collection in the picker's inventory to add the item to (defaults to Item.Collection.Main if not set). + * 指定拾取者库存中要添加道具的集合(如果未设置,默认为Item.Collection.Main)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup", meta=(Categories="GIS.Collection")) + FGameplayTag CollectionTag; + + /** + * If true, the pickup fails if the full amount cannot be added to the inventory. + * 如果为true,当无法将全部数量添加到库存集合时,拾取失败。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup") + bool bFailIfFullAmountNotFit = false; + + /** + * Called when the game starts to initialize the component. + * 游戏开始时调用以初始化组件。 + */ + virtual void BeginPlay() override; + + /** + * Attempts to add the item to the picker's inventory collection. + * 尝试将道具添加到拾取者的库存集合。 + * @param Picker The inventory system component of the actor performing the pickup. 执行拾取的演员的库存系统组件。 + * @return True if the addition was successful, false otherwise. 如果添加成功则返回true,否则返回false。 + */ + bool TryAddToCollection(UGIS_InventorySystemComponent* Picker); + + /** + * The associated world item component. + * 关联的世界道具组件。 + */ + UPROPERTY() + TObjectPtr WorldItemComponent; +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupActorInterface.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupActorInterface.h new file mode 100644 index 0000000..347b8b9 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupActorInterface.h @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GIS_PickupActorInterface.generated.h" + +/** + * Interface for filtering pickup class selection. + * 用于筛选拾取类选择的接口。 + * @details Acts as a marker interface for actors that support pickup functionality. + * @细节 作为支持拾取功能的演员的标记接口。 + */ +UINTERFACE() +class UGIS_PickupActorInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface class for pickup actor functionality. + * 拾取演员功能的接口类。 + */ +class GENERICINVENTORYSYSTEM_API IGIS_PickupActorInterface +{ + GENERATED_BODY() + + // Add interface functions to this class. This is the class that will be inherited to implement this interface. + // 在此添加接口函数。此类将被继承以实现该接口。 +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupComponent.h new file mode 100644 index 0000000..24da893 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_PickupComponent.h @@ -0,0 +1,70 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GIS_PickupComponent.generated.h" + +class UGIS_InventorySystemComponent; + +/** + * Delegate triggered when a pickup action succeeds or fails. + * 拾取动作成功或失败时触发的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGIS_PickupSignature); + +/** + * Base component for handling pickup logic in the inventory system. + * 库存系统中处理拾取逻辑的基组件。 + * @details Provides the core functionality for picking up items, currencies, or inventories. + * @细节 提供拾取道具、货币或库存的核心功能。 + */ +UCLASS(Abstract, Blueprintable, BlueprintType) +class GENERICINVENTORYSYSTEM_API UGIS_PickupComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the pickup component. + * 拾取组件的构造函数。 + */ + UGIS_PickupComponent(); + + /** + * Performs the pickup logic, typically called during interaction. + * 执行拾取逻辑,通常在交互时调用。 + * @param Picker The inventory system component of the actor performing the pickup. 执行拾取的演员的库存系统组件。 + * @return True if the pickup was successful, false otherwise. 如果拾取成功则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|Pickup") + virtual bool Pickup(UGIS_InventorySystemComponent* Picker); + + /** + * Delegate triggered when the pickup action succeeds. + * 拾取动作成功时触发的委托。 + */ + UPROPERTY(BlueprintAssignable, Category="Pickup") + FGIS_PickupSignature OnPickupSuccess; + + /** + * Delegate triggered when the pickup action fails. + * 拾取动作失败时触发的委托。 + */ + UPROPERTY(BlueprintAssignable, Category="Pickup") + FGIS_PickupSignature OnPickupFail; + +protected: + /** + * Notifies listeners of a successful pickup. + * 通知监听者拾取成功。 + */ + virtual void NotifyPickupSuccess(); + + /** + * Notifies listeners of a failed pickup. + * 通知监听者拾取失败。 + */ + virtual void NotifyPickupFailed(); +}; \ No newline at end of file diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_WorldItemComponent.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_WorldItemComponent.h new file mode 100644 index 0000000..a8e8898 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Pickups/GIS_WorldItemComponent.h @@ -0,0 +1,160 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CoreStructLibray.h" +#include "Items/GIS_ItemInfo.h" +#include "Components/ActorComponent.h" +#include "GIS_WorldItemComponent.generated.h" + +class UGIS_ItemInstance; + +/** + * Delegate triggered when an item info is set. + * 道具信息设置时触发的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_ItemInfoSetSignature, const FGIS_ItemInfo&, ItemInfo); + +/** + * Component for generating or referencing items in the world. + * 用于在世界中生成或引用道具的组件。 + * @details Used by item pickups to generate item instances or by spawned equipment to hold references to source items. + * @细节 道具拾取使用此组件生成道具实例,或由生成的装备使用以持有源道具的引用。 + */ +UCLASS(ClassGroup=(GIS), meta=(BlueprintSpawnableComponent)) +class GENERICINVENTORYSYSTEM_API UGIS_WorldItemComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor for the world item component. + * 世界道具组件的构造函数。 + * @param ObjectInitializer The object initializer. 对象初始化器。 + */ + UGIS_WorldItemComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Gets the properties that should be replicated for this component. + * 获取需要为此组件复制的属性。 + * @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the world item component from an actor. + * 从演员获取世界道具组件。 + * @param Actor The actor to query for the world item component. 要查询世界道具组件的演员。 + * @return The world item component, or nullptr if not found. 世界道具组件,如果未找到则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|WorldItem", meta=(DefaultToSelf="Actor")) + static UGIS_WorldItemComponent* GetWorldItemComponent(const AActor* Actor); + + /** + * Creates an item instance from a definition. + * 从定义创建道具实例。 + * @param ItemDefinition The item definition and amount to create the instance from. 用于创建实例的道具定义和数量。 + */ + UFUNCTION(BlueprintCallable, Category="GIS|WorldItem") + void CreateItemFromDefinition(FGIS_ItemDefinitionAmount ItemDefinition); + + /** + * Checks if the component has a valid item definition. + * 检查组件是否具有有效的道具定义。 + * @return True if the component will auto-create and own an item, false otherwise. 如果组件将自动创建并拥有道具则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|WorldItem") + bool HasValidDefinition() const; + + /** + * Sets the item info for the component (should be called only once). + * 设置组件的道具信息(应仅调用一次)。 + * @param InItem The item instance to set. 要设置的道具实例。 + * @param InAmount The amount of the item. 道具数量。 + */ + virtual void SetItemInfo(UGIS_ItemInstance* InItem, int32 InAmount); + + /** + * Resets the item info for the component. + * 重置组件的道具信息。 + */ + void ResetItemInfo(); + + /** + * Gets the associated item instance. + * 获取关联的道具实例。 + * @return The item instance, or nullptr if not set. 道具实例,如果未设置则返回nullptr。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|WorldItem") + UGIS_ItemInstance* GetItemInstance(); + + /** + * Creates a duplicated item instance with a new owner. + * 创建具有新拥有者的重复道具实例。 + * @param NewOwner The new owner for the duplicated instance. 重复实例的新拥有者。 + * @return The duplicated item instance, or nullptr if not possible. 重复的道具实例,如果无法创建则返回nullptr。 + */ + UGIS_ItemInstance* GetDuplicatedItemInstance(AActor* NewOwner); + + /** + * Gets the item info associated with the component. + * 获取与组件关联的道具信息。 + * @return The item info. 道具信息。 + */ + FGIS_ItemInfo GetItemInfo() const; + + /** + * Gets the amount of the associated item instance. + * 获取关联道具实例的数量。 + * @return The item amount. 道具数量。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|WorldItem") + int32 GetItemAmount() const; + + /** + * Called when the game starts to initialize the component. + * 游戏开始时调用以初始化组件。 + */ + virtual void BeginPlay() override; + + /** + * Gets the owner cast to a specific type. + * 获取特定类型的拥有者。 + * @return The owner cast to the specified type, or nullptr if the cast fails. 转换为指定类型的拥有者,如果转换失败则返回nullptr。 + */ + template + T* GetTypedOwner() + { + return Cast(GetOwner()); + } + + /** + * Delegate triggered when a valid item info is set. + * 有效道具信息设置时触发的委托。 + */ + UPROPERTY(BlueprintAssignable) + FGIS_ItemInfoSetSignature ItemInfoSetEvent; + +protected: + /** + * Item definition used to auto-create an item instance (if set, the component owns the instance). + * 用于自动创建道具实例的道具定义(如果设置,组件拥有该实例)。 + */ + UPROPERTY(EditAnywhere, Category="WorldItem") + FGIS_ItemDefinitionAmount Definition; + + /** + * The item info associated with this component. + * 与该组件关联的道具信息。 + */ + UPROPERTY(VisibleAnywhere, Category="WorldItem", ReplicatedUsing=OnRep_ItemInfo, meta=(ShowInnerProperties)) + FGIS_ItemInfo ItemInfo; + + /** + * Called when the item info is replicated. + * 道具信息复制时调用。 + */ + UFUNCTION() + void OnRep_ItemInfo(); +}; diff --git a/Plugins/GIS/Source/GenericInventorySystem/Public/Serialization/GIS_SerializationStructLibrary.h b/Plugins/GIS/Source/GenericInventorySystem/Public/Serialization/GIS_SerializationStructLibrary.h new file mode 100644 index 0000000..7e90312 --- /dev/null +++ b/Plugins/GIS/Source/GenericInventorySystem/Public/Serialization/GIS_SerializationStructLibrary.h @@ -0,0 +1,248 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SubclassOf.h" +#include "GameplayTagContainer.h" +#include "GIS_CurrencyEntry.h" +#include "GIS_MixinContainer.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "GIS_SerializationStructLibrary.generated.h" + +class UGIS_ItemFragment; +class UGIS_ItemCollectionDefinition; +class UGIS_ItemDefinition; +class UGIS_ItemCollection; +class UGIS_ItemInstance; + +/** + * Record of an item instance for serialization. + * 用于序列化的道具实例记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_ItemRecord +{ + GENERATED_BODY() + + /** + * Unique identifier for the item instance. + * 道具实例的唯一标识符。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGuid ItemId; + + /** + * Asset path to the item definition. + * 道具定义的资产路径。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FString DefinitionAssetPath; + + /** + * The serialized runtime state of each item fragment. + * 已经序列化的各item fragment 对应的运行时数据。 + */ + // UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + // TArray FragmentStateRecords; + + /** + * The serialized runtime state of each item fragment. + * 已经序列化的各item fragment 对应的运行时数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + TArray FragmentStateRecords; + + /** + * Binary data for the item instance. + * 道具实例的二进制数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + TArray ByteData; + + /** + * Equality operator to compare item records. + * 比较道具记录的相等性运算符。 + * @param Other The other item record to compare with. 要比较的其他道具记录。 + * @return True if the item IDs are equal, false otherwise. 如果道具ID相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_ItemRecord& Other) const; + + /** + * Checks if the item record is valid. + * 检查道具记录是否有效。 + * @return True if the item ID and definition asset path are valid, false otherwise. 如果道具ID和定义资产路径有效则返回true,否则返回false。 + */ + bool IsValid() const; +}; + + +/** + * Record for an item stack for serialization. + * 用于序列化的道具堆栈记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_StackRecord +{ + GENERATED_BODY() + + /** + * Unique identifier for the stack. + * 堆栈的唯一标识符。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGuid Id; + + /** + * Unique identifier for the associated item instance. + * 关联道具实例的唯一标识符。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGuid ItemId; + + /** + * Unique identifier for the collection containing the stack. + * 包含堆栈的集合的唯一标识符。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + FGuid CollectionId; + + /** + * Amount of items in the stack. + * 堆栈中的道具数量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS") + int32 Amount{0}; + + /** + * Equality operator to compare stack records. + * 比较堆栈记录的相等性运算符。 + * @param Other The other stack record to compare with. 要比较的其他堆栈记录。 + * @return True if the stack ID, item ID, and collection ID are equal, false otherwise. 如果堆栈ID、道具ID和集合ID相等则返回true,否则返回false。 + */ + bool operator==(const FGIS_StackRecord& Other) const + { + return ItemId == Other.ItemId && Id == Other.Id && CollectionId == Other.CollectionId; + } + + /** + * Checks if the stack record is valid. + * 检查堆栈记录是否有效。 + * @return True if the stack ID, item ID, and collection ID are valid, false otherwise. 如果堆栈ID、道具ID和集合ID有效则返回true,否则返回false。 + */ + bool IsValid() const; +}; + +/** + * Record of a collection instance for serialization. + * 用于序列化的道具集合记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_CollectionRecord +{ + GENERATED_BODY() + + /** + * Gameplay tag identifying the collection. + * 标识集合的游戏标签。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + FGameplayTag Tag; + + /** + * Unique identifier for the collection. + * 集合的唯一标识符。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + FGuid Id; + + /** + * Asset path to the collection definition. + * 集合定义的资产路径。 + */ + UPROPERTY(BlueprintReadWrite, SaveGame, Category="GIS") + FString DefinitionAssetPath; + + /** + * Array of stack records within the collection. + * 集合中的堆栈记录数组。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + TArray StackRecords; + + /** + * Checks if the collection record is valid. + * 检查集合记录是否有效。 + * @return True if the collection ID and definition asset path are valid, false otherwise. 如果集合ID和定义资产路径有效则返回true,否则返回false。 + */ + bool IsValid() const; +}; + +/** + * Record of an inventory for serialization. + * 用于序列化的库存记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_InventoryRecord +{ + GENERATED_BODY() + + /** + * Array of collection records within the inventory. + * 库存中的集合记录数组。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + TArray CollectionRecords; + + /** + * Array of item records within the inventory. + * 库存中的道具记录数组。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + TArray ItemRecords; + + /** + * Checks if the inventory record is valid. + * 检查库存记录是否有效。 + * @return True if the inventory contains at least one collection record, false otherwise. 如果库存包含至少一个集合记录则返回true,否则返回false。 + */ + bool IsValid() const + { + return CollectionRecords.Num() > 0; + } +}; + +/** + * Record of a currency system for serialization. + * 用于序列化的货币系统记录。 + */ +USTRUCT(BlueprintType) +struct GENERICINVENTORYSYSTEM_API FGIS_CurrencyRecord +{ + GENERATED_BODY() + + /** + * Default constructor for the currency record. + * 货币记录的默认构造函数。 + */ + FGIS_CurrencyRecord(); + + /** + * Unique key for the currency record. + * 货币记录的唯一键。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + FName Key; + + /** + * Array of currency entries in the record. + * 记录中的货币条目数组。 + */ + UPROPERTY(BlueprintReadOnly, SaveGame, Category="GIS") + TArray Currencies; +}; diff --git a/Plugins/GMS/Config/BaseGenericMovementSystem.ini b/Plugins/GMS/Config/BaseGenericMovementSystem.ini new file mode 100644 index 0000000..1c1d6da --- /dev/null +++ b/Plugins/GMS/Config/BaseGenericMovementSystem.ini @@ -0,0 +1,18 @@ +[CoreRedirects] +;GMS 1.4 -> 1.5 refactoring. ++ClassRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimLayer_Overlay_PoseBased",NewName="/Script/GenericMovementSystem.GMS_AnimLayer_Overlay_PoseStack") ++ClassRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimLayerSetting_Overlay_PoseBased",NewName="/Script/GenericMovementSystem.GMS_AnimLayerSetting_Overlay_PoseStack") ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_PoseOverlaySetting",NewName="/Script/GenericMovementSystem.GMS_OverlayModeSetting_PoseStack") ++ClassRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimLayerSetting_Overlay_Stack",NewName="/Script/GenericMovementSystem.GMS_AnimLayerSetting_Overlay_ParallelSequenceStack") ++ClassRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimLayer_Overlay_Stack",NewName="/Script/GenericMovementSystem.GMS_AnimLayer_Overlay_ParallelSequenceStack") ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_StackedOverlaySetting",NewName="/Script/GenericMovementSystem.GMS_OverlayModeSetting_ParallelSequenceStack") ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimData_StackedOverlays",NewName="/Script/GenericMovementSystem.GMS_ParallelSequenceStack") ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimData_Overlay",NewName="/Script/GenericMovementSystem.GMS_ParallelSequenceStackEntry") ++PropertyRedirects = (OldName="/Script/GenericMovementSystem.GMS_OverlayModeSetting_ParallelSequenceStack.StackedOverlays",NewName="/Script/GenericMovementSystem.GMS_OverlayModeSetting_ParallelSequenceStack.Stacks") ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_OverlayStackState",NewName="/Script/GenericMovementSystem.GMS_ParallelSequenceStackState") ++ClassRedirects = (OldName="/Game/GenericGame/MovementSystem/Core/AnimLayers/ABPT_GMS_Layer_Overlay_PoseBased.ABPT_GMS_Layer_Overlay_PoseBased_C",NewName="/Game/GenericGame/MovementSystem/Core/AnimLayers/ABPT_GMS_Layer_Overlay_PoseStack.ABPT_GMS_Layer_Overlay_PoseStack_C") ++ClassRedirects = (OldName="/Game/GenericGame/MovementSystem/Core/AnimLayers/ABPT_GMS_Layer_Overlay_StackBased.ABPT_GMS_Layer_Overlay_StackBased_C",NewName="/Game/GenericGame/MovementSystem/Core/AnimLayers/ABPT_GMS_Layer_Overlay_ParallelSequenceStack.ABPT_GMS_Layer_Overlay_ParallelSequenceStack_C") + ++StructRedirects = (OldName="/Script/GenericMovementSystem.GMS_AnimData_OverlayEntry_Pose",NewName="/Script/GenericMovementSystem.GMS_PoseStackEntry") + + diff --git a/Plugins/GMS/Config/FilterPlugin.ini b/Plugins/GMS/Config/FilterPlugin.ini new file mode 100644 index 0000000..386e260 --- /dev/null +++ b/Plugins/GMS/Config/FilterPlugin.ini @@ -0,0 +1,9 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll +/Config/* \ No newline at end of file diff --git a/Plugins/GMS/GenericMovementSystem.uplugin b/Plugins/GMS/GenericMovementSystem.uplugin new file mode 100644 index 0000000..ec530cb --- /dev/null +++ b/Plugins/GMS/GenericMovementSystem.uplugin @@ -0,0 +1,66 @@ +{ + "FileVersion": 3, + "Version": 8, + "VersionName": "1.5.1", + "FriendlyName": "GenericMovementSystem", + "Description": "Powerful and advanced movement control and locomotion(animation)system designed for enhanced flexibility, seamless integration, user-friendly experience, and effortless expandability.", + "Category": "Gameplay", + "CreatedBy": "YuewuDev", + "CreatedByURL": "https://yuewu.dev", + "DocsURL": "https://www.yuewu.dev/en/wiki", + "MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/c4eca691-0c0c-45fc-ada3-7e9af9beae71", + "SupportURL": "https://discord.com/invite/xMRXAB2", + "EngineVersion": "5.7.0", + "CanContainContent": false, + "Installed": true, + "Modules": [ + { + "Name": "GenericMovementSystem", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Android", + "Linux" + ] + }, + { + "Name": "GenericMovementEditor", + "Type": "UncookedOnly", + "LoadingPhase": "PreDefault", + "PlatformAllowList": [ + "Win64" + ] + } + ], + "Plugins": [ + { + "Name": "Niagara", + "Enabled": true + }, + { + "Name": "ModularGameplay", + "Enabled": true + }, + { + "Name": "AnimationWarping", + "Enabled": true + }, + { + "Name": "AnimationLocomotionLibrary", + "Enabled": true + }, + { + "Name": "Mover", + "Enabled": true + }, + { + "Name": "PoseSearch", + "Enabled": true + }, + { + "Name": "Chooser", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/GMS/Resources/Icon128.png b/Plugins/GMS/Resources/Icon128.png new file mode 100644 index 0000000..d617ef1 Binary files /dev/null and b/Plugins/GMS/Resources/Icon128.png differ diff --git a/Plugins/GMS/Source/GenericMovementEditor/GenericMovementEditor.Build.cs b/Plugins/GMS/Source/GenericMovementEditor/GenericMovementEditor.Build.cs new file mode 100644 index 0000000..62436a1 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/GenericMovementEditor.Build.cs @@ -0,0 +1,33 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericMovementEditor : ModuleRules +{ + public GenericMovementEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + CppCompileWarningSettings.NonInlinedGenCppWarningLevel = WarningLevel.Warning; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", "CoreUObject", "Engine", "AnimGraphRuntime", "AnimationModifiers", "AnimationBlueprintLibrary", + "SlateCore", + "GenericMovementSystem", + } + ); + + PrivateDependencyModuleNames.AddRange(new[] + { + "UnrealEd", + "AnimGraph", + "BlueprintGraph", + "ToolMenus", + "Blutility", + "AssetTools", + "GameplayTags", + }); + } +} \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Blutility/GMS_EditorUtilityLibrary.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Blutility/GMS_EditorUtilityLibrary.cpp new file mode 100644 index 0000000..1a8baaa --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Blutility/GMS_EditorUtilityLibrary.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Blutility/GMS_EditorUtilityLibrary.h" +#include "AnimationModifier.h" +#include "AnimationModifiersAssetUserData.h" +#include "EditorUtilityLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_EditorUtilityLibrary) + +void UGMS_EditorUtilityLibrary::RevertAnimModifierOfClass(TSubclassOf ModifierClass) +{ + if (!IsValid(ModifierClass)) + { + return; + } + TArray Anims = UEditorUtilityLibrary::GetSelectedAssetsOfClass(UAnimSequence::StaticClass()); + + for (int32 i = 0; i < Anims.Num(); i++) + { + if (UAnimSequence* AnimSequence = Cast(Anims[i])) + { + if (UAnimationModifiersAssetUserData* UserData = AnimSequence->GetAssetUserData()) + { + const TArray& Modifiers = UserData->GetAnimationModifierInstances().FilterByPredicate([&](const UAnimationModifier* Instance) + { + return Instance && Instance->GetClass()->IsChildOf(ModifierClass); + }); + + for (const UAnimationModifier* Modifier : Modifiers) + { + if (Modifier) + { + Modifier->RevertFromAnimationSequence(AnimSequence); + } + } + } + } + } +} + +float UGMS_EditorUtilityLibrary::GetSamplingFrameRate(const UAnimSequence* AnimSequence) +{ + if (AnimSequence) + { + return AnimSequence->GetSamplingFrameRate().AsDecimal(); + } + + return 0; +} + +TArray UGMS_EditorUtilityLibrary::GetAllCurveNames(const UAnimSequence* AnimSequence) +{ + TArray Names; + if (IsValid(AnimSequence)) + { + for (const FFloatCurve& FloatCurve : AnimSequence->GetCurveData().FloatCurves) + { + Names.Add(FloatCurve.GetName()); + } + } + + return Names; +} + + diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_AssetTypeActions.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_AssetTypeActions.cpp new file mode 100644 index 0000000..a229df2 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_AssetTypeActions.cpp @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Factories/GMS_AssetTypeActions.h" +#include "GenericMovementEditor.h" +#include "Locomotions/GMS_AnimLayer_Overlay_PoseStack.h" +#include "Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h" +#include "Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h" +#include "Settings/GMS_SettingObjectLibrary.h" + + +uint32 FGMS_AssetTypeAction::GetCategories() +{ + return FGenericMovementEditorModule::GetAssetsCategory(); +} + +FColor FGMS_AssetTypeAction::GetTypeColor() const +{ + return FColor(114, 40, 199); +} + +#define IMPLEMENT_GMS_ASSET_ACTION(ActionName, NameText, DescText) \ +FText FGMS_AssetTypeAction_##ActionName::GetName() const \ +{ \ +return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Name", NameText); \ +} \ +FText FGMS_AssetTypeAction_##ActionName::GetAssetDescription(const FAssetData& AssetData) const \ +{ \ +return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Description", DescText); \ +} \ +UClass* FGMS_AssetTypeAction_##ActionName::GetSupportedClass() const \ +{ \ +return UGMS_##ActionName::StaticClass(); \ +} + +IMPLEMENT_GMS_ASSET_ACTION(MovementDefinition, "Movement Definition", + "Data Asset that defines multiple movement set settings together.") +IMPLEMENT_GMS_ASSET_ACTION(MovementControlSetting_Default, "Movement Control Setting", + "Data Asset that defines movement control settings") +IMPLEMENT_GMS_ASSET_ACTION(AnimGraphSetting, "Anim Graph Setting", + "Data Asset that stores animation graph-specific settings, one per unique skeleton.") +IMPLEMENT_GMS_ASSET_ACTION(AnimLayerSetting_States_Default, "States Anim Layer Setting(Basic Locomotion)", + "Data Asset that stores basic locomotion(default states animation layer settings.)") +IMPLEMENT_GMS_ASSET_ACTION(AnimLayerSetting_Overlay_PoseStack, "Overlay Anim Layer Setting(Pose Stack)", + "Data Asset that stores pose stack overlay settings") +IMPLEMENT_GMS_ASSET_ACTION(AnimLayerSetting_Overlay_SequenceStack, "Overlay Anim Layer Setting(Sequence Stack)", + "Data Asset that stores sequence stack overlay settings") diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_DataAssetsFactories.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_DataAssetsFactories.cpp new file mode 100644 index 0000000..d2f4027 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Factories/GMS_DataAssetsFactories.cpp @@ -0,0 +1,46 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Factories/GMS_DataAssetsFactories.h" + +#include "Locomotions/GMS_AnimLayer_Overlay_PoseStack.h" +#include "Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h" +#include "Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h" +#include "Settings/GMS_SettingObjectLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_DataAssetsFactories) + + +UGMS_Factory::UGMS_Factory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + bEditAfterNew = true; + bCreateNew = true; +} + +uint32 UGMS_Factory::GetMenuCategories() const +{ + return Super::GetMenuCategories(); +} + +const TArray& UGMS_Factory::GetMenuCategorySubMenus() const +{ + return Super::GetMenuCategorySubMenus(); +} + +#define IMPLEMENT_GMS_FACTORY(FactoryName) \ +UGMS_Factory_##FactoryName::UGMS_Factory_##FactoryName(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) \ +{ \ +SupportedClass = UGMS_##FactoryName::StaticClass(); \ +} \ +UObject* UGMS_Factory_##FactoryName::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) \ +{ \ +check(Class->IsChildOf(UGMS_##FactoryName::StaticClass())); \ +return NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); \ +} + +IMPLEMENT_GMS_FACTORY(MovementDefinition) +IMPLEMENT_GMS_FACTORY(MovementControlSetting_Default) +IMPLEMENT_GMS_FACTORY(AnimGraphSetting) +IMPLEMENT_GMS_FACTORY(AnimLayerSetting_States_Default) +IMPLEMENT_GMS_FACTORY(AnimLayerSetting_Overlay_PoseStack) +IMPLEMENT_GMS_FACTORY(AnimLayerSetting_Overlay_SequenceStack) diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.cpp new file mode 100644 index 0000000..e7cc66a --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.cpp @@ -0,0 +1,44 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericMovementEditor.h" +#include "AssetToolsModule.h" +#include "Factories/GMS_AssetTypeActions.h" + +#define LOCTEXT_NAMESPACE "FGenericMovementEditorModule" + +TArray> FGenericMovementEditorModule::AssetTypeActions = { + MakeShared(), + MakeShared(), + MakeShared(), + MakeShared(), + MakeShared(), + MakeShared() +}; + +EAssetTypeCategories::Type FGenericMovementEditorModule::AssetsCategory; + + +void FGenericMovementEditorModule::StartupModule() +{ + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + AssetsCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("GenericMovementSystem")), LOCTEXT("GMS_AssetsCategory", "Generic Movement System")); + for (TSharedPtr& Action : AssetTypeActions) + { + AssetTools.RegisterAssetTypeActions(Action.ToSharedRef()); + } +} + +void FGenericMovementEditorModule::ShutdownModule() +{ + if (const FAssetToolsModule* AssetTools = FModuleManager::GetModulePtr("AssetTools")) + { + for (TSharedPtr& Action : AssetTypeActions) + { + AssetTools->Get().UnregisterAssetTypeActions(Action.ToSharedRef()); + } + } +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericMovementEditorModule, GenericMovementEditor) diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.h b/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.h new file mode 100644 index 0000000..6425005 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/GenericMovementEditor.h @@ -0,0 +1,24 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeCategories.h" +#include "IAssetTools.h" +#include "Modules/ModuleManager.h" + +class FGenericMovementEditorModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + static EAssetTypeCategories::Type GetAssetsCategory() + { + return AssetsCategory; + } + +private: + static TArray> AssetTypeActions; + static EAssetTypeCategories::Type AssetsCategory; +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_CurvesBlend.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_CurvesBlend.cpp new file mode 100644 index 0000000..2beac72 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_CurvesBlend.cpp @@ -0,0 +1,26 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Nodes/GMS_AnimGraphNode_CurvesBlend.h" + + +#define LOCTEXT_NAMESPACE "GMS_CurvesBlendAnimationGraphNode" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimGraphNode_CurvesBlend) + +FText UGMS_AnimGraphNode_CurvesBlend::GetNodeTitle(const ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("Title", "Blend Curves"); +} + +FText UGMS_AnimGraphNode_CurvesBlend::GetTooltipText() const +{ + return LOCTEXT("Tooltip", "Blend Curves"); +} + +FString UGMS_AnimGraphNode_CurvesBlend::GetNodeCategory() const +{ + return FString{TEXTVIEW("GMS|Blends")}; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.cpp new file mode 100644 index 0000000..abb4a45 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.cpp @@ -0,0 +1,106 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Nodes/GMS_AnimGraphNode_GameplayTagsBlend.h" + +#include "GameplayTagsManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimGraphNode_GameplayTagsBlend) + +#define LOCTEXT_NAMESPACE "GMS_AnimGraphNode_GameplayTagsBlend" + +UGMS_AnimGraphNode_GameplayTagsBlend::UGMS_AnimGraphNode_GameplayTagsBlend() +{ + Node.AddPose(); +} + +void UGMS_AnimGraphNode_GameplayTagsBlend::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(FGMS_AnimNode_GameplayTagsBlend, Tags)) + { + ReconstructNode(); + } + + Super::PostEditChangeProperty(PropertyChangedEvent); +} + +FText UGMS_AnimGraphNode_GameplayTagsBlend::GetNodeTitle(const ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("Title", "Blend Poses by Gameplay Tag"); +} + +FText UGMS_AnimGraphNode_GameplayTagsBlend::GetTooltipText() const +{ + return LOCTEXT("Tooltip", "Blend Poses by Gameplay Tag"); +} + +void UGMS_AnimGraphNode_GameplayTagsBlend::ReallocatePinsDuringReconstruction(TArray& PreviousPins) +{ + Node.RefreshPoses(); + + Super::ReallocatePinsDuringReconstruction(PreviousPins); +} + +FString UGMS_AnimGraphNode_GameplayTagsBlend::GetNodeCategory() const +{ + return FString{TEXTVIEW("GMS|Blends")}; +} + +FName GetSimpleTagName(const FGameplayTag& Tag) +{ + const auto TagNode{UGameplayTagsManager::Get().FindTagNode(Tag)}; + + return TagNode.IsValid() ? TagNode->GetSimpleTagName() : NAME_None; +} + +void UGMS_AnimGraphNode_GameplayTagsBlend::CustomizePinData(UEdGraphPin* Pin, const FName SourcePropertyName, const int32 PinIndex) const +{ + Super::CustomizePinData(Pin, SourcePropertyName, PinIndex); + + bool bBlendPosePin; + bool bBlendTimePin; + GetBlendPinProperties(Pin, bBlendPosePin, bBlendTimePin); + + if (!bBlendPosePin && !bBlendTimePin) + { + return; + } + + Pin->PinFriendlyName = PinIndex <= 0 + ? LOCTEXT("Default", "Default") + : PinIndex > Node.Tags.Num() + ? LOCTEXT("Invalid", "Invalid") + : FText::AsCultureInvariant(GetSimpleTagName(Node.Tags[PinIndex - 1]).ToString()); + + if (bBlendPosePin) + { + static const FTextFormat BlendPosePinFormat{LOCTEXT("Pose", "{PinName} Pose")}; + + Pin->PinFriendlyName = FText::Format(BlendPosePinFormat, {{FString{TEXTVIEW("PinName")}, Pin->PinFriendlyName}}); + } + else if (bBlendTimePin) + { + static const FTextFormat BlendTimePinFormat{LOCTEXT("BlendTime", "{PinName} Blend Time")}; + + Pin->PinFriendlyName = FText::Format(BlendTimePinFormat, {{FString{TEXTVIEW("PinName")}, Pin->PinFriendlyName}}); + } +} + +void UGMS_AnimGraphNode_GameplayTagsBlend::GetBlendPinProperties(const UEdGraphPin* Pin, bool& bBlendPosePin, bool& bBlendTimePin) +{ + const auto PinFullName{Pin->PinName.ToString()}; + const auto SeparatorIndex{PinFullName.Find(TEXT("_"), ESearchCase::CaseSensitive)}; + + if (SeparatorIndex <= 0) + { + bBlendPosePin = false; + bBlendTimePin = false; + return; + } + + const auto PinName{PinFullName.Left(SeparatorIndex)}; + bBlendPosePin = PinName == TEXTVIEW("BlendPose"); + bBlendTimePin = PinName == TEXTVIEW("BlendTime"); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.cpp new file mode 100644 index 0000000..994bafa --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.cpp @@ -0,0 +1,111 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Nodes/GMS_AnimGraphNode_LayeredBoneBlend.h" +#include "ToolMenus.h" +#include "Kismet2/BlueprintEditorUtils.h" + +#include "AnimGraphCommands.h" +#include "ScopedTransaction.h" + +#include "Kismet2/CompilerResultsLog.h" +#include "UObject/UE5ReleaseStreamObjectVersion.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimGraphNode_LayeredBoneBlend) + +///////////////////////////////////////////////////// +// UGMS_AnimGraphNode_LayeredBoneBlend + +#define LOCTEXT_NAMESPACE "A3Nodes" + +UGMS_AnimGraphNode_LayeredBoneBlend::UGMS_AnimGraphNode_LayeredBoneBlend(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + Node.AddPose(); +} + +FLinearColor UGMS_AnimGraphNode_LayeredBoneBlend::GetNodeTitleColor() const +{ + return FLinearColor(0.2f, 0.8f, 0.3f); +} + +FText UGMS_AnimGraphNode_LayeredBoneBlend::GetTooltipText() const +{ + return LOCTEXT("AnimGraphNode_LayeredBoneBlend_Tooltip", "Generic Layered blend per bone"); +} + +FText UGMS_AnimGraphNode_LayeredBoneBlend::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("AnimGraphNode_LayeredBoneBlend_Title", "Generic Layered blend per bone"); +} + +FString UGMS_AnimGraphNode_LayeredBoneBlend::GetNodeCategory() const +{ + return TEXT("Animation|Blends"); +} + +void UGMS_AnimGraphNode_LayeredBoneBlend::AddPinToBlendByFilter() +{ + FScopedTransaction Transaction( LOCTEXT("AddPinToBlend", "AddPinToBlendByFilter") ); + Modify(); + + Node.AddPose(); + ReconstructNode(); + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint()); +} + +void UGMS_AnimGraphNode_LayeredBoneBlend::RemovePinFromBlendByFilter(UEdGraphPin* Pin) +{ + FScopedTransaction Transaction( LOCTEXT("RemovePinFromBlend", "RemovePinFromBlendByFilter") ); + Modify(); + + FProperty* AssociatedProperty; + int32 ArrayIndex; + GetPinAssociatedProperty(GetFNodeType(), Pin, /*out*/ AssociatedProperty, /*out*/ ArrayIndex); + + if (ArrayIndex != INDEX_NONE) + { + //@TODO: ANIMREFACTOR: Need to handle moving pins below up correctly + // setting up removed pins info + RemovedPinArrayIndex = ArrayIndex; + Node.RemovePose(ArrayIndex); + ReconstructNode(); + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint()); + } +} + +void UGMS_AnimGraphNode_LayeredBoneBlend::GetNodeContextMenuActions(UToolMenu* Menu, UGraphNodeContextMenuContext* Context) const +{ + if (!Context->bIsDebugging) + { + { + FToolMenuSection& Section = Menu->AddSection("AnimGraphNodeLayeredBoneblend", LOCTEXT("LayeredBoneBlend", "Generic Layered Bone Blend")); + if (Context->Pin != NULL) + { + // we only do this for normal BlendList/BlendList by enum, BlendList by Bool doesn't support add/remove pins + if (Context->Pin->Direction == EGPD_Input) + { + //@TODO: Only offer this option on arrayed pins + Section.AddMenuEntry(FAnimGraphCommands::Get().RemoveBlendListPin); + } + } + else + { + Section.AddMenuEntry(FAnimGraphCommands::Get().AddBlendListPin); + } + } + } +} + +void UGMS_AnimGraphNode_LayeredBoneBlend::ValidateAnimNodeDuringCompilation(class USkeleton* ForSkeleton, class FCompilerResultsLog& MessageLog) +{ + UAnimGraphNode_Base::ValidateAnimNodeDuringCompilation(ForSkeleton, MessageLog); + + // ensure to cache the per-bone blend weights + if (!Node.ArePerBoneBlendWeightsValid(ForSkeleton)) + { + Node.RebuildPerBoneBlendWeights(ForSkeleton); + } +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_OrientationWarping.cpp b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_OrientationWarping.cpp new file mode 100644 index 0000000..46582b6 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Private/Nodes/GMS_AnimGraphNode_OrientationWarping.cpp @@ -0,0 +1,287 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Nodes/GMS_AnimGraphNode_OrientationWarping.h" +#include "Animation/AnimRootMotionProvider.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/CompilerResultsLog.h" +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "PropertyHandle.h" +#include "ScopedTransaction.h" + +#define LOCTEXT_NAMESPACE "GMS_AnimGraphNode_OrientationWarping" +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimGraphNode_OrientationWarping) + +UGMS_AnimGraphNode_OrientationWarping::UGMS_AnimGraphNode_OrientationWarping(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +FText UGMS_AnimGraphNode_OrientationWarping::GetControllerDescription() const +{ + return LOCTEXT("OrientationWarping", "Generic Orientation Warping"); +} + +FText UGMS_AnimGraphNode_OrientationWarping::GetTooltipText() const +{ + return LOCTEXT("OrientationWarpingTooltip", "Rotates the root and lower body by the specified angle, while counter rotating the upper body to maintain the forward facing direction."); +} + +FText UGMS_AnimGraphNode_OrientationWarping::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return GetControllerDescription(); +} + +FLinearColor UGMS_AnimGraphNode_OrientationWarping::GetNodeTitleColor() const +{ + return FLinearColor(FColor(0.f, 255.f, 0.f)); +} + +FString UGMS_AnimGraphNode_OrientationWarping::GetNodeCategory() const +{ + return TEXT("GMS|Animation Warping"); +} + +void UGMS_AnimGraphNode_OrientationWarping::CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const +{ + Super::CustomizePinData(Pin, SourcePropertyName, ArrayIndex); + + return; //直接返回,编辑器模式下不根据配置情况动态隐藏/显示属性 + + // if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, OrientationAngle)) + // { + // Pin->bHidden = (Node.Mode == EWarpingEvaluationMode::Graph); + // } + // + // if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngle)) + // { + // Pin->bHidden = (Node.Mode == EWarpingEvaluationMode::Manual); + // } + // + // if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngleDeltaThreshold)) + // { + // Pin->bHidden = (Node.Mode == EWarpingEvaluationMode::Manual); + // } +} + +void UGMS_AnimGraphNode_OrientationWarping::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + Super::CustomizeDetails(DetailBuilder); + + return; //直接返回,编辑器模式下不根据配置情况动态隐藏/显示属性 + + // DetailBuilder.SortCategories([](const TMap& CategoryMap) + // { + // for (const TPair& Pair : CategoryMap) + // { + // int32 SortOrder = Pair.Value->GetSortOrder(); + // const FName CategoryName = Pair.Key; + // + // if (CategoryName == "Evaluation") + // { + // SortOrder += 1; + // } + // else if (CategoryName == "Settings") + // { + // SortOrder += 2; + // } + // else if (CategoryName == "Debug") + // { + // SortOrder += 3; + // } + // else + // { + // const int32 ValueSortOrder = Pair.Value->GetSortOrder(); + // if (ValueSortOrder >= SortOrder && ValueSortOrder < SortOrder + 10) + // { + // SortOrder += 10; + // } + // else + // { + // continue; + // } + // } + // + // Pair.Value->SetSortOrder(SortOrder); + // } + // }); + // + // TSharedRef NodeHandle = DetailBuilder.GetProperty(FName(TEXT("Node")), GetClass()); + // + // if (Node.Mode == EWarpingEvaluationMode::Graph) + // { + // DetailBuilder.HideProperty(NodeHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGMS_AnimNode_OrientationWarping, OrientationAngle))); + // } + // + // if (Node.Mode == EWarpingEvaluationMode::Manual) + // { + // DetailBuilder.HideProperty(NodeHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngle))); + // DetailBuilder.HideProperty(NodeHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngleDeltaThreshold))); + // DetailBuilder.HideProperty(NodeHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FGMS_AnimNode_OrientationWarping, MinRootMotionSpeedThreshold))); + // } +} + +void UGMS_AnimGraphNode_OrientationWarping::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + Super::PostEditChangeProperty(PropertyChangedEvent); + + return; //直接返回,编辑器模式下不根据配置情况动态隐藏/显示属性 + + // bool bRequiresNodeReconstruct = false; + // FProperty* ChangedProperty = PropertyChangedEvent.Property; + // + // if (ChangedProperty) + // { + // // Evaluation mode + // if (ChangedProperty->GetFName() == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, Mode)) + // { + // FScopedTransaction Transaction(LOCTEXT("ChangeEvaluationMode", "Change Evaluation Mode")); + // Modify(); + // + // // Break links to pins going away + // for (int32 PinIndex = 0; PinIndex < Pins.Num(); ++PinIndex) + // { + // UEdGraphPin* Pin = Pins[PinIndex]; + // if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, OrientationAngle)) + // { + // if (Node.Mode == EWarpingEvaluationMode::Graph) + // { + // Pin->BreakAllPinLinks(); + // } + // } + // else if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngle)) + // { + // if (Node.Mode == EWarpingEvaluationMode::Manual) + // { + // Pin->BreakAllPinLinks(); + // } + // } + // else if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, LocomotionAngleDeltaThreshold)) + // { + // if (Node.Mode == EWarpingEvaluationMode::Manual) + // { + // Pin->BreakAllPinLinks(); + // } + // } + // else if (Pin->PinName == GET_MEMBER_NAME_STRING_CHECKED(FGMS_AnimNode_OrientationWarping, MinRootMotionSpeedThreshold)) + // { + // if (Node.Mode == EWarpingEvaluationMode::Manual) + // { + // Pin->BreakAllPinLinks(); + // } + // } + // } + // + // bRequiresNodeReconstruct = true; + // } + // } + // + // if (bRequiresNodeReconstruct) + // { + // ReconstructNode(); + // FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint()); + // } +} + +void UGMS_AnimGraphNode_OrientationWarping::GetInputLinkAttributes(FNodeAttributeArray& OutAttributes) const +{ + if (Node.Mode == EWarpingEvaluationMode::Graph) + { + OutAttributes.Add(UE::Anim::IAnimRootMotionProvider::AttributeName); + } +} + +void UGMS_AnimGraphNode_OrientationWarping::GetOutputLinkAttributes(FNodeAttributeArray& OutAttributes) const +{ + if (Node.Mode == EWarpingEvaluationMode::Graph) + { + OutAttributes.Add(UE::Anim::IAnimRootMotionProvider::AttributeName); + } +} + +void UGMS_AnimGraphNode_OrientationWarping::ValidateAnimNodeDuringCompilation(USkeleton* ForSkeleton, FCompilerResultsLog& MessageLog) +{ + return Super::ValidateAnimNodeDuringCompilation(ForSkeleton, MessageLog); //直接返回,编辑器模式下不做任何校验。 + // auto HasInvalidBoneName = [](const FName& BoneName) + // { + // return BoneName == NAME_None; + // }; + // + // auto HasInvalidBoneIndex = [&](const FName& BoneName) + // { + // return ForSkeleton && ForSkeleton->GetReferenceSkeleton().FindBoneIndex(BoneName) == INDEX_NONE; + // }; + // + // auto InvalidBoneNameMessage = [&](const FName& BoneName) + // { + // FFormatNamedArguments Args; + // Args.Add(TEXT("BoneName"), FText::FromName(BoneName)); + // const FText Message = FText::Format(NSLOCTEXT("OrientationWarping", "Invalid{BoneName}BoneName", "@@ - {BoneName} bone not found in Skeleton"), Args); + // MessageLog.Warning(*Message.ToString(), this); + // }; + // + // auto InvalidBoneIndexMessage = [&](const FName& BoneName) + // { + // FFormatNamedArguments Args; + // Args.Add(TEXT("BoneName"), FText::FromName(BoneName)); + // const FText Message = FText::Format(NSLOCTEXT("OrientationWarping", "Invalid{BoneName}BoneInSkeleton", "@@ - {BoneName} bone definition is required"), Args); + // MessageLog.Warning(*Message.ToString(), this); + // }; + // + // if (Node.RotationAxis == EAxis::None) + // { + // MessageLog.Warning(*NSLOCTEXT("OrientationWarping", "InvalidRotationAxis", "@@ - Rotation Axis choice of X, Y, or Z is required").ToString(), this); + // } + // + // if (Node.SpineBones.IsEmpty()) + // { + // MessageLog.Warning(*NSLOCTEXT("OrientationWarping", "InvalidSpineBones", "@@ - Spine bone definitions are required").ToString(), this); + // } + // else + // { + // for (const auto& Bone : Node.SpineBones) + // { + // if (HasInvalidBoneName(Bone.BoneName)) + // { + // InvalidBoneIndexMessage("Spine"); + // } + // else if (HasInvalidBoneIndex(Bone.BoneName)) + // { + // InvalidBoneNameMessage(Bone.BoneName); + // } + // } + // } + // + // if (HasInvalidBoneName(Node.IKFootRootBone.BoneName)) + // { + // InvalidBoneIndexMessage("IK Foot Root"); + // } + // else if (HasInvalidBoneIndex(Node.IKFootRootBone.BoneName)) + // { + // InvalidBoneNameMessage(Node.IKFootRootBone.BoneName); + // } + // + // if (Node.SpineBones.IsEmpty()) + // { + // MessageLog.Warning(*NSLOCTEXT("OrientationWarping", "InvalidIKFootBones", "@@ - IK Foot bone definitions are required").ToString(), this); + // } + // else + // { + // for (const auto& Bone : Node.IKFootBones) + // { + // if (HasInvalidBoneName(Bone.BoneName)) + // { + // InvalidBoneIndexMessage("IK Foot"); + // } + // else if (HasInvalidBoneIndex(Bone.BoneName)) + // { + // InvalidBoneNameMessage(Bone.BoneName); + // } + // } + // } + // + // Super::ValidateAnimNodeDuringCompilation(ForSkeleton, MessageLog); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Blutility/GMS_EditorUtilityLibrary.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Blutility/GMS_EditorUtilityLibrary.h new file mode 100644 index 0000000..ab95437 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Blutility/GMS_EditorUtilityLibrary.h @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GMS_EditorUtilityLibrary.generated.h" + +class UAnimationModifier; +/** + * + */ +UCLASS(Blueprintable) +class GENERICMOVEMENTEDITOR_API UGMS_EditorUtilityLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + UFUNCTION(BlueprintCallable, Category="GMS|Development|Editor") + static void RevertAnimModifierOfClass(TSubclassOf ModifierClass); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Development|Editor") + static float GetSamplingFrameRate(const UAnimSequence* AnimSequence); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Development|Editor") + static TArray GetAllCurveNames(const UAnimSequence* AnimSequence); +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_AssetTypeActions.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_AssetTypeActions.h new file mode 100644 index 0000000..7fce856 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_AssetTypeActions.h @@ -0,0 +1,33 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "AssetTypeActions/AssetTypeActions_DataAsset.h" + +class FGMS_AssetTypeAction : public FAssetTypeActions_DataAsset +{ +public: + virtual uint32 GetCategories() override; + virtual FColor GetTypeColor() const override; +}; + +#define DEFINE_GMS_ASSET_ACTION(ActionName) \ +class FGMS_AssetTypeAction_##ActionName final : public FGMS_AssetTypeAction \ +{ \ +public: \ +virtual FText GetName() const override; \ +virtual FText GetAssetDescription(const FAssetData& AssetData) const override; \ +virtual UClass* GetSupportedClass() const override; \ +}; + +DEFINE_GMS_ASSET_ACTION(MovementDefinition) + +DEFINE_GMS_ASSET_ACTION(MovementControlSetting_Default) + +DEFINE_GMS_ASSET_ACTION(AnimGraphSetting) + +DEFINE_GMS_ASSET_ACTION(AnimLayerSetting_States_Default) + +DEFINE_GMS_ASSET_ACTION(AnimLayerSetting_Overlay_PoseStack) + +DEFINE_GMS_ASSET_ACTION(AnimLayerSetting_Overlay_SequenceStack) diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_DataAssetsFactories.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_DataAssetsFactories.h new file mode 100644 index 0000000..d092aef --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Factories/GMS_DataAssetsFactories.h @@ -0,0 +1,89 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "UObject/ObjectMacros.h" +#include "Templates/SubclassOf.h" +#include "Factories/Factory.h" +#include "GMS_DataAssetsFactories.generated.h" + +UCLASS(Abstract) +class UGMS_Factory : public UFactory +{ + GENERATED_BODY() + +public: + UGMS_Factory(const FObjectInitializer& ObjectInitializer); + virtual uint32 GetMenuCategories() const override; + virtual const TArray& GetMenuCategorySubMenus() const override; +}; + +#define DEFINE_GMS_FACTORY(FactoryName) \ +UCLASS() \ +class UGMS_Factory_##FactoryName : public UGMS_Factory \ +{ \ +GENERATED_BODY() \ +public: \ +UGMS_Factory_##FactoryName(const FObjectInitializer& ObjectInitializer); \ +virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; \ +}; + +UCLASS() +class UGMS_Factory_MovementDefinition : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_MovementDefinition(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGMS_Factory_MovementControlSetting_Default : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_MovementControlSetting_Default(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGMS_Factory_AnimGraphSetting : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_AnimGraphSetting(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGMS_Factory_AnimLayerSetting_States_Default : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_AnimLayerSetting_States_Default(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGMS_Factory_AnimLayerSetting_Overlay_PoseStack : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_AnimLayerSetting_Overlay_PoseStack(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; + +UCLASS() +class UGMS_Factory_AnimLayerSetting_Overlay_SequenceStack : public UGMS_Factory +{ + GENERATED_BODY() + +public: + UGMS_Factory_AnimLayerSetting_Overlay_SequenceStack(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_CurvesBlend.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_CurvesBlend.h new file mode 100644 index 0000000..4e66a2b --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_CurvesBlend.h @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AnimGraphNode_Base.h" +#include "Nodes/GMS_AnimNode_CurvesBlend.h" +#include "UObject/Object.h" +#include "GMS_AnimGraphNode_CurvesBlend.generated.h" + + +UCLASS() +class GENERICMOVEMENTEDITOR_API UGMS_AnimGraphNode_CurvesBlend : public UAnimGraphNode_Base +{ + GENERATED_BODY() + +protected: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings") + FGMS_AnimNode_CurvesBlend Node; + +public: + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + + virtual FText GetTooltipText() const override; + + virtual FString GetNodeCategory() const override; +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.h new file mode 100644 index 0000000..1d05aa9 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_GameplayTagsBlend.h @@ -0,0 +1,35 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "AnimGraphNode_BlendListBase.h" +#include "Nodes/GMS_AnimNode_GameplayTagsBlend.h" +#include "GMS_AnimGraphNode_GameplayTagsBlend.generated.h" + +UCLASS() +class GENERICMOVEMENTEDITOR_API UGMS_AnimGraphNode_GameplayTagsBlend : public UAnimGraphNode_BlendListBase +{ + GENERATED_BODY() + +protected: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings") + FGMS_AnimNode_GameplayTagsBlend Node; + +public: + UGMS_AnimGraphNode_GameplayTagsBlend(); + + virtual void PostEditChangeProperty(FPropertyChangedEvent &PropertyChangedEvent) override; + + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + + virtual FText GetTooltipText() const override; + + virtual void ReallocatePinsDuringReconstruction(TArray &PreviousPins) override; + + virtual FString GetNodeCategory() const override; + + virtual void CustomizePinData(UEdGraphPin *Pin, FName SourcePropertyName, int32 PinIndex) const override; + +protected: + static void GetBlendPinProperties(const UEdGraphPin *Pin, bool &bBlendPosePin, bool &bBlendTimePin); +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.h new file mode 100644 index 0000000..d2c0d2b --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_LayeredBoneBlend.h @@ -0,0 +1,44 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "AnimGraphNode_BlendListBase.h" +#include "Nodes/GMS_AnimNode_LayeredBoneBlend.h" +#include "GMS_AnimGraphNode_LayeredBoneBlend.generated.h" + +UCLASS(MinimalAPI) +class UGMS_AnimGraphNode_LayeredBoneBlend : public UAnimGraphNode_BlendListBase +{ + GENERATED_UCLASS_BODY() + UPROPERTY(EditAnywhere, Category=Settings) + FGMS_AnimNode_LayeredBoneBlend Node; + + // Adds a new pose pin + //@TODO: Generalize this behavior (returning a list of actions/delegates maybe?) + GENERICMOVEMENTEDITOR_API virtual void AddPinToBlendByFilter(); + GENERICMOVEMENTEDITOR_API virtual void RemovePinFromBlendByFilter(UEdGraphPin* Pin); + + // UObject interface + // End of UObject interface + + // UEdGraphNode interface + virtual FLinearColor GetNodeTitleColor() const override; + virtual FText GetTooltipText() const override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + // End of UEdGraphNode interface + + // UK2Node interface + virtual void GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const override; + // End of UK2Node interface + + // UAnimGraphNode_Base interface + virtual FString GetNodeCategory() const override; + // End of UAnimGraphNode_Base interface + + + // Gives each visual node a chance to validate that they are still valid in the context of the compiled class, giving a last shot at error or warning generation after primary compilation is finished + virtual void ValidateAnimNodeDuringCompilation(class USkeleton* ForSkeleton, class FCompilerResultsLog& MessageLog) override; + // End of UAnimGraphNode_Base interface +}; diff --git a/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_OrientationWarping.h b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_OrientationWarping.h new file mode 100644 index 0000000..2026a43 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementEditor/Public/Nodes/GMS_AnimGraphNode_OrientationWarping.h @@ -0,0 +1,38 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once +#include "AnimGraphNode_SkeletalControlBase.h" +#include "Nodes/GMS_AnimNode_OrientationWarping.h" +#include "GMS_AnimGraphNode_OrientationWarping.generated.h" + +UCLASS() +class UGMS_AnimGraphNode_OrientationWarping : public UAnimGraphNode_SkeletalControlBase +{ + GENERATED_UCLASS_BODY() + + UPROPERTY(EditAnywhere, Category="Settings") + FGMS_AnimNode_OrientationWarping Node; + +public: + // UEdGraphNode interface + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual FLinearColor GetNodeTitleColor() const override; + virtual FText GetTooltipText() const override; + virtual FString GetNodeCategory() const override; + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + virtual void GetInputLinkAttributes(FNodeAttributeArray& OutAttributes) const override; + virtual void GetOutputLinkAttributes(FNodeAttributeArray& OutAttributes) const override; + virtual void ValidateAnimNodeDuringCompilation(USkeleton* ForSkeleton, FCompilerResultsLog& MessageLog) override; + // End of UEdGraphNode interface + + protected: + // UAnimGraphNode_Base interface + virtual void CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const override; + // End of UAnimGraphNode_Base interface + + // UAnimGraphNode_SkeletalControlBase interface + virtual FText GetControllerDescription() const override; + virtual const FAnimNode_SkeletalControlBase* GetNode() const override { return &Node; } + // End of UAnimGraphNode_SkeletalControlBase interface +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/GenericMovementSystem.Build.cs b/Plugins/GMS/Source/GenericMovementSystem/GenericMovementSystem.Build.cs new file mode 100644 index 0000000..546ef3d --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/GenericMovementSystem.Build.cs @@ -0,0 +1,43 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +using UnrealBuildTool; + +public class GenericMovementSystem : ModuleRules +{ + public GenericMovementSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + CppCompileWarningSettings.NonInlinedGenCppWarningLevel = WarningLevel.Warning; + + PublicDependencyModuleNames.AddRange( + new[] + { + "GameplayTags", + "AnimationWarpingRuntime", + "Chooser", "PoseSearch", "Mover" + // ... add other public dependencies that you statically link with here ... + } + ); + + PrivateDependencyModuleNames.AddRange( + new[] + { + "Core", + "CoreUObject", + "NetCore", + "ModularGameplay", + "EngineSettings", + "Engine", + "AnimGraphRuntime", + "BlendStack", + "AnimationLocomotionLibraryRuntime", + "Niagara", + "DeveloperSettings" + // ... add private dependencies that you statically link with here ... + } + ); + + if (Target.Type == TargetRules.TargetType.Editor) PrivateDependencyModuleNames.AddRange(new[] { "MessageLog" }); + } +} \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_CharacterMovementSystemComponent.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_CharacterMovementSystemComponent.cpp new file mode 100644 index 0000000..198a011 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_CharacterMovementSystemComponent.cpp @@ -0,0 +1,1083 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GMS_CharacterMovementSystemComponent.h" +#include "TimerManager.h" +#include "Components/CapsuleComponent.h" +#include "Curves/CurveFloat.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/PlayerController.h" +#include "Kismet/KismetMathLibrary.h" +#include "Net/UnrealNetwork.h" +#include "Components/SkeletalMeshComponent.h" +#include "Animation/AnimInstance.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Net/Core/PushModel/PushModel.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "Utility/GMS_Constants.h" +#include "Utility/GMS_Log.h" +#include "Utility/GMS_Math.h" +#include "Utility/GMS_Rotation.h" +#include "Utility/GMS_Utility.h" +#include "Utility/GMS_Vector.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_CharacterMovementSystemComponent) + +namespace GMS_MovementComponentConstants +{ + inline static constexpr auto TeleportDistanceThresholdSquared{FMath::Square(50.0f)}; +} + +UGMS_CharacterMovementSystemComponent::UGMS_CharacterMovementSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + SetIsReplicatedByDefault(true); + PrimaryComponentTick.bCanEverTick = true; + bWantsInitializeComponent = true; + bReplicateUsingRegisteredSubObjectList = true; + + MovementIntent = FVector::ZeroVector; + + MovementModeToTagMapping = { + {MOVE_None, GMS_MovementModeTags::None}, + {MOVE_Walking, GMS_MovementModeTags::Grounded}, + {MOVE_NavWalking, GMS_MovementModeTags::Grounded}, + {MOVE_Falling, GMS_MovementModeTags::InAir}, + {MOVE_Swimming, GMS_MovementModeTags::Swimming}, + {MOVE_Flying, GMS_MovementModeTags::Flying}, + }; +} + +void UGMS_CharacterMovementSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + Parameters.Condition = COND_SkipOwner; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, DesiredMovementState, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, DesiredRotationMode, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, MovementIntent, Parameters) +} + +void UGMS_CharacterMovementSystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + OwnerCharacter = Cast(GetOwner()); + if (OwnerCharacter) + { + MovementState = DesiredMovementState; + RotationMode = DesiredRotationMode; + + CharacterMovement = Cast(OwnerCharacter->GetMovementComponent()); + OwnerCharacter->bUseControllerRotationPitch = false; + OwnerCharacter->bUseControllerRotationRoll = false; + OwnerCharacter->bUseControllerRotationYaw = false; + CharacterMovement->bUseControllerDesiredRotation = false; + CharacterMovement->bOrientRotationToMovement = false; + + AnimationInstance = GetMesh()->GetAnimInstance(); + // Make sure the mesh and animation blueprint are ticking after the character so they can access the most up-to-date character state. + GetMesh()->AddTickPrerequisiteComponent(this); + RefreshTargetYawAngleUsingActorRotation(); + } +} + + +void UGMS_CharacterMovementSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + //This callback fires on both sides, including simulated proxy. + OwnerCharacter->MovementModeChangedDelegate.AddDynamic(this, &ThisClass::OnCharacterMovementModeChanged); + OnCharacterMovementModeChanged(OwnerCharacter, OwnerCharacter->GetCharacterMovement()->GetGroundMovementMode(), 0); + + RefreshRotationMode(); + + RefreshMovementSetSetting(); +} + +void UGMS_CharacterMovementSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (IsValid(OwnerCharacter)) + { + OwnerCharacter->MovementModeChangedDelegate.RemoveDynamic(this, &ThisClass::OnCharacterMovementModeChanged); + } + Super::EndPlay(EndPlayReason); +} + +void UGMS_CharacterMovementSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_CharacterMovementSystemComponent::TickComponent"), STAT_GMS_MovementSystem_Tick, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + if (!GetMovementDefinition().IsValid() || !IsValid(AnimationInstance) || !IsValid(ControlSetting)) + { + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + return; + } + + RefreshMovementBase(); + + RefreshInput(DeltaTime); + + RefreshLocomotionEarly(); + + RefreshView(DeltaTime); + + RefreshRotationMode(); + + RefreshLocomotion(DeltaTime); + + RefreshDynamicMovementState(); + + RefreshMovementState(); + + RefreshRotation(DeltaTime); + + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + RefreshLocomotionLate(DeltaTime); +} + +void UGMS_CharacterMovementSystemComponent::OnCharacterMovementModeChanged(ACharacter* InCharacter, EMovementMode PrevMovementMode, uint8 PreviousCustomMode) +{ + // Use the character movement mode to set the locomotion mode to the right value. This allows you to have a + // custom set of movement modes but still use the functionality of the default character movement component. + + EMovementMode CharMovementMode = CharacterMovement->MovementMode; + uint8 CharCustomMovementMode = CharacterMovement->CustomMovementMode; + + if (CharMovementMode != MOVE_Custom) + { + if (MovementModeToTagMapping.Contains(CharMovementMode) && MovementModeToTagMapping[CharMovementMode].IsValid()) + { + SetLocomotionMode(MovementModeToTagMapping[CharMovementMode]); + } + else + { + GMS_CLOG(Error, "No locomotion mode mapping for MovementMode:%s", *UEnum::GetDisplayValueAsText(CharMovementMode).ToString()); + } + } + else + { + if (CustomMovementModeToTagMapping.Contains(CharCustomMovementMode) && CustomMovementModeToTagMapping[CharCustomMovementMode].IsValid()) + { + SetLocomotionMode(CustomMovementModeToTagMapping[CharCustomMovementMode]); + } + else + { + GMS_CLOG(Error, "No locomotion mode mapping for CustomMovementMode:%d", CharCustomMovementMode); + } + } +} + +void UGMS_CharacterMovementSystemComponent::OnReplicated_LocomotionMode(const FGameplayTag& PreviousLocomotionMode) +{ + // OnMovementModeChanged will fire on all clients(including simulated pawn), mover is not. so don't call parent. +} + +void UGMS_CharacterMovementSystemComponent::RefreshMovementState() +{ + if (!DesiredMovementState.IsValid()) + { + return; + } + + if (MovementState == DesiredMovementState) + { + return; + } + + ApplyMovementSetting(); + + SetMovementState(CalculateActualMovementState()); +} + +FGameplayTag UGMS_CharacterMovementSystemComponent::CalculateActualMovementState() +{ + check(GetNumOfMovementStateSettings() != 0) + + if (ControlSetting->MovementStates.Num() == 1) + { + return ControlSetting->MovementStates[0].Tag; + } + + for (int32 i = 0; i < ControlSetting->MovementStates.Num(); i++) + { + float Speed = ControlSetting->MovementStates[i].Speed; + if (Speed > 0.0f && LocomotionState.Speed < Speed + 10.0f) + { + return ControlSetting->MovementStates[i].Tag; + } + } + + return FGameplayTag::EmptyTag; +} + +void UGMS_CharacterMovementSystemComponent::ApplyMovementSetting() +{ + if (bAllowRefreshCharacterMovementSettings && IsValid(ControlSetting)) + { + if (const FGMS_MovementStateSetting* TempMS = ControlSetting->GetMovementStateSetting(DesiredMovementState, true)) + { + CharacterMovement->MaxWalkSpeed = TempMS->Speed; + CharacterMovement->MaxAcceleration = TempMS->Acceleration; + CharacterMovement->BrakingDecelerationWalking = TempMS->BrakingDeceleration; + CharacterMovement->MaxWalkSpeedCrouched = TempMS->Speed; + } + } +} + +void UGMS_CharacterMovementSystemComponent::SetDesiredMovement(const FGameplayTag& NewDesiredMovement) +{ + SetDesiredMovement(NewDesiredMovement, true); +} + +void UGMS_CharacterMovementSystemComponent::SetMovementState(const FGameplayTag& NewMovementState) +{ + if (NewMovementState.IsValid() && MovementState != NewMovementState) + { + const FGameplayTag PreviousMovementState{MovementState}; + + MovementState = NewMovementState; + + OnMovementStateChanged(PreviousMovementState); + } +} + +const FGameplayTag& UGMS_CharacterMovementSystemComponent::GetMovementState() const +{ + return MovementState; +} + +void UGMS_CharacterMovementSystemComponent::SetDesiredMovement(const FGameplayTag& NewDesiredMovement, bool bSendRpc) +{ + if (DesiredMovementState == NewDesiredMovement || GetOwner()->GetLocalRole() < ROLE_AutonomousProxy) + { + return; + } + const auto PreviousMovement{DesiredMovementState}; + + DesiredMovementState = NewDesiredMovement; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, DesiredMovementState, this) + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientSetDesiredMovement(NewDesiredMovement); + } + else + { + ServerSetDesiredMovement(NewDesiredMovement); + } + } +} + +void UGMS_CharacterMovementSystemComponent::ClientSetDesiredMovement_Implementation(const FGameplayTag& NewDesiredMovement) +{ + SetDesiredMovement(NewDesiredMovement, false); +} + +void UGMS_CharacterMovementSystemComponent::ServerSetDesiredMovement_Implementation(const FGameplayTag& NewDesiredMovement) +{ + SetDesiredMovement(NewDesiredMovement, false); +} + +const FGameplayTag& UGMS_CharacterMovementSystemComponent::GetDesiredRotationMode() const +{ + return DesiredRotationMode; +} + +void UGMS_CharacterMovementSystemComponent::SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode) +{ + SetDesiredRotationMode(NewDesiredRotationMode, true); +} + +const FGameplayTag& UGMS_CharacterMovementSystemComponent::GetRotationMode() const +{ + return RotationMode; +} + +void UGMS_CharacterMovementSystemComponent::SetRotationMode(const FGameplayTag& NewRotationMode) +{ + if (RotationMode != NewRotationMode) + { + if (bRespectAllowedRotationModesSettings && !GetMovementStateSetting().AllowedRotationModes.Contains(NewRotationMode)) + { + GMS_CLOG(Warning, "current movement state(%s) doesn't allow new rotation mode(%s).", *MovementState.ToString(), *NewRotationMode.ToString()); + return; + } + const auto PreviousRotationMode{RotationMode}; + + RotationMode = NewRotationMode; + + OnRotationModeChanged(PreviousRotationMode); + } +} + +void UGMS_CharacterMovementSystemComponent::RefreshRotationMode() +{ + SetRotationMode(DesiredRotationMode); +} + + +void UGMS_CharacterMovementSystemComponent::SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode, bool bSendRpc) +{ + if (DesiredRotationMode == NewDesiredRotationMode || GetOwner()->GetLocalRole() < ROLE_AutonomousProxy) + { + return; + } + + DesiredRotationMode = NewDesiredRotationMode; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, DesiredRotationMode, this) + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientSetDesiredRotationMode(DesiredRotationMode); + } + else + { + ServerSetDesiredRotationMode(DesiredRotationMode); + } + } +} + +void UGMS_CharacterMovementSystemComponent::ClientSetDesiredRotationMode_Implementation(const FGameplayTag& NewDesiredRotationMode) +{ + SetDesiredRotationMode(NewDesiredRotationMode, false); +} + +void UGMS_CharacterMovementSystemComponent::ServerSetDesiredRotationMode_Implementation(const FGameplayTag& NewDesiredRotationMode) +{ + SetDesiredRotationMode(NewDesiredRotationMode, false); +} + + +void UGMS_CharacterMovementSystemComponent::SetMovementIntent(FVector NewMovementIntent) +{ + NewMovementIntent = NewMovementIntent.GetSafeNormal(); + + COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY(ThisClass, MovementIntent, NewMovementIntent, this); +} + +void UGMS_CharacterMovementSystemComponent::RefreshInput(float DeltaTime) +{ + // Using current acceleration as movement input. + if (OwnerCharacter->GetLocalRole() >= ROLE_AutonomousProxy) + { + SetMovementIntent(CharacterMovement->GetCurrentAcceleration() / CharacterMovement->GetMaxAcceleration()); + } + + Super::RefreshInput(DeltaTime); +} + +void UGMS_CharacterMovementSystemComponent::OnRotationModeChanged_Implementation(const FGameplayTag& PreviousRotationMode) +{ + Super::OnRotationModeChanged_Implementation(PreviousRotationMode); + + // if (PreviousRotationMode == GMS_RotationModeTags::VelocityDirection && !LocomotionState.bMoving) + // { + // // This prevents the actor from rotating in the last input direction after the + // // rotation mode has been changed and the actor is not moving at that moment. + // // LocomotionState.InputYawAngle = ViewState.Rotation.Yaw; + // LocomotionState.TargetYawAngle = ViewState.Rotation.Yaw; + // } +} + +void UGMS_CharacterMovementSystemComponent::RefreshMovementBase() +{ + const FBasedMovementInfo& BasedMovement = OwnerCharacter->GetBasedMovement(); + if (BasedMovement.MovementBase != MovementBase.Primitive || BasedMovement.BoneName != MovementBase.BoneName) + { + MovementBase.Primitive = BasedMovement.MovementBase; + MovementBase.BoneName = BasedMovement.BoneName; + MovementBase.bBaseChanged = true; + } + else + { + MovementBase.bBaseChanged = false; + } + + MovementBase.bHasRelativeLocation = BasedMovement.HasRelativeLocation(); + MovementBase.bHasRelativeRotation = MovementBase.bHasRelativeLocation && BasedMovement.bRelativeRotation; + + const auto PreviousRotation{MovementBase.Rotation}; + + MovementBaseUtility::GetMovementBaseTransform(BasedMovement.MovementBase, BasedMovement.BoneName, + MovementBase.Location, MovementBase.Rotation); + + MovementBase.DeltaRotation = MovementBase.bHasRelativeLocation && !MovementBase.bBaseChanged + ? (MovementBase.Rotation * PreviousRotation.Inverse()).Rotator() + : FRotator::ZeroRotator; +} + +const FGameplayTag& UGMS_CharacterMovementSystemComponent::GetDesiredMovementState() const +{ + return DesiredMovementState; +} + +FVector UGMS_CharacterMovementSystemComponent::GetMovementIntent() const +{ + return MovementIntent; +} + +void UGMS_CharacterMovementSystemComponent::RefreshView(const float DeltaTime) +{ + if (MovementBase.bHasRelativeRotation) + { + // Offset the rotations to keep them relative to the movement base. + + ViewState.Rotation.Pitch += MovementBase.DeltaRotation.Pitch; + ViewState.Rotation.Yaw += MovementBase.DeltaRotation.Yaw; + ViewState.Rotation.Normalize(); + } + + ViewState.PreviousYawAngle = UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw); + + // update view/control rotation. + if (MovementBase.bHasRelativeRotation) + { + if (OwnerPawn->IsLocallyControlled()) + { + // We can't depend on the view rotation sent by the character movement component + // since it's in world space, so in this case we always send it ourselves. + + //将相对于平台的视角朝向设置为新的视角朝向。保持视角不变。 + SetReplicatedViewRotation((MovementBase.Rotation.Inverse() * OwnerPawn->GetViewRotation().Quaternion()).Rotator(), true); + } + } + else + { + if (OwnerPawn->IsLocallyControlled() || (OwnerPawn->IsReplicatingMovement() && OwnerPawn->GetLocalRole() >= ROLE_Authority && IsValid(OwnerPawn->GetController()))) + { + // The character movement component already sends the view rotation to the + // server if movement is replicated, so we don't have to do this ourselves. + SetReplicatedViewRotation(OwnerPawn->GetViewRotation().GetNormalized(), !OwnerPawn->IsReplicatingMovement()); + } + } + + // update view rotation based on if movement is based. + ViewState.Rotation = MovementBase.bHasRelativeRotation + ? (MovementBase.Rotation * ReplicatedViewRotation.Quaternion()).Rotator() + : ReplicatedViewRotation; + + // Set the yaw speed by comparing the current and previous view yaw angle, divided by + // delta seconds. This represents the speed the camera is rotating from left to right. + if (DeltaTime > UE_SMALL_NUMBER) + { + ViewState.YawSpeed = FMath::Abs(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - ViewState.PreviousYawAngle)) / DeltaTime; + } +} + +#pragma region Abstraction + +bool UGMS_CharacterMovementSystemComponent::IsCrouching() const +{ + return CharacterMovement->IsCrouching(); +} + +float UGMS_CharacterMovementSystemComponent::GetMaxSpeed() const +{ + return CharacterMovement->GetMaxSpeed(); +} + +float UGMS_CharacterMovementSystemComponent::GetScaledCapsuleRadius() const +{ + return OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleRadius(); +} + +float UGMS_CharacterMovementSystemComponent::GetScaledCapsuleHalfHeight() const +{ + return OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); +} + +float UGMS_CharacterMovementSystemComponent::GetMaxAcceleration() const +{ + return CharacterMovement->GetMaxAcceleration(); +} + +float UGMS_CharacterMovementSystemComponent::GetMaxBrakingDeceleration() const +{ + return CharacterMovement->GetMaxBrakingDeceleration(); +} + +float UGMS_CharacterMovementSystemComponent::GetWalkableFloorZ() const +{ + return CharacterMovement->GetWalkableFloorZ(); +} + +float UGMS_CharacterMovementSystemComponent::GetGravityZ() const +{ + return CharacterMovement->GetGravityZ(); +} + +USkeletalMeshComponent* UGMS_CharacterMovementSystemComponent::GetMesh() const +{ + return OwnerCharacter ? OwnerCharacter->GetMesh() : nullptr; +} + +bool UGMS_CharacterMovementSystemComponent::IsMovingOnGround() const +{ + return CharacterMovement->IsMovingOnGround(); +} + +#pragma endregion + +#pragma region Locomotion +void UGMS_CharacterMovementSystemComponent::RefreshLocomotionEarly() +{ + if (!LocomotionState.bMoving && + RotationMode == GMS_RotationModeTags::VelocityDirection && + ControlSetting->VelocityDirectionSetting.Get().bInheritBaseRotation) + { + DesiredVelocityYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + DesiredVelocityYawAngle + MovementBase.DeltaRotation.Yaw)); + + LocomotionState.VelocityYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + LocomotionState.VelocityYawAngle + MovementBase.DeltaRotation.Yaw)); + } + + if (MovementBase.bHasRelativeLocation) + { + // Offset the rotations (the actor's rotation too) to keep them relative to the movement base. + + LocomotionState.TargetYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + LocomotionState.TargetYawAngle + MovementBase.DeltaRotation.Yaw)); + + LocomotionState.ViewRelativeTargetYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + LocomotionState.ViewRelativeTargetYawAngle + MovementBase.DeltaRotation.Yaw)); + + LocomotionState.SmoothTargetYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + LocomotionState.SmoothTargetYawAngle + MovementBase.DeltaRotation.Yaw)); + + auto NewRotation{OwnerPawn->GetActorRotation()}; + NewRotation.Pitch += MovementBase.DeltaRotation.Pitch; + NewRotation.Yaw += MovementBase.DeltaRotation.Yaw; + NewRotation.Normalize(); + + SetActorRotation(NewRotation); + } + + LocomotionState.bAimingLimitAppliedThisFrame = false; +} + +void UGMS_CharacterMovementSystemComponent::RefreshLocomotion(const float DeltaTime) +{ + const auto bHadVelocity{LocomotionState.bHasVelocity}; + + LocomotionState.Velocity = OwnerPawn->GetVelocity(); + + // Determine if the character is moving by getting its speed. The speed equals the length + // of the horizontal velocity, so it does not take vertical movement into account. If the + // character is moving, update the last velocity rotation. This value is saved because it might + // be useful to know the last orientation of a movement even after the character has stopped. + + LocomotionState.Speed = UE_REAL_TO_FLOAT(LocomotionState.Velocity.Size2D()); + + static constexpr auto HasSpeedThreshold{1.0f}; + + LocomotionState.bHasVelocity = LocomotionState.Speed >= HasSpeedThreshold; + + if (LocomotionState.bHasVelocity) + { + LocomotionState.VelocityYawAngle = UE_REAL_TO_FLOAT(UGMS_Vector::DirectionToAngleXY(LocomotionState.Velocity)); + } + + // Character is moving if has speed and current acceleration, or if the speed is greater than the moving speed threshold. + + LocomotionState.bMoving = (LocomotionState.bHasInput && LocomotionState.bHasVelocity) || + LocomotionState.Speed > ControlSetting->MovingSpeedThreshold; +} + +void UGMS_CharacterMovementSystemComponent::RefreshDynamicMovementState() +{ + if (bAllowRefreshCharacterMovementSettings) + { + return; + } + + if (IsValid(SpeedToMovementStateCurve) && OwnerPawn->HasAuthority()) + { + int32 Index = UKismetMathLibrary::Round(SpeedToMovementStateCurve->GetFloatValue(LocomotionState.Speed)); + FGMS_MovementStateSetting TempSetting; + if (ControlSetting->GetStateByIndex(Index, TempSetting)) + { + SetDesiredMovement(TempSetting.Tag); + } + else + { + GMS_CLOG(Warning, "Found invalid index output from SpeedToMovementStateCurve, Index(%d) of movement definitions can't be found. dynamic adjust movement state failed! Actor:%s", + Index, *GetOwner()->GetName()); + } + } +} + +void UGMS_CharacterMovementSystemComponent::RefreshLocomotionLate(const float DeltaTime) +{ + if (!LocomotionMode.IsValid()) + { + RefreshTargetYawAngleUsingActorRotation(); + } + + LocomotionState.bResetAimingLimit = !LocomotionState.bAimingLimitAppliedThisFrame; +} + +#pragma endregion + +void UGMS_CharacterMovementSystemComponent::RefreshRotation_Implementation(float DeltaTime) +{ + RefreshGroundedRotation(DeltaTime); + RefreshInAirRotation(DeltaTime); +} + +void UGMS_CharacterMovementSystemComponent::RefreshGroundedRotation(const float DeltaTime) +{ + if (LocomotionMode != GMS_MovementModeTags::Grounded || GetGameplayTags().HasAny(GroundedRotationBlockingTags)) + { + return; + } + + if (OwnerCharacter->HasAnyRootMotion()) + { + RefreshTargetYawAngleUsingActorRotation(); + return; + } + + if (!LocomotionState.bMoving) + { + RefreshGroundedNotMovingRotation(DeltaTime); + } + else + { + RefreshGroundedMovingRotation(DeltaTime); + } +} + +void UGMS_CharacterMovementSystemComponent::RefreshGroundedNotMovingRotation(float DeltaTime) +{ + ApplyRotationYawSpeedAnimationCurve(DeltaTime); + + if (RefreshCustomGroundedNotMovingRotation(DeltaTime)) + { + return; + } + + if (RotationMode == GMS_RotationModeTags::ViewDirection) + { + if (const auto* Setting = ControlSetting->ViewDirectionSetting.GetPtr()) + { + if (Setting->bEnableRotationWhenNotMoving) + { + // const auto& TargetYawAngle = LocomotionState.bHasInput ? ViewState.Rotation.Yaw : LocomotionState.TargetYawAngle; + SetRotationExtraSmooth(ViewState.Rotation.Yaw, DeltaTime, Setting->RotationInterpolationSpeed, Setting->TargetYawAngleRotationSpeed); + return; + } + return; + } + + if (const auto* Setting = ControlSetting->ViewDirectionSetting.GetPtr()) + { + // refresh not moving aiming rotation. + { + if (Setting->bEnableRotationWhenNotMoving) // 吸附到视角 + { + SetRotationExtraSmooth(ViewState.Rotation.Yaw, DeltaTime, Setting->RotationInterpolationSpeed, Setting->TargetYawAngleRotationSpeed); + return; + } + + SetTargetYawAngle(ViewState.Rotation.Yaw); + + FRotator NewActorRotation{GetOwner()->GetActorRotation()}; + + //Limit rotation so turn in place can catch up. + if (ConstrainAimingRotation(NewActorRotation, DeltaTime, true)) + { + SetActorRotation(NewActorRotation); + } + } + return; + } + } + + if (RotationMode == GMS_RotationModeTags::VelocityDirection) + { + if (const auto* Setting = ControlSetting->VelocityDirectionSetting.GetPtr()) + { + if (Setting->bEnableRotationWhenNotMoving) + { + SetRotationExtraSmooth(LocomotionState.TargetYawAngle, DeltaTime, Setting->RotationInterpolationSpeed, Setting->TargetYawAngleRotationSpeed); + return; + } + } + + if (const auto* Setting = ControlSetting->VelocityDirectionSetting.GetPtr()) + { + if (Setting->bEnableRotationWhenNotMoving) + { + SetRotationInstant(DesiredVelocityYawAngle, ETeleportType::None); + return; + } + } + } + + RefreshTargetYawAngleUsingActorRotation(); +} + +float UGMS_CharacterMovementSystemComponent::CalculateGroundedMovingRotationInterpolationSpeed(TObjectPtr InterpolationSpeedCurve, float Default) const +{ + // Calculate the rotation speed by using the rotation speed curve in the rotation settings. Using + // the curve in conjunction with the speed amount gives you a high level of control over the rotation + // rates for each speed. Increase the speed if the camera is rotating quickly for more responsive rotation. + + const auto InterpolationHalfLife{ + IsValid(InterpolationSpeedCurve) + ? InterpolationSpeedCurve->GetFloatValue(FMath::Max(1.0f, GetMappedMovementSpeedLevel(UE_REAL_TO_FLOAT(LocomotionState.Velocity.Size2D())))) + : Default + }; + + static constexpr auto MinInterpolationHalfLifeMultiplier{0.333333f}; + static constexpr auto ReferenceViewYawSpeed{300.0f}; + + return InterpolationHalfLife * UGMS_Math::LerpClamped(1.0f, MinInterpolationHalfLifeMultiplier, + ViewState.YawSpeed / ReferenceViewYawSpeed); +} + +void UGMS_CharacterMovementSystemComponent::RefreshGroundedMovingRotation(float DeltaTime) +{ + // Moving. + if (RefreshCustomGroundedMovingRotation(DeltaTime)) + { + return; + } + + if (RotationMode == GMS_RotationModeTags::ViewDirection) + { + if (const auto* Setting = ControlSetting->ViewDirectionSetting.GetPtr()) + { + //TODO maybe use root yaw offset here? + float RotationYawOffset = AnimationInstance->GetCurveValue(UGMS_Constants::RotationYawOffsetCurveName()); + const auto& TargetYawAngle = UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw + RotationYawOffset); + GMS_CLOG(VeryVerbose, "rotating to target yaw angle(%f) with Yaw offset(%f)", TargetYawAngle, RotationYawOffset); + + const float CalculatedRotationInterpolationSpeed = CalculateGroundedMovingRotationInterpolationSpeed(Setting->RotationInterpolationSpeedCurve, Setting->RotationInterpolationSpeed); + SetRotationExtraSmooth(TargetYawAngle, DeltaTime, CalculatedRotationInterpolationSpeed, Setting->TargetYawAngleRotationSpeed); + return; + } + + if (const auto* Setting = ControlSetting->ViewDirectionSetting.GetPtr()) + { + //TODO Should use this if using root yaw offset? + FRotator NewActorRotation{GetOwner()->GetActorRotation()}; + + SetTargetYawAngleSmooth(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw), DeltaTime, Setting->TargetYawAngleRotationSpeed); + + GMS_CLOG(VeryVerbose, "wants to aiming to target yaw angle(%f) with view yaw(%f)", NewActorRotation.Yaw, ViewState.Rotation.Yaw); + + NewActorRotation.Yaw = UGMS_Rotation::DamperExactAngle( + UE_REAL_TO_FLOAT(FMath::UnwindDegrees(NewActorRotation.Yaw)), LocomotionState.SmoothTargetYawAngle, DeltaTime, Setting->RotationInterpolationSpeed); + if (ConstrainAimingRotation(NewActorRotation, DeltaTime)) + { + // Cancel the extra smooth rotation, otherwise the actor will rotate too weirdly. + LocomotionState.SmoothTargetYawAngle = LocomotionState.TargetYawAngle; + } + + GMS_CLOG(VeryVerbose, "aiming to target yaw angle(%f)", NewActorRotation.Yaw); + + SetActorRotation(NewActorRotation); + + return; + } + } + + if (RotationMode == GMS_RotationModeTags::VelocityDirection) + { + if (const auto* Setting = ControlSetting->VelocityDirectionSetting.GetPtr()) + { + SetRotationInstant(DesiredVelocityYawAngle, ETeleportType::None); + return; + } + + if (const auto* Setting = ControlSetting->VelocityDirectionSetting.GetPtr()) + { + if (LocomotionState.bHasInput) + { + const auto& TargetRotation = Setting->bOrientateToMoveInputIntent ? LocomotionState.InputYawAngle : LocomotionState.VelocityYawAngle; + const float CalculatedRotationInterpolationSpeed = CalculateGroundedMovingRotationInterpolationSpeed(Setting->RotationInterpolationSpeedCurve, Setting->RotationInterpolationSpeed); + GMS_CLOG(VeryVerbose, "rotating to target yaw angle(%f) at rotation speed(%f)", TargetRotation, CalculatedRotationInterpolationSpeed); + SetRotationExtraSmooth(TargetRotation, DeltaTime, CalculatedRotationInterpolationSpeed, Setting->TargetYawAngleRotationSpeed); + } + return; + } + } + + RefreshTargetYawAngleUsingActorRotation(); +} + +bool UGMS_CharacterMovementSystemComponent::ConstrainAimingRotation(FRotator& ActorRotation, float DeltaTime, bool bApplySecondaryConstraint) +{ + const FGMS_ViewDirectionSetting_Aiming* Setting = ControlSetting->ViewDirectionSetting.GetPtr(); + if (Setting == nullptr) + { + return false; + } + + LocomotionState.bAimingLimitAppliedThisFrame = true; + + if (LocomotionState.bResetAimingLimit) + { + LocomotionState.AimingYawAngleLimit = 180.0f; + } + + // Limit the actor's rotation when aiming to prevent situations where the lower body noticeably + // fails to keep up with the rotation of the upper body when the camera is rotating very fast. + + float ViewRelativeAngle{FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - ActorRotation.Yaw))}; + + if (FMath::Abs(ViewRelativeAngle) <= Setting->MinAimingYawAngleLimit + UE_KINDA_SMALL_NUMBER) + { + LocomotionState.AimingYawAngleLimit = Setting->MinAimingYawAngleLimit; + return false; + } + + ViewRelativeAngle = UGMS_Rotation::RemapAngleForCounterClockwiseRotation(ViewRelativeAngle); + + // Secondary constraint. Simply increases the actor's rotation speed. Typically only used when the actor is standing still. + + if (bApplySecondaryConstraint) + { + static constexpr auto RotationInterpolationHalfLife{0.1f}; + + // Interpolate the angle only to the point where the constraints no longer apply to ensure a smoother completion of the rotation. + + const auto TargetViewRelativeAngle{ + FMath::Clamp(ViewRelativeAngle, -Setting->MinAimingYawAngleLimit, + Setting->MinAimingYawAngleLimit) + }; + + + const auto DeltaAngle{FMath::UnwindDegrees(TargetViewRelativeAngle - ViewRelativeAngle)}; + + if (FMath::IsNearlyZero(DeltaAngle, UE_KINDA_SMALL_NUMBER)) + { + ViewRelativeAngle = TargetViewRelativeAngle; + } + else + { + const auto InterpolationAmount{UGMS_Math::DamperExactAlpha(DeltaTime, RotationInterpolationHalfLife)}; + ViewRelativeAngle = FMath::UnwindDegrees(ViewRelativeAngle + DeltaAngle * InterpolationAmount); + } + } + + // Primary constraint. Prevents the actor from rotating beyond a certain angle relative to the camera. + + if (FMath::Abs(ViewRelativeAngle) > LocomotionState.AimingYawAngleLimit + UE_KINDA_SMALL_NUMBER) + { + ViewRelativeAngle = FMath::Clamp(ViewRelativeAngle, -LocomotionState.AimingYawAngleLimit, LocomotionState.AimingYawAngleLimit); + } + else + { + LocomotionState.AimingYawAngleLimit = FMath::Max(FMath::Abs(ViewRelativeAngle), Setting->MinAimingYawAngleLimit); + } + + const auto PreviousActorYawAngle{ActorRotation.Yaw}; + + ActorRotation.Yaw = FMath::UnwindDegrees(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - ViewRelativeAngle)); + + // We use UE_KINDA_SMALL_NUMBER here because even if ViewRelativeAngle hasn't + // changed, converting it back to ActorRotation.Yaw may introduce a rounding + // error, and FMath::IsNearlyZero() with default arguments will return false. + + return !FMath::IsNearlyZero(FMath::UnwindDegrees(ActorRotation.Yaw - PreviousActorYawAngle), UE_KINDA_SMALL_NUMBER); +} + +bool UGMS_CharacterMovementSystemComponent::ApplyRotationYawSpeedAnimationCurve(float DeltaTime) +{ + // Use curve to drive actor rotation only if no root bone rotation. 仅在没有使用根骨旋转时,采用曲线驱动Actor旋转。 + if (UGMS_MainAnimInstance* AnimInst = Cast(MainAnimInstance)) + { + if (AnimInst->GetOffsetRootBoneRotationMode() != EOffsetRootBoneMode::Release) + { + return false; + } + } + const float CurveValue = AnimationInstance->GetCurveValue(UGMS_Constants::RotationYawSpeedCurveName()); + const float DeltaYawAngle{CurveValue * DeltaTime}; + + if (FMath::Abs(DeltaYawAngle) > UE_SMALL_NUMBER) + { + auto NewActorRotation{GetOwner()->GetActorRotation()}; + NewActorRotation.Yaw += DeltaYawAngle; + + SetActorRotation(NewActorRotation); + + RefreshTargetYawAngleUsingActorRotation(); + return true; + } + return false; +} + +bool UGMS_CharacterMovementSystemComponent::RefreshCustomGroundedMovingRotation_Implementation(float DeltaTime) +{ + return false; +} + +bool UGMS_CharacterMovementSystemComponent::RefreshCustomGroundedNotMovingRotation_Implementation(float DeltaTime) +{ + return false; +} + +void UGMS_CharacterMovementSystemComponent::RefreshInAirRotation(const float DeltaTime) +{ + if (LocomotionMode != GMS_MovementModeTags::InAir || GetGameplayTags().HasAny(InAirRotationBlockingTags)) + { + return; + } + + if (RotationMode == GMS_RotationModeTags::VelocityDirection || RotationMode == GMS_RotationModeTags::ViewDirection) + { + switch (ControlSetting->InAirRotationMode) + { + case EGMS_InAirRotationMode::RotateToVelocityOnJump: + if (LocomotionState.bMoving) + { + SetRotationSmooth(LocomotionState.VelocityYawAngle, DeltaTime, ControlSetting->InAirRotationInterpolationSpeed); + } + else + { + RefreshTargetYawAngleUsingActorRotation(); + } + break; + + case EGMS_InAirRotationMode::KeepRelativeRotation: + SetRotationSmooth( + FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw) - LocomotionState.ViewRelativeTargetYawAngle), + DeltaTime, ControlSetting->InAirRotationInterpolationSpeed); + break; + + default: + RefreshTargetYawAngleUsingActorRotation(); + break; + } + } + else + { + RefreshTargetYawAngleUsingActorRotation(); + } +} + +void UGMS_CharacterMovementSystemComponent::SetRotationInstant_Implementation(const float TargetYawAngle, const ETeleportType Teleport) +{ + SetTargetYawAngle(TargetYawAngle); + + auto NewRotation{GetOwner()->GetActorRotation()}; + NewRotation.Yaw = TargetYawAngle; + + SetActorRotation(NewRotation); +} + +void UGMS_CharacterMovementSystemComponent::SetRotationSmooth_Implementation(const float TargetYawAngle, const float DeltaTime, const float InterpolationHalfLife) +{ + SetTargetYawAngle(TargetYawAngle); + + auto DesiredRotation{OwnerPawn->GetActorRotation()}; + DesiredRotation.Yaw = UGMS_Rotation::DamperExactAngle(UE_REAL_TO_FLOAT(FMath::UnwindDegrees(DesiredRotation.Yaw)), + LocomotionState.SmoothTargetYawAngle, DeltaTime, InterpolationHalfLife); + + + SetActorRotation(DesiredRotation); +} + +void UGMS_CharacterMovementSystemComponent::SetRotationExtraSmooth_Implementation(const float TargetYawAngle, const float DeltaTime, + const float InterpolationHalfLife, const float TargetYawAngleRotationSpeed) +{ + SetTargetYawAngleSmooth(TargetYawAngle, DeltaTime, TargetYawAngleRotationSpeed); + + auto DesiredRotation{OwnerPawn->GetActorRotation()}; + DesiredRotation.Yaw = UGMS_Rotation::DamperExactAngle(UE_REAL_TO_FLOAT(FMath::UnwindDegrees(DesiredRotation.Yaw)), + LocomotionState.SmoothTargetYawAngle, DeltaTime, InterpolationHalfLife); + + SetActorRotation(DesiredRotation); +} + +void UGMS_CharacterMovementSystemComponent::RefreshTargetYawAngleUsingActorRotation() +{ + const auto YawAngle{UE_REAL_TO_FLOAT(OwnerPawn->GetActorRotation().Yaw)}; + + SetTargetYawAngle(YawAngle); +} + +void UGMS_CharacterMovementSystemComponent::SetTargetYawAngle(const float TargetYawAngle) +{ + LocomotionState.TargetYawAngle = FMath::UnwindDegrees(TargetYawAngle); + + RefreshViewRelativeTargetYawAngle(); + + LocomotionState.SmoothTargetYawAngle = LocomotionState.TargetYawAngle; +} + +void UGMS_CharacterMovementSystemComponent::SetTargetYawAngleSmooth(float TargetYawAngle, float DeltaTime, float RotationSpeed) +{ + LocomotionState.TargetYawAngle = FMath::UnwindDegrees(TargetYawAngle); + + LocomotionState.SmoothTargetYawAngle = UGMS_Rotation::InterpolateAngleConstant( + LocomotionState.SmoothTargetYawAngle, LocomotionState.TargetYawAngle, DeltaTime, RotationSpeed); + + RefreshViewRelativeTargetYawAngle(); +} + +void UGMS_CharacterMovementSystemComponent::RefreshViewRelativeTargetYawAngle() +{ + LocomotionState.ViewRelativeTargetYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + ViewState.Rotation.Yaw - LocomotionState.TargetYawAngle)); +} + +void UGMS_CharacterMovementSystemComponent::SetActorRotation(FRotator DesiredRotation) +{ + const bool bWantsToBeVertical = CharacterMovement->ShouldRemainVertical(); + + if (bWantsToBeVertical) + { + DesiredRotation.Pitch = 0.f; + DesiredRotation.Yaw = FMath::UnwindDegrees(DesiredRotation.Yaw); + DesiredRotation.Roll = 0.f; + } + else + { + DesiredRotation.Normalize(); + } + OwnerPawn->SetActorRotation(DesiredRotation); +} + +FGMS_PredictGroundMovementPivotLocationParams UGMS_CharacterMovementSystemComponent::GetPredictGroundMovementPivotLocationParams() const +{ + FGMS_PredictGroundMovementPivotLocationParams Params; + if (CharacterMovement) + { + Params.Acceleration = CharacterMovement->GetCurrentAcceleration(); + Params.Velocity = CharacterMovement->GetLastUpdateVelocity(); + Params.GroundFriction = CharacterMovement->GroundFriction; + } + return Params; +} + +FGMS_PredictGroundMovementStopLocationParams UGMS_CharacterMovementSystemComponent::GetPredictGroundMovementStopLocationParams() const +{ + FGMS_PredictGroundMovementStopLocationParams Params; + if (CharacterMovement) + { + Params.Velocity = CharacterMovement->GetLastUpdateVelocity(); + Params.bUseSeparateBrakingFriction = CharacterMovement->bUseSeparateBrakingFriction; + Params.BrakingFriction = CharacterMovement->BrakingFriction; + Params.GroundFriction = CharacterMovement->GroundFriction; + Params.BrakingFrictionFactor = CharacterMovement->BrakingFrictionFactor; + Params.BrakingDecelerationWalking = CharacterMovement->BrakingDecelerationWalking; + } + return Params; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MovementSystemComponent.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MovementSystemComponent.cpp new file mode 100644 index 0000000..3e308ed --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MovementSystemComponent.cpp @@ -0,0 +1,898 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GMS_MovementSystemComponent.h" +#include "GameFramework/Pawn.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "GameplayTagAssetInterface.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" +#include "Animation/AnimInstance.h" +#include "Misc/DataValidation.h" +#include "Net/Core/PushModel/PushModel.h" +#include "Net/UnrealNetwork.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "Utility/GMS_Log.h" +#include "Utility/GMS_Math.h" +#include "Utility/GMS_Vector.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_MovementSystemComponent) + +UGMS_MovementSystemComponent::UGMS_MovementSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + PrimaryComponentTick.bCanEverTick = true; + bWantsInitializeComponent = true; +} + +void UGMS_MovementSystemComponent::PostLoad() +{ + Super::PostLoad(); +#if WITH_EDITOR + PRAGMA_DISABLE_DEPRECATION_WARNINGS + if (!MovementDefinitions.IsEmpty()) + { + MovementDefinition = MovementDefinitions.Last(); + MovementDefinitions.Empty(); + } + PRAGMA_ENABLE_DEPRECATION_WARNINGS +#endif +} + +void UGMS_MovementSystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + OwnerPawn = Cast(GetOwner()); + + check(OwnerPawn) + + if (OwnerPawn) + { + // Set some default values here to ensure that the animation instance and the + // camera component can read the most up-to-date values during their initialization. + SetReplicatedViewRotation(OwnerPawn->GetViewRotation().GetNormalized(), false); + + ViewState.Rotation = ReplicatedViewRotation; + ViewState.PreviousYawAngle = UE_REAL_TO_FLOAT(OwnerPawn->GetViewRotation().Yaw); + + const auto YawAngle{UE_REAL_TO_FLOAT(OwnerPawn->GetActorRotation().Yaw)}; + + LocomotionState.InputYawAngle = YawAngle; + LocomotionState.VelocityYawAngle = YawAngle; + } +} + +void UGMS_MovementSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + if (GetOwner()->GetClass()->ImplementsInterface(UGameplayTagAssetInterface::StaticClass())) + { + SetGameplayTagsProvider(GetOwner()); + } + else + { + TArray Components = GetOwner()->GetComponentsByInterface(UGameplayTagAssetInterface::StaticClass()); + if (Components.IsValidIndex(0)) + { + SetGameplayTagsProvider(Components[0]); + } + } +} + +void UGMS_MovementSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + Parameters.Condition = COND_SkipOwner; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, LocomotionMode, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, OverlayMode, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, MovementSet, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, MovementDefinition, Parameters) + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ReplicatedViewRotation, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, DesiredVelocityYawAngle, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, OwnedTags, Parameters) +} + +UGMS_MovementSystemComponent* UGMS_MovementSystemComponent::GetMovementSystemComponent(const AActor* Actor) +{ + return Actor != nullptr ? Actor->FindComponentByClass() : nullptr; +} + +bool UGMS_MovementSystemComponent::K2_FindMovementComponent(const AActor* Actor, UGMS_MovementSystemComponent*& Instance) +{ + if (Actor == nullptr) + { + return false; + } + Instance = GetMovementSystemComponent(Actor); + return Instance != nullptr; +} + +bool UGMS_MovementSystemComponent::K2_FindMovementComponentExt(const AActor* Actor, TSubclassOf DesiredClass, UGMS_MovementSystemComponent*& Instance) +{ + if (DesiredClass) + { + Instance = GetMovementSystemComponent(Actor); + return Instance != nullptr && Instance->GetClass()->IsChildOf(DesiredClass); + } + return false; +} + +FGameplayTagContainer UGMS_MovementSystemComponent::GetGameplayTags() const +{ + if (IGameplayTagAssetInterface* TagAssetInterface = Cast(GameplayTagsProvider)) + { + FGameplayTagContainer RetTags; + TagAssetInterface->GetOwnedGameplayTags(RetTags); + + if (!OwnedTags.IsEmpty()) + { + RetTags.AppendTags(OwnedTags); + } + + RetTags.AddTagFast(GetMovementState()); + RetTags.AddTagFast(GetRotationMode()); + + return RetTags; + } + return OwnedTags; +} + +void UGMS_MovementSystemComponent::SetGameplayTagsProvider(UObject* Provider) +{ + if (!IsValid(Provider)) + { + GMS_CLOG(Warning, "Passed invalid GameplayTagsProvider."); + return; + } + if (IGameplayTagAssetInterface* TagAssetInterface = Cast(Provider)) + { + GameplayTagsProvider = Provider; + } + else + { + GMS_CLOG(Warning, "Passed in GameplayTagsProvider(%s) Doesn't implement GameplayTagAssetInterface, it can't provide gameplay tags.", *Provider->GetName()); + } +} +#pragma region GameplayTags +void UGMS_MovementSystemComponent::AddGameplayTag(FGameplayTag TagToAdd) +{ + AddGameplayTag(TagToAdd, true); +} + +void UGMS_MovementSystemComponent::RemoveGameplay(FGameplayTag TagToRemove) +{ + RemoveGameplayTag(TagToRemove, true); +} + +void UGMS_MovementSystemComponent::SetGameplayTags(FGameplayTagContainer TagsToSet) +{ + SetGameplayTags(TagsToSet, true); +} + +void UGMS_MovementSystemComponent::AddGameplayTag(const FGameplayTag& TagToAdd, bool bSendRpc) +{ + if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy) + { + return; + } + + OwnedTags.AddTag(TagToAdd); + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwnedTags, this) + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientAddGameplayTag(TagToAdd); + } + else + { + ServerAddGameplayTag(TagToAdd); + } + } +} + +void UGMS_MovementSystemComponent::RemoveGameplayTag(const FGameplayTag& TagToRemove, bool bSendRpc) +{ + if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy) + { + return; + } + + OwnedTags.RemoveTag(TagToRemove); + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwnedTags, this) + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientRemoveGameplayTag(TagToRemove); + } + else + { + ServerRemoveGameplayTag(TagToRemove); + } + } +} + +void UGMS_MovementSystemComponent::SetGameplayTags(const FGameplayTagContainer& TagsToSet, bool bSendRpc) +{ + if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy) + { + return; + } + + OwnedTags = TagsToSet; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwnedTags, this) + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientSetGameplayTags(TagsToSet); + } + else + { + ServerSetGameplayTags(TagsToSet); + } + } +} + +void UGMS_MovementSystemComponent::ClientAddGameplayTag_Implementation(const FGameplayTag& TagToAdd) +{ + AddGameplayTag(TagToAdd, false); +} + +void UGMS_MovementSystemComponent::ServerAddGameplayTag_Implementation(const FGameplayTag& TagToAdd) +{ + AddGameplayTag(TagToAdd, false); +} + +void UGMS_MovementSystemComponent::ClientRemoveGameplayTag_Implementation(const FGameplayTag& TagToRemove) +{ + RemoveGameplayTag(TagToRemove, false); +} + +void UGMS_MovementSystemComponent::ServerRemoveGameplayTag_Implementation(const FGameplayTag& TagToRemove) +{ + RemoveGameplayTag(TagToRemove, false); +} + +void UGMS_MovementSystemComponent::ClientSetGameplayTags_Implementation(const FGameplayTagContainer& TagsToSet) +{ + SetGameplayTags(TagsToSet, false); +} + +void UGMS_MovementSystemComponent::ServerSetGameplayTags_Implementation(const FGameplayTagContainer& TagsToSet) +{ + SetGameplayTags(TagsToSet, false); +} +#pragma endregion GameplayTags + +#pragma region Locomotion + +const FGMS_LocomotionState& UGMS_MovementSystemComponent::GetLocomotionState() const +{ + return LocomotionState; +} + +TScriptInterface UGMS_MovementSystemComponent::GetTrajectoryPredictor() const +{ + return nullptr; +} + +bool UGMS_MovementSystemComponent::IsCrouching() const +{ + return false; +} + +float UGMS_MovementSystemComponent::GetMaxSpeed() const +{ + return 0.0; +} + +float UGMS_MovementSystemComponent::GetMaxAcceleration() const +{ + return 1000.0f; +} + +bool UGMS_MovementSystemComponent::IsMovingOnGround() const +{ + return true; +} + +void UGMS_MovementSystemComponent::SetDesiredVelocityYawAngle(float NewDesiredVelocityYawAngle) +{ + COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY(ThisClass, DesiredVelocityYawAngle, NewDesiredVelocityYawAngle, this); +} + +void UGMS_MovementSystemComponent::ServerSetDesiredVelocityYawAngle_Implementation(float NewDesiredVelocityYawAngle) +{ + SetDesiredVelocityYawAngle(NewDesiredVelocityYawAngle); +} + + +const FGameplayTag& UGMS_MovementSystemComponent::GetLocomotionMode() const +{ + return LocomotionMode; +} + +const FGMS_MovementBaseState& UGMS_MovementSystemComponent::GetMovementBase() const +{ + return MovementBase; +} + +void UGMS_MovementSystemComponent::SetLocomotionMode(const FGameplayTag& NewLocomotionMode) +{ + if (LocomotionMode != NewLocomotionMode) + { + const auto PreviousLocomotionMode{LocomotionMode}; + + LocomotionMode = NewLocomotionMode; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, LocomotionMode, this) + + OnLocomotionModeChanged(PreviousLocomotionMode); + } +} + +void UGMS_MovementSystemComponent::OnReplicated_LocomotionMode(const FGameplayTag& PreviousLocomotionMode) +{ + OnLocomotionModeChanged(PreviousLocomotionMode); +} + +void UGMS_MovementSystemComponent::OnLocomotionModeChanged_Implementation(const FGameplayTag& PreviousLocomotionMode) +{ + GMS_CLOG(Verbose, "locomotion mode changed from %s to %s", *PreviousLocomotionMode.ToString(), *LocomotionMode.ToString()); + OnLocomotionModeChangedEvent.Broadcast(PreviousLocomotionMode); +} + +#pragma endregion + +void UGMS_MovementSystemComponent::RefreshMovementBase() +{ +} + +#pragma region MovementSet + +const FGameplayTag& UGMS_MovementSystemComponent::GetMovementSet() const +{ + return MovementSet; +} + +const FGMS_MovementSetSetting& UGMS_MovementSystemComponent::GetMovementSetSetting() const +{ + return MovementSetSetting; +} + +const FGMS_MovementStateSetting& UGMS_MovementSystemComponent::GetMovementStateSetting() const +{ + return MovementStateSetting; +} + +const UGMS_MovementControlSetting_Default* UGMS_MovementSystemComponent::GetControlSetting() const +{ + return ControlSetting; +} + +int32 UGMS_MovementSystemComponent::GetNumOfMovementStateSettings() const +{ + return ControlSetting->MovementStates.Num(); +} + +TSoftObjectPtr UGMS_MovementSystemComponent::GetMovementDefinition() const +{ + return MovementDefinition; +} + +TSoftObjectPtr UGMS_MovementSystemComponent::GetPrevMovementDefinition() const +{ + return PrevMovementDefinition; +} + +const UGMS_MovementDefinition* UGMS_MovementSystemComponent::GetLoadedMovementDefinition() const +{ + return MovementDefinition.IsValid() ? MovementDefinition.Get() : nullptr; +} + +void UGMS_MovementSystemComponent::SetMovementSet(const FGameplayTag& NewMovementSet) +{ + SetMovementSet(NewMovementSet, true); +} + +void UGMS_MovementSystemComponent::SetMovementDefinition(TSoftObjectPtr NewDefinition) +{ + if (NewDefinition.IsNull()) + { + return; + } + if (NewDefinition != MovementDefinition) + { + auto LoadedDefinition = NewDefinition.LoadSynchronous(); + + if (LoadedDefinition->MovementSets.Contains(MovementSet)) + { + InternalSetMovementDefinition(NewDefinition, true); + } + } +} + +void UGMS_MovementSystemComponent::LocalSetMovementDefinition(TSoftObjectPtr NewDefinition) +{ + InternalSetMovementDefinition(NewDefinition, false); +} + +void UGMS_MovementSystemComponent::PushAvailableMovementDefinition(TSoftObjectPtr NewDefinition, bool bPopCurrent) +{ + InternalSetMovementDefinition(NewDefinition, true); +} + +void UGMS_MovementSystemComponent::PopAvailableMovementDefinition() +{ + // PopMovementDefinition(true); +} + +void UGMS_MovementSystemComponent::InternalSetMovementDefinition(const TSoftObjectPtr NewDefinition, bool bSendRpc) +{ + if (!NewDefinition.IsNull() && NewDefinition != MovementDefinition) + { + PrevMovementDefinition = MovementDefinition; + MovementDefinition = NewDefinition; + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, MovementDefinition, this) + + OnMovementSetChanged(MovementSet); + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientSetMovementDefinition(NewDefinition); + } + else + { + ServerSetMovementDefinition(NewDefinition); + } + } + } +} + +void UGMS_MovementSystemComponent::ClientSetMovementDefinition_Implementation(const TSoftObjectPtr& NewDefinition) +{ + InternalSetMovementDefinition(NewDefinition, false); +} + +void UGMS_MovementSystemComponent::ServerSetMovementDefinition_Implementation(const TSoftObjectPtr& NewDefinition) +{ + InternalSetMovementDefinition(NewDefinition, false); +} + +void UGMS_MovementSystemComponent::OnReplicated_MovementDefinition() +{ + OnMovementSetChanged(MovementSet); +} + +void UGMS_MovementSystemComponent::SetMovementSet(const FGameplayTag& NewMovementSet, bool bSendRpc) +{ + if (MovementSet == NewMovementSet || GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy) + { + return; + } + + const auto PreviousMovementSet{MovementSet}; + + MovementSet = NewMovementSet; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, MovementSet, this) + + OnMovementSetChanged(PreviousMovementSet); + + if (bSendRpc) + { + if (GetOwner()->GetLocalRole() >= ROLE_Authority) + { + ClientSetMovementSet(MovementSet); + } + else + { + ServerSetMovementSet(MovementSet); + } + } +} + +void UGMS_MovementSystemComponent::ClientSetMovementSet_Implementation(const FGameplayTag& NewMovementSet) +{ + SetMovementSet(NewMovementSet, false); +} + +void UGMS_MovementSystemComponent::ServerSetMovementSet_Implementation(const FGameplayTag& NewMovementSet) +{ + SetMovementSet(NewMovementSet, false); +} + +void UGMS_MovementSystemComponent::OnReplicated_MovementSet(const FGameplayTag& PreviousMovementSet) +{ + OnMovementSetChanged(PreviousMovementSet); +} + +void UGMS_MovementSystemComponent::RefreshMovementSetSetting() +{ + const UGMS_MovementDefinition* LoadedDefinition = MovementDefinition.LoadSynchronous(); + + if (!LoadedDefinition) + { + GMS_CLOG(Warning, "Missing valid movement definition!") + return; + } + + if (!LoadedDefinition->MovementSets.Contains(MovementSet)) + { + GMS_CLOG(Warning, "No movement set(%s) found in movement definition(%s)!", *MovementSet.ToString(), *GetNameSafe(LoadedDefinition)) + return; + } + MovementSetSetting = MovementDefinition->MovementSets[MovementSet]; + RefreshControlSetting(); + + // bool bFoundMovementSet{false}; + // for (int32 i = MovementDefinitions.Num() - 1; i >= 0; i--) + // { + // if (MovementDefinitions[i].IsNull()) + // { + // continue; + // } + // if (!MovementDefinitions[i].IsValid()) + // { + // MovementDefinitions[i].LoadSynchronous(); + // } + // if (MovementDefinitions[i]->MovementSets.Contains(MovementSet)) + // { + // MovementDefinition = MovementDefinitions[i].Get(); + // MovementSetSetting = MovementDefinition->MovementSets[MovementSet]; + // bFoundMovementSet = true; + // RefreshControlSetting(); + // break; + // } + // } + // if (!bFoundMovementSet) + // { + // GMS_CLOG(Error, "No movement set(%s) found in movement definitions!", *MovementSet.ToString()) + // } +} + +void UGMS_MovementSystemComponent::RefreshControlSetting() +{ + const UGMS_MovementControlSetting_Default* NewSetting = MovementSetSetting.bControlSettingPerOverlayMode && MovementSetSetting.ControlSettings.Contains(OverlayMode) + ? MovementSetSetting.ControlSettings[OverlayMode] + : MovementSetSetting.ControlSetting; + + if (NewSetting != nullptr && !NewSetting->MovementStates.IsEmpty()) + { + ControlSetting = NewSetting; + RefreshMovementStateSetting(); + ApplyMovementSetting(); + } + else + { + ControlSetting = nullptr; + GMS_CLOG(Error, "Empty MovementState settings are found in the movement set(%s) of definition(%s), which is not allowed!", *MovementSet.ToString(), + *MovementDefinition->GetName()) + } +} + +void UGMS_MovementSystemComponent::OnMovementSetChanged_Implementation(const FGameplayTag& PreviousMovementSet) +{ + RefreshMovementSetSetting(); + OnMovementSetChangedEvent.Broadcast(PreviousMovementSet); +} + +const FGameplayTag& UGMS_MovementSystemComponent::GetDesiredMovementState() const +{ + return FGameplayTag::EmptyTag; +} + + +void UGMS_MovementSystemComponent::RefreshMovementStateSetting() +{ + if (!IsValid(ControlSetting)) + { + return; + } + + FGMS_MovementStateSetting NewStateSetting; + if (!ControlSetting->GetStateByTag(GetMovementState(), NewStateSetting)) + { + checkf(!ControlSetting->MovementStates.IsEmpty(), TEXT("Found empty MovementState Settings on %s!"), *ControlSetting->GetName()) + NewStateSetting = ControlSetting->MovementStates.Last(); + SetDesiredMovement(NewStateSetting.Tag); + GMS_CLOG(Verbose, "No MovementState setting for current movement state(%s), Change desired last one(%s) in list.", *GetMovementState().ToString(), + *NewStateSetting.Tag.ToString()); + } + + MovementStateSetting = NewStateSetting; + + if (bRespectAllowedRotationModesSettings) + { + if (!MovementStateSetting.AllowedRotationModes.Contains(GetDesiredRotationMode())) + { + FGameplayTag AdjustedRotationMode = MovementStateSetting.AllowedRotationModes.Last(); + GMS_CLOG(Warning, "current movement state(%s) doesn't allow current desired rotation mode(%s), adjusted to:%s", *GetMovementState().ToString(), *GetDesiredRotationMode().ToString(), + *AdjustedRotationMode.ToString()); + SetDesiredRotationMode(AdjustedRotationMode); + } + } +} + +void UGMS_MovementSystemComponent::ApplyMovementSetting() +{ +} + +#pragma endregion + +#pragma region MovementState +void UGMS_MovementSystemComponent::SetDesiredMovement(const FGameplayTag& NewDesiredMovement) +{ +} + +void UGMS_MovementSystemComponent::CycleDesiredMovementState(bool bForward) +{ + if (GetNumOfMovementStateSettings() == 1) + { + return; + } + int32 Index = ControlSetting->MovementStates.IndexOfByKey(GetMovementState()); + + if (Index == INDEX_NONE) + { + return; + } + + if (bForward && ControlSetting->MovementStates.IsValidIndex(Index + 1)) + { + SetDesiredMovement(ControlSetting->MovementStates[Index + 1].Tag); + } + + if (!bForward && ControlSetting->MovementStates.IsValidIndex(Index - 1)) + { + SetDesiredMovement(ControlSetting->MovementStates[Index - 1].Tag); + } +} + +const FGameplayTag& UGMS_MovementSystemComponent::GetMovementState() const +{ + return FGameplayTag::EmptyTag; +} + +int32 UGMS_MovementSystemComponent::GetSpeedLevel() const +{ + return MovementStateSetting.SpeedLevel; +} + +float UGMS_MovementSystemComponent::GetMappedMovementSpeedLevel(float Speed) const +{ + if (!ControlSetting) + { + return 0.0f; + } + float SpeedLevelAmount = ControlSetting->MovementStates.Last().SpeedLevel; + for (int32 i = ControlSetting->MovementStates.Num() - 2; i >= 0; i--) + { + const FGMS_MovementStateSetting& Max = ControlSetting->MovementStates[i + 1]; + const FGMS_MovementStateSetting& Min = ControlSetting->MovementStates[i]; + // In current range. + if (Min.Speed > 0 && Speed >= Min.Speed && Speed <= Max.Speed) + { + SpeedLevelAmount = FMath::GetMappedRangeValueClamped(FVector2f{Min.Speed, Max.Speed}, {static_cast(Min.SpeedLevel), static_cast(Max.SpeedLevel)}, Speed); + } + } + GMS_CLOG(VeryVerbose, "Mapped speed(%f) to speed level(%f)", Speed, SpeedLevelAmount) + return SpeedLevelAmount; +} + + +void UGMS_MovementSystemComponent::OnMovementStateChanged_Implementation(const FGameplayTag& PreviousMovementState) +{ + RefreshMovementStateSetting(); + ApplyMovementSetting(); + OnMovementStateChangedEvent.Broadcast(PreviousMovementState); +} + +#pragma endregion + +#pragma region Input + +FVector UGMS_MovementSystemComponent::GetMovementIntent() const +{ + checkf(0, TEXT("Not implemented")); + return FVector::ZeroVector; +} + +void UGMS_MovementSystemComponent::RefreshInput(float DeltaTime) +{ + LocomotionState.bHasInput = GetMovementIntent().SizeSquared() > UE_KINDA_SMALL_NUMBER; + + if (LocomotionState.bHasInput) + { + LocomotionState.InputYawAngle = UE_REAL_TO_FLOAT(UGMS_Vector::DirectionToAngleXY(GetMovementIntent())); + } +} + +void UGMS_MovementSystemComponent::TurnAtRate(float Direction) +{ + if (Direction != 0 && GetRotationMode() == GMS_RotationModeTags::VelocityDirection && + OwnerPawn->GetLocalRole() >= ROLE_AutonomousProxy) + { + if (const auto Setting = ControlSetting->VelocityDirectionSetting.GetPtr()) + { + const float TurnRate = + IsValid(Setting->TurnRateSpeedCurve) + ? Setting->TurnRateSpeedCurve->GetFloatValue(FMath::Max(1.0f, GetMappedMovementSpeedLevel(UE_REAL_TO_FLOAT(LocomotionState.Velocity.Size2D())))) + : Setting->TurnRate; + + float YawDelta = Direction * TurnRate * GetWorld()->GetDeltaSeconds(); + if (FMath::Abs(YawDelta) > UE_SMALL_NUMBER) + { + float TargetYawAngle = FMath::UnwindDegrees(OwnerPawn->GetActorRotation().Yaw + YawDelta); + + // 或者,方法2:使用 RotateAngleAxis(如果需要保持四元数精度) + // FQuat DeltaRotation = FQuat(FVector::UpVector, FMath::DegreesToRadians(YawDelta)); + // FQuat NewRotation = DeltaRotation * LocomotionState.RotationQuaternion; + // NewDesiredVelocityYawAngle = NewRotation.Rotator().Yaw; + + // FRotator RotationDelta = FRotator(0,Direction * GetVelocityDirectionSetting().TurningRate * DeltaTime,0); + // + // NewDesiredVelocityYawAngle = (RotationDelta.Quaternion() * LocomotionState.RotationQuaternion).Rotator().Yaw; + + SetDesiredVelocityYawAngle(TargetYawAngle); + if (OwnerPawn->GetLocalRole() < ROLE_Authority) + { + ServerSetDesiredVelocityYawAngle(TargetYawAngle); + } + } + } + } +} + +#pragma endregion + +#pragma region ViewSystem + +const FGMS_ViewState& UGMS_MovementSystemComponent::GetViewState() const +{ + return ViewState; +} + +void UGMS_MovementSystemComponent::SetReplicatedViewRotation(const FRotator& NewViewRotation, bool bSendRpc) +{ + if (!ReplicatedViewRotation.Equals(NewViewRotation)) + { + ReplicatedViewRotation = NewViewRotation; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ReplicatedViewRotation, this) + + if (bSendRpc && GetOwner()->GetLocalRole() == ROLE_AutonomousProxy) + { + ServerSetReplicatedViewRotation(ReplicatedViewRotation); + } + } +} + +void UGMS_MovementSystemComponent::ServerSetReplicatedViewRotation_Implementation(const FRotator& NewViewRotation) +{ + SetReplicatedViewRotation(NewViewRotation, false); +} + +void UGMS_MovementSystemComponent::OnReplicated_ReplicatedViewRotation() +{ + ViewState.Rotation = MovementBase.bHasRelativeRotation ? (MovementBase.Rotation * ReplicatedViewRotation.Quaternion()).Rotator() : ReplicatedViewRotation; +} + +#pragma endregion + +#pragma region Rotation Mode + +const FGameplayTag& UGMS_MovementSystemComponent::GetDesiredRotationMode() const +{ + return FGameplayTag::EmptyTag; +} + +void UGMS_MovementSystemComponent::SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode) +{ +} + +const FGameplayTag& UGMS_MovementSystemComponent::GetRotationMode() const +{ + return FGameplayTag::EmptyTag; +} + +void UGMS_MovementSystemComponent::OnRotationModeChanged_Implementation(const FGameplayTag& PreviousRotationMode) +{ + ApplyMovementSetting(); + OnRotationModeChangedEvent.Broadcast(PreviousRotationMode); +} + +#pragma endregion + +#pragma region OverlayMode +const FGameplayTag& UGMS_MovementSystemComponent::GetOverlayMode() const +{ + return OverlayMode; +} + +void UGMS_MovementSystemComponent::SetOverlayMode(const FGameplayTag& NewOverlayMode) +{ + SetOverlayMode(NewOverlayMode, true); +} + +void UGMS_MovementSystemComponent::SetOverlayMode(const FGameplayTag& NewOverlayMode, bool bSendRpc) +{ + if (OverlayMode == NewOverlayMode || OwnerPawn->GetLocalRole() <= ROLE_SimulatedProxy) + { + return; + } + + const auto PreviousOverlayMode{OverlayMode}; + + OverlayMode = NewOverlayMode; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OverlayMode, this) + + OnOverlayModeChanged(PreviousOverlayMode); + + if (bSendRpc) + { + if (OwnerPawn->GetLocalRole() >= ROLE_Authority) + { + ClientSetOverlayMode(OverlayMode); + } + else + { + ServerSetOverlayMode(OverlayMode); + } + } +} + +void UGMS_MovementSystemComponent::ClientSetOverlayMode_Implementation(const FGameplayTag& NewOverlayMode) +{ + SetOverlayMode(NewOverlayMode, false); +} + +void UGMS_MovementSystemComponent::ServerSetOverlayMode_Implementation(const FGameplayTag& NewOverlayMode) +{ + SetOverlayMode(NewOverlayMode, false); +} + +void UGMS_MovementSystemComponent::OnReplicated_OverlayMode(const FGameplayTag& PreviousOverlayMode) +{ + OnOverlayModeChanged(PreviousOverlayMode); +} + +void UGMS_MovementSystemComponent::OnOverlayModeChanged_Implementation(const FGameplayTag& PreviousOverlayMode) +{ + if (MovementSetSetting.bControlSettingPerOverlayMode) + { + RefreshControlSetting(); + } + OnOverlayModeChangedEvent.Broadcast(PreviousOverlayMode); +} +#pragma endregion + + +#if WITH_EDITOR +EDataValidationResult UGMS_MovementSystemComponent::IsDataValid(class FDataValidationContext& Context) const +{ + if (IsTemplate() && AnimGraphSetting == nullptr) + { + Context.AddError(FText::FromString("AnimGraphSetting is required!")); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MoverMovementSystemComponent.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MoverMovementSystemComponent.cpp new file mode 100644 index 0000000..71a6782 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/GMS_MoverMovementSystemComponent.cpp @@ -0,0 +1,753 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "GMS_MoverMovementSystemComponent.h" +#include "PoseSearch/PoseSearchTrajectoryPredictor.h" +#include "Runtime/Launch/Resources/Version.h" +#include "GameFramework/Pawn.h" +#include "Components/SkeletalMeshComponent.h" +#include "MoverComponent.h" +#include "MoverPoseSearchTrajectoryPredictor.h" +#include "BoneControllers/AnimNode_OffsetRootBone.h" +#include "Components/CapsuleComponent.h" +#include "DefaultMovementSet/CharacterMoverComponent.h" +#include "DefaultMovementSet/NavMoverComponent.h" +#include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "MoveLibrary/MovementMixer.h" +#include "Mover/GMS_MoverStructLibrary.h" +#include "Mover/Modifers/GMS_MovementStateModifer.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "Utility/GMS_Constants.h" +#include "Utility/GMS_Log.h" +#include "Utility/GMS_Math.h" +#include "Utility/GMS_Utility.h" +#include "Utility/GMS_Vector.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_MoverMovementSystemComponent) + + +UGMS_MoverMovementSystemComponent::UGMS_MoverMovementSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + SetIsReplicatedByDefault(true); + PrimaryComponentTick.bCanEverTick = true; + bWantsInitializeComponent = true; + bReplicateUsingRegisteredSubObjectList = true; + + MovementModeToTagMapping = { + {TEXT("None"), GMS_MovementModeTags::None}, + {TEXT("Walking"), GMS_MovementModeTags::Grounded}, + {TEXT("NavWalking"), GMS_MovementModeTags::Grounded}, + {TEXT("Falling"), GMS_MovementModeTags::InAir}, + {TEXT("Swimming"), GMS_MovementModeTags::Swimming}, + {TEXT("Flying"), GMS_MovementModeTags::Flying}, + }; +} + +void UGMS_MoverMovementSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams Parameters; + Parameters.bIsPushBased = true; + + Parameters.Condition = COND_SkipOwner; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, MovementState, Parameters) + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, RotationMode, Parameters) +} + +void UGMS_MoverMovementSystemComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + MovementState = DesiredMovementState; + RotationMode = DesiredRotationMode; + MoverComponent = OwnerPawn->FindComponentByClass(); + if (MoverComponent) + { + TrajectoryPredictor = NewObject(this, UMoverTrajectoryPredictor::StaticClass()); + TrajectoryPredictor->Setup(MoverComponent); + MoverComponent->InputProducer = this; + if (!MoverComponent->MovementMixer) + { + // Prevent crash by early create this object on initialzie component. + MoverComponent->MovementMixer = NewObject(this, TEXT("Default Movement Mixer")); + } + // Make sure this component are ticking after the mover component so this component can access the most up-to-date mover state. + AddTickPrerequisiteComponent(MoverComponent); + } + + NavMoverComponent = OwnerPawn->FindComponentByClass(); + + MeshComponent = OwnerPawn->FindComponentByClass(); + if (MeshComponent) + { + AnimationInstance = MeshComponent->GetAnimInstance(); + // Make sure the mesh and animation blueprint are ticking after the character so they can access the most up-to-date character state. + MeshComponent->AddTickPrerequisiteComponent(this); + } +} + + +// Called when the game starts +void UGMS_MoverMovementSystemComponent::BeginPlay() +{ + Super::BeginPlay(); + + //This callback fires only on predicting client and server, not simulated pawn. + MoverComponent->OnMovementModeChanged.AddDynamic(this, &ThisClass::OnMoverMovementModeChanged); + + RefreshMovementSetSetting(); + + MoverComponent->OnPreSimulationTick.AddDynamic(this, &ThisClass::OnMoverPreSimulationTick); +} + +void UGMS_MoverMovementSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (IsValid(MoverComponent)) + { + MoverComponent->OnMovementModeChanged.RemoveDynamic(this, &ThisClass::OnMoverMovementModeChanged); + } + Super::EndPlay(EndPlayReason); +} + + +// Called every frame +void UGMS_MoverMovementSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_MoverMovementSystemComponent::TickComponent"), STAT_GMS_MovementSystem_Tick, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + if (!GetMovementDefinition().IsValid() || !IsValid(AnimationInstance) || !IsValid(ControlSetting)) + { + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + return; + } + + RefreshMovementBase(); + + RefreshInput(DeltaTime); + + RefreshLocomotionEarly(); + + RefreshView(DeltaTime); + + RefreshLocomotion(DeltaTime); + + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + RefreshLocomotionLate(DeltaTime); +} + +void UGMS_MoverMovementSystemComponent::OnMoverPreSimulationTick(const FMoverTimeStep& TimeStep, const FMoverInputCmdContext& InputCmd) +{ + const FCharacterDefaultInputs* CharacterInputs = InputCmd.InputCollection.FindDataByType(); + const FGMS_MoverMovementControlInputs* ControlInputs = InputCmd.InputCollection.FindDataByType(); + + if (ControlInputs) + { + // update movement state, settings. before actually do it. + ApplyMovementState(ControlInputs->DesiredMovementState); + + ApplyRotationMode(ControlInputs->DesiredRotationMode); + } +} + +void UGMS_MoverMovementSystemComponent::OnMoverMovementModeChanged(const FName& PreviousMovementModeName, const FName& NewMovementModeName) +{ + // Use the mover movement mode to set the locomotion mode to the right value. + + if (NewMovementModeName != NAME_None) + { + if (MovementModeToTagMapping.Contains(NewMovementModeName) && MovementModeToTagMapping[NewMovementModeName].IsValid()) + { + if (LocomotionMode != MovementModeToTagMapping[NewMovementModeName]) + { + SetLocomotionMode(MovementModeToTagMapping[NewMovementModeName]); + } + } + else + { + GMS_CLOG(Error, "No locomotion mode mapping for MovementMode:%s", *NewMovementModeName.ToString()); + } + } +} + +void UGMS_MoverMovementSystemComponent::ApplyMovementSetting() +{ + if (IsValid(MoverComponent) && IsValid(ControlSetting)) + { + if (const FGMS_MovementStateSetting* TempMS = ControlSetting->GetMovementStateSetting(DesiredMovementState, true)) + { + if (UCommonLegacyMovementSettings* LegacyMovementSettings = MoverComponent->FindSharedSettings_Mutable()) + { + LegacyMovementSettings->MaxSpeed = TempMS->Speed; + LegacyMovementSettings->Acceleration = TempMS->Acceleration; + LegacyMovementSettings->Deceleration = TempMS->BrakingDeceleration; + } + } + } +} + +void UGMS_MoverMovementSystemComponent::RefreshView(float DeltaTime) +{ + ViewState.PreviousYawAngle = UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw); + + if (OwnerPawn->IsLocallyControlled() || (OwnerPawn->GetLocalRole() >= ROLE_Authority && IsValid(OwnerPawn->GetController()))) + { + // The character movement component already sends the view rotation to the + // server if movement is replicated, so we don't have to do this ourselves. + SetReplicatedViewRotation(OwnerPawn->GetViewRotation().GetNormalized(), true); + } + + ViewState.Rotation = ReplicatedViewRotation; + // Set the yaw speed by comparing the current and previous view yaw angle, divided by + // delta seconds. This represents the speed the camera is rotating from left to right. + if (DeltaTime > UE_SMALL_NUMBER) + { + ViewState.YawSpeed = FMath::Abs(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - ViewState.PreviousYawAngle)) / DeltaTime; + } +} + +void UGMS_MoverMovementSystemComponent::ServerSetReplicatedViewRotation_Implementation(const FRotator& NewViewRotation) +{ + Super::ServerSetReplicatedViewRotation_Implementation(NewViewRotation); + + // Mover doesn't send control rotation to server, so we do it. + if (OwnerPawn->GetController() && !OwnerPawn->GetController()->GetControlRotation().Equals(NewViewRotation)) + { + OwnerPawn->GetController()->SetControlRotation(NewViewRotation); + } +} + +TScriptInterface UGMS_MoverMovementSystemComponent::GetTrajectoryPredictor() const +{ + return TrajectoryPredictor; +} + +bool UGMS_MoverMovementSystemComponent::IsCrouching() const +{ + if (UCharacterMoverComponent* CharacterMover = Cast(MoverComponent)) + { + return CharacterMover->IsCrouching(); + } + return false; +} + +float UGMS_MoverMovementSystemComponent::GetMaxSpeed() const +{ + const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings(); + + return CommonLegacySettings->MaxSpeed; +} + +float UGMS_MoverMovementSystemComponent::GetScaledCapsuleRadius() const +{ + if (UCapsuleComponent* Capsule = Cast(MoverComponent->GetUpdatedComponent())) + { + return Capsule->GetScaledCapsuleRadius(); + } + return GetDefault()->GetUnscaledCapsuleRadius(); +} + +float UGMS_MoverMovementSystemComponent::GetScaledCapsuleHalfHeight() const +{ + if (UCapsuleComponent* Capsule = Cast(MoverComponent->GetUpdatedComponent())) + { + return Capsule->GetScaledCapsuleHalfHeight(); + } + return GetDefault()->GetUnscaledCapsuleHalfHeight(); +} + +float UGMS_MoverMovementSystemComponent::GetMaxAcceleration() const +{ + const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings(); + + return CommonLegacySettings->Acceleration; +} + +float UGMS_MoverMovementSystemComponent::GetMaxBrakingDeceleration() const +{ + const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings(); + + return CommonLegacySettings->Deceleration; +} + +float UGMS_MoverMovementSystemComponent::GetWalkableFloorZ() const +{ + const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings(); + + return CommonLegacySettings->MaxWalkSlopeCosine; +} + +float UGMS_MoverMovementSystemComponent::GetGravityZ() const +{ + return MoverComponent->GetGravityAcceleration().Z; +} + +USkeletalMeshComponent* UGMS_MoverMovementSystemComponent::GetMesh() const +{ + return MeshComponent; +} + +bool UGMS_MoverMovementSystemComponent::IsMovingOnGround() const +{ + return IsValid(MoverComponent) ? MoverComponent->HasGameplayTag(Mover_IsOnGround, true) : false; +} + +void UGMS_MoverMovementSystemComponent::RefreshLocomotionEarly() +{ +} + +void UGMS_MoverMovementSystemComponent::RefreshLocomotion(float DeltaTime) +{ + LocomotionState.Velocity = MoverComponent->GetVelocity(); + + // Determine if the character is moving by getting its speed. The speed equals the length + // of the horizontal velocity, so it does not take vertical movement into account. If the + // character is moving, update the last velocity rotation. This value is saved because it might + // be useful to know the last orientation of a movement even after the character has stopped. + + LocomotionState.Speed = UE_REAL_TO_FLOAT(LocomotionState.Velocity.Size2D()); + + static constexpr auto HasSpeedThreshold{1.0f}; + + LocomotionState.bHasVelocity = LocomotionState.Speed >= HasSpeedThreshold; + + if (LocomotionState.bHasVelocity) + { + LocomotionState.VelocityYawAngle = UE_REAL_TO_FLOAT(UGMS_Vector::DirectionToAngleXY(LocomotionState.Velocity)); + } + + // Character is moving if has speed and current acceleration, or if the speed is greater than the moving speed threshold. + + LocomotionState.bMoving = (LocomotionState.bHasInput && LocomotionState.bHasVelocity) || + LocomotionState.Speed > ControlSetting->MovingSpeedThreshold; +} + +void UGMS_MoverMovementSystemComponent::RefreshDynamicMovementState() +{ +} + +void UGMS_MoverMovementSystemComponent::RefreshLocomotionLate(float DeltaTime) +{ + if (!LocomotionMode.IsValid()) + { + RefreshTargetYawAngleUsingActorRotation(); + } +} + +void UGMS_MoverMovementSystemComponent::RefreshTargetYawAngleUsingActorRotation() +{ + const auto YawAngle{UE_REAL_TO_FLOAT(OwnerPawn->GetActorRotation().Yaw)}; + + SetTargetYawAngle(YawAngle); +} + +void UGMS_MoverMovementSystemComponent::SetTargetYawAngle(const float TargetYawAngle) +{ + LocomotionState.TargetYawAngle = FRotator3f::NormalizeAxis(TargetYawAngle); + + RefreshViewRelativeTargetYawAngle(); + + LocomotionState.SmoothTargetYawAngle = LocomotionState.TargetYawAngle; +} + +void UGMS_MoverMovementSystemComponent::RefreshViewRelativeTargetYawAngle() +{ + LocomotionState.ViewRelativeTargetYawAngle = FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT( + ViewState.Rotation.Yaw - LocomotionState.TargetYawAngle)); +} + +FGMS_PredictGroundMovementPivotLocationParams UGMS_MoverMovementSystemComponent::GetPredictGroundMovementPivotLocationParams() const +{ + FGMS_PredictGroundMovementPivotLocationParams Params; + + if (MoverComponent) + { + if (const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings()) + { + Params.Acceleration = MoverComponent->GetMovementIntent() * CommonLegacySettings->Acceleration; + Params.Velocity = MoverComponent->GetVelocity(); + Params.GroundFriction = CommonLegacySettings->GroundFriction; + } + } + return Params; +} + +FGMS_PredictGroundMovementStopLocationParams UGMS_MoverMovementSystemComponent::GetPredictGroundMovementStopLocationParams() const +{ + FGMS_PredictGroundMovementStopLocationParams Params; + if (MoverComponent) + { + if (const UCommonLegacyMovementSettings* CommonLegacySettings = MoverComponent->FindSharedSettings()) + { + Params.Velocity = MoverComponent->GetVelocity(); + Params.bUseSeparateBrakingFriction = true; + Params.BrakingFriction = CommonLegacySettings->BrakingFriction; + Params.GroundFriction = CommonLegacySettings->GroundFriction; + Params.BrakingFrictionFactor = CommonLegacySettings->BrakingFrictionFactor; + Params.BrakingDecelerationWalking = CommonLegacySettings->Deceleration; + } + } + return Params; +} + +void UGMS_MoverMovementSystemComponent::RefreshMovementBase() +{ + UPrimitiveComponent* BasePrimitive = MoverComponent->GetMovementBase(); + FName BaseBoneName = MoverComponent->GetMovementBaseBoneName(); + if (BasePrimitive != MovementBase.Primitive || BaseBoneName != MovementBase.BoneName) + { + MovementBase.Primitive = BasePrimitive; + MovementBase.BoneName = BaseBoneName; + MovementBase.bBaseChanged = true; + } + else + { + MovementBase.bBaseChanged = false; + } + + + MovementBase.bHasRelativeLocation = UBasedMovementUtils::IsADynamicBase(BasePrimitive); + MovementBase.bHasRelativeRotation = MovementBase.bHasRelativeLocation && bUseBaseRelativeMovement; + + const auto PreviousRotation{MovementBase.Rotation}; + + UBasedMovementUtils::GetMovementBaseTransform(BasePrimitive, BaseBoneName, + MovementBase.Location, MovementBase.Rotation); + + MovementBase.DeltaRotation = MovementBase.bHasRelativeLocation && !MovementBase.bBaseChanged + ? (MovementBase.Rotation * PreviousRotation.Inverse()).Rotator() + : FRotator::ZeroRotator; +} + + +void UGMS_MoverMovementSystemComponent::ProduceInput_Implementation(int32 SimTimeMs, FMoverInputCmdContext& InputCmdResult) +{ + InputCmdResult = OnProduceInput(static_cast(SimTimeMs), InputCmdResult); +} + +#pragma region MovementState + +const FGameplayTag& UGMS_MoverMovementSystemComponent::GetDesiredMovementState() const +{ + return DesiredMovementState; +} + +void UGMS_MoverMovementSystemComponent::SetDesiredMovement(const FGameplayTag& NewDesiredMovement) +{ + DesiredMovementState = NewDesiredMovement; +} + + +const FGameplayTag& UGMS_MoverMovementSystemComponent::GetMovementState() const +{ + return MovementState; +} + +void UGMS_MoverMovementSystemComponent::ApplyMovementState(const FGameplayTag& NewMovementState) +{ + if (MovementState == NewMovementState || GetOwner()->GetLocalRole() < ROLE_AutonomousProxy) + { + return; + } + + FGameplayTag Prev = MovementState; + + MovementState = NewMovementState; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, MovementState, this) + OnMovementStateChanged(Prev); +} + +#pragma endregion + +#pragma region RotationMode +const FGameplayTag& UGMS_MoverMovementSystemComponent::GetDesiredRotationMode() const +{ + return DesiredRotationMode; +} + +void UGMS_MoverMovementSystemComponent::SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode) +{ + DesiredRotationMode = NewDesiredRotationMode; +} + +const FGameplayTag& UGMS_MoverMovementSystemComponent::GetRotationMode() const +{ + return RotationMode; +} + +void UGMS_MoverMovementSystemComponent::ApplyRotationMode(const FGameplayTag& NewRotationMode) +{ + if (RotationMode == NewRotationMode || GetOwner()->GetLocalRole() < ROLE_AutonomousProxy) + { + return; + } + + FGameplayTag Prev = RotationMode; + + RotationMode = NewRotationMode; + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, RotationMode, this) + OnRotationModeChanged(Prev); +} + +#pragma endregion +FVector UGMS_MoverMovementSystemComponent::GetMovementIntent() const +{ + if (const FCharacterDefaultInputs* CharacterInputs = MoverComponent->GetLastInputCmd().InputCollection.FindDataByType()) + { + return CharacterInputs->GetMoveInput_WorldSpace(); + } + return MoverComponent->GetMovementIntent(); +} + +FVector UGMS_MoverMovementSystemComponent::AdjustOrientationIntent(float DeltaSeconds, const FVector& OrientationIntent) const +{ + FVector Intent = OrientationIntent; + if (GetRotationMode() == GMS_RotationModeTags::VelocityDirection) + { + if (const FGMS_VelocityDirectionSetting_RateBased* Setting = GetControlSetting()->VelocityDirectionSetting.GetPtr()) + { + float YawDelta = CachedTurnInput.Yaw * DeltaSeconds * Setting->TurnRate; + Intent.Z += YawDelta; + } + } + + if (GetRotationMode() == GMS_RotationModeTags::ViewDirection) + { + // Use curve to drive actor rotation only if no root bone rotation. 仅在没有使用根骨旋转时,采用曲线驱动Actor旋转。 + if (UGMS_MainAnimInstance* AnimInst = Cast(MainAnimInstance)) + { + if (AnimInst->GetOffsetRootBoneRotationMode() == EOffsetRootBoneMode::Release) + { + const float CurveValue = AnimationInstance->GetCurveValue(UGMS_Constants::RotationYawSpeedCurveName()); + const float DeltaYawAngle{CurveValue * DeltaSeconds}; + + if (FMath::Abs(DeltaYawAngle) > UE_SMALL_NUMBER) + { + Intent = Intent.RotateAngleAxis(DeltaYawAngle, FVector::ZAxisVector); + } + } + } + } + + return Intent; +} + +FMoverInputCmdContext UGMS_MoverMovementSystemComponent::OnProduceInput_Implementation(float DeltaMs, FMoverInputCmdContext InputCmdResult) +{ + check(OwnerPawn) + + FCharacterDefaultInputs& CharacterInputs = InputCmdResult.InputCollection.FindOrAddMutableDataByType(); + + FGMS_MoverMovementControlInputs& ControlInputs = InputCmdResult.InputCollection.FindOrAddMutableDataByType(); + + ControlInputs.DesiredMovementSet = MovementSet; + ControlInputs.DesiredRotationMode = DesiredRotationMode; + ControlInputs.DesiredMovementState = DesiredMovementState; + + if (OwnerPawn->Controller == nullptr) + { + if (OwnerPawn->GetLocalRole() == ROLE_Authority && OwnerPawn->GetRemoteRole() == ROLE_SimulatedProxy) + { + static const FCharacterDefaultInputs DoNothingInput; + // If we get here, that means this pawn is not currently possessed and we're choosing to provide default do-nothing input + CharacterInputs = DoNothingInput; + } + + // We don't have a local controller so we can't run the code below. This is ok. Simulated proxies will just use previous input when extrapolating + return InputCmdResult; + } + + CharacterInputs.ControlRotation = FRotator::ZeroRotator; + + // APlayerController* PC = Cast(OwnerPawn->Controller); + // if (PC) + // { + // CharacterInputs.ControlRotation = PC->GetControlRotation(); + // } + CharacterInputs.ControlRotation = ViewState.Rotation; + + bool bRequestedNavMovement = false; + if (NavMoverComponent) + { + bRequestedNavMovement = NavMoverComponent->ConsumeNavMovementData(CachedMoveInputIntent, CachedMoveInputVelocity); + } + + // setup move input based on velocity/raw input. + { + // Favor velocity input + bool bUsingInputIntentForMove = CachedMoveInputVelocity.IsZero(); + + if (bUsingInputIntentForMove) + { + const FVector FinalDirectionalIntent = CharacterInputs.ControlRotation.RotateVector(CachedMoveInputIntent); + // FRotator Rotator = CharacterInputs.ControlRotation; + // FVector FinalDirectionalIntent; + // if (const UCharacterMoverComponent* MoverComp = Cast(MoverComponent)) + // { + // if (MoverComp->IsOnGround() || MoverComp->IsFalling()) + // { + // const FVector RotationProjectedOntoUpDirection = FVector::VectorPlaneProject(Rotator.Vector(), MoverComp->GetUpDirection()).GetSafeNormal(); + // Rotator = RotationProjectedOntoUpDirection.Rotation(); + // } + // FinalDirectionalIntent = Rotator.RotateVector(CachedMoveInputIntent); + // } + CharacterInputs.SetMoveInput(EMoveInputType::DirectionalIntent, FinalDirectionalIntent); + } + else + { + CharacterInputs.SetMoveInput(EMoveInputType::Velocity, CachedMoveInputVelocity); + } + + // Normally cached input is cleared by OnMoveCompleted input event but that won't be called if movement came from nav movement + if (bRequestedNavMovement) + { + CachedMoveInputIntent = FVector::ZeroVector; + CachedMoveInputVelocity = FVector::ZeroVector; + } + } + + + static float RotationMagMin(1e-3); + + const bool bHasAffirmativeMoveInput = (CharacterInputs.GetMoveInput().Size() >= RotationMagMin); + + // Figure out intended orientation + CharacterInputs.OrientationIntent = FVector::ZeroVector; + + // setup orientation. + { + const bool bVelocityDirection = GetRotationMode() == GMS_RotationModeTags::VelocityDirection; + const bool bViewDirection = !bVelocityDirection; + if (!bHasAffirmativeMoveInput && bVelocityDirection) + { + if (const FGMS_VelocityDirectionSetting_RateBased* Setting = GetControlSetting()->VelocityDirectionSetting.GetPtr()) + { + const float DeltaSeconds = DeltaMs * 0.001f; + CharacterInputs.OrientationIntent = AdjustOrientationIntent(DeltaSeconds, MoverComponent->GetTargetOrientation().Vector()); + } + else if (bMaintainLastInputOrientation) + { + // There is no movement intent, so use the last-known affirmative move input + CharacterInputs.OrientationIntent = LastAffirmativeMoveInput; + } + } + if (bHasAffirmativeMoveInput && bVelocityDirection) + { + if (const FGMS_VelocityDirectionSetting_RateBased* Setting = GetControlSetting()->VelocityDirectionSetting.GetPtr()) + { + const float DeltaSeconds = DeltaMs * 0.001f; + CharacterInputs.OrientationIntent = AdjustOrientationIntent(DeltaSeconds, MoverComponent->GetTargetOrientation().Vector()); + } + else + { + // set the intent to the actors movement direction + CharacterInputs.OrientationIntent = CharacterInputs.GetMoveInput().GetSafeNormal(); + } + } + if (!bHasAffirmativeMoveInput && bViewDirection) + { + if (GetControlSetting()->ViewDirectionSetting.Get().bEnableRotationWhenNotMoving) + { + // set intent to the the control rotation - often a player's camera rotation + CharacterInputs.OrientationIntent = CharacterInputs.ControlRotation.Vector().GetSafeNormal(); + } + else + { + const float DeltaSeconds = DeltaMs * 0.001f; + CharacterInputs.OrientationIntent = AdjustOrientationIntent(DeltaSeconds, MoverComponent->GetTargetOrientation().Vector()); + } + } + if (bHasAffirmativeMoveInput && bViewDirection) + { + // set intent to the control rotation - often a player's camera rotation + CharacterInputs.OrientationIntent = CharacterInputs.ControlRotation.Vector().GetSafeNormal(); + } + } + + if (bHasAffirmativeMoveInput) + { + LastAffirmativeMoveInput = CharacterInputs.GetMoveInput(); + } + + if (bShouldRemainVertical) + { + // canceling out any z intent if the actor is supposed to remain vertical + CharacterInputs.OrientationIntent = CharacterInputs.OrientationIntent.GetSafeNormal2D(); + } + + CharacterInputs.bIsJumpPressed = bIsJumpPressed; + CharacterInputs.bIsJumpJustPressed = bIsJumpJustPressed; + + if (bShouldToggleFlying) + { + if (!bIsFlyingActive) + { + CharacterInputs.SuggestedMovementMode = DefaultModeNames::Flying; + } + else + { + CharacterInputs.SuggestedMovementMode = DefaultModeNames::Falling; + } + + bIsFlyingActive = !bIsFlyingActive; + } + else + { + CharacterInputs.SuggestedMovementMode = NAME_None; + } + + // Convert inputs to be relative to the current movement base (depending on options and state) + CharacterInputs.bUsingMovementBase = false; + + if (bUseBaseRelativeMovement) + { + if (const UCharacterMoverComponent* MoverComp = OwnerPawn->GetComponentByClass()) + { + if (UPrimitiveComponent* MovementBasePtr = MoverComp->GetMovementBase()) + { + FName MovementBaseBoneName = MoverComp->GetMovementBaseBoneName(); + + FVector RelativeMoveInput, RelativeOrientDir; + + UBasedMovementUtils::TransformWorldDirectionToBased(MovementBasePtr, MovementBaseBoneName, CharacterInputs.GetMoveInput(), RelativeMoveInput); + UBasedMovementUtils::TransformWorldDirectionToBased(MovementBasePtr, MovementBaseBoneName, CharacterInputs.OrientationIntent, RelativeOrientDir); + + CharacterInputs.SetMoveInput(CharacterInputs.GetMoveInputType(), RelativeMoveInput); + CharacterInputs.OrientationIntent = RelativeOrientDir; + + CharacterInputs.bUsingMovementBase = true; + CharacterInputs.MovementBase = MovementBasePtr; + CharacterInputs.MovementBaseBoneName = MovementBaseBoneName; + } + } + } + + // Clear/consume temporal movement inputs. We are not consuming others in the event that the game world is ticking at a lower rate than the Mover simulation. + // In that case, we want most input to carry over between simulation frames. + { + bIsJumpJustPressed = false; + bShouldToggleFlying = false; + } + + return InputCmdResult; +} + +#if WITH_EDITOR +EDataValidationResult UGMS_MoverMovementSystemComponent::IsDataValid(class FDataValidationContext& Context) const +{ + // if (IsTemplate() && InputProducerClass.IsNull()) + // { + // Context.AddError(FText::FromString("Input Producer Class is required!")); + // return EDataValidationResult::Invalid; + // } + return Super::IsDataValid(Context); +} +#endif diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/GenericMovementSystem.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/GenericMovementSystem.cpp new file mode 100644 index 0000000..59af6de --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/GenericMovementSystem.cpp @@ -0,0 +1,60 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "GenericMovementSystem.h" + +#include "Utility/GMS_Log.h" +#if ALLOW_CONSOLE +#include "Engine/Console.h" +#endif + +#if WITH_EDITOR +#include "MessageLogModule.h" +#endif + +#define LOCTEXT_NAMESPACE "FGenericMovementSystemModule" + +void FGenericMovementSystemModule::StartupModule() +{ +#if ALLOW_CONSOLE + UConsole::RegisterConsoleAutoCompleteEntries.AddRaw(this, &FGenericMovementSystemModule::Console_OnRegisterAutoCompleteEntries); +#endif + +#if WITH_EDITOR + // Register dedicated message log category for GMS. + auto& MessageLog{FModuleManager::LoadModuleChecked(FName{TEXTVIEW("MessageLog")})}; + + FMessageLogInitializationOptions MessageLogOptions; + MessageLogOptions.bShowFilters = true; + MessageLogOptions.bAllowClear = true; + MessageLogOptions.bDiscardDuplicates = true; + + MessageLog.RegisterLogListing(GMSLog::MessageLogName, LOCTEXT("MessageLogLabel", "GMS"), MessageLogOptions); +#endif +} + +void FGenericMovementSystemModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + + +#if ALLOW_CONSOLE +void FGenericMovementSystemModule::Console_OnRegisterAutoCompleteEntries(TArray& AutoCompleteCommands) +{ + const auto CommandColor{GetDefault()->AutoCompleteCommandColor}; + + auto* Command{&AutoCompleteCommands.AddDefaulted_GetRef()}; + Command->Command = FString{TEXTVIEW("Stat GMS")}; + Command->Desc = FString{TEXTVIEW("Displays GMS performance statistics.")}; + Command->Color = CommandColor; + + + // Visual debugging will continue in 1.6 +} +#endif + + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGenericMovementSystemModule, GenericMovementSystem) diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer.cpp new file mode 100644 index 0000000..e399b5e --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer.cpp @@ -0,0 +1,128 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer.h" +#include "GameFramework/Pawn.h" +#include "GMS_MovementSystemComponent.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Misc/DataValidation.h" +#include "Settings/GMS_SettingObjectLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer) + + +bool UGMS_AnimLayerSetting::GetOverrideAnimLayerClass_Implementation(TSubclassOf& OutLayerClass) const +{ + return false; +} + +bool UGMS_AnimLayerSetting::K2_IsDataValid_Implementation(FText& ErrorText) const +{ + return true; +} + +#if WITH_EDITOR + +EDataValidationResult UGMS_AnimLayerSetting::IsDataValid(class FDataValidationContext& Context) const +{ + FText ErrorText; + if (!IsTemplate() && !K2_IsDataValid(ErrorText)) + { + Context.AddError(ErrorText); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} +#endif + + +void UGMS_AnimLayer::OnLinked_Implementation() +{ + // make sure to get reference to parent when linked. + if (!Parent.IsValid()) + { + Parent = Cast(GetSkelMeshComponent()->GetAnimInstance()); + checkf(Parent!=nullptr, TEXT("Parent is not GMS_MainAnimInstance!")); + } + if (!PawnOwner) + { + PawnOwner = Cast(GetOwningActor()); + checkf(PawnOwner!=nullptr, TEXT("PawnOwner is not valid!")); + } + if (!MovementSystem) + { + MovementSystem = PawnOwner->FindComponentByClass(); + checkf(MovementSystem!=nullptr, TEXT("Movement Sysytem Component is not valid!")); + } + + if (!AnimStateNameToTagMapping.IsEmpty()) + { + Parent->RegisterStateNameToTagMapping(this, AnimStateNameToTagMapping); + } +} + +void UGMS_AnimLayer::OnUnlinked_Implementation() +{ + if (Parent.IsValid()) + { + if (!AnimStateNameToTagMapping.IsEmpty()) + { + Parent->UnregisterStateNameToTagMapping(this); + } + Parent = nullptr; + } +} + + +UGMS_AnimLayer::UGMS_AnimLayer() +{ + bUseMainInstanceMontageEvaluationData = true; +} + +UGMS_MainAnimInstance* UGMS_AnimLayer::GetParent() const +{ + return Parent.Get(); +} + +void UGMS_AnimLayer::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ +} + +void UGMS_AnimLayer::ResetSetting_Implementation() +{ +} + +void UGMS_AnimLayer::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); + + Parent = Cast(GetSkelMeshComponent()->GetAnimInstance()); + + PawnOwner = Cast(GetOwningActor()); + +#if WITH_EDITOR + if (!GetWorld()->IsGameWorld()) + { + // Use default objects for editor preview. + + if (!Parent.IsValid()) + { + Parent = GetMutableDefault(); + } + + if (!IsValid(PawnOwner)) + { + PawnOwner = GetMutableDefault(); + } + } +#endif +} + +void UGMS_AnimLayer::NativeBeginPlay() +{ + Super::NativeBeginPlay(); + + ensure(PawnOwner); + + MovementSystem = PawnOwner->FindComponentByClass(); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Additive.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Additive.cpp new file mode 100644 index 0000000..9e562e9 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Additive.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_Additive.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Additive) \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay.cpp new file mode 100644 index 0000000..09fd2ce --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay.cpp @@ -0,0 +1,7 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_Overlay.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Overlay) + diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.cpp new file mode 100644 index 0000000..575b481 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.cpp @@ -0,0 +1,448 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.h" +#include "Animation/AnimSequence.h" +#include "SequenceEvaluatorLibrary.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "UObject/ObjectSaveContext.h" +#include "Utility/GMS_Log.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Overlay_ParallelPoseStack) + +void FGMS_AnimData_BodyPose_Full::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.HeadBlendAmount = HeadBlend.BlendAmount; + LayeringState.HeadAdditiveBlendAmount = HeadBlend.AdditiveBlendAmount; + LayeringState.HeadSlotBlendAmount = HeadBlend.SlotBlendAmount; + + LayeringState.ArmLeftBlendAmount = ArmLeftBlend.BlendAmount; + LayeringState.ArmLeftAdditiveBlendAmount = ArmLeftBlend.AdditiveBlendAmount; + LayeringState.ArmLeftSlotBlendAmount = ArmLeftBlend.SlotBlendAmount; + LayeringState.ArmLeftLocalSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmLeftMeshSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 1.f : 0.f; + + LayeringState.ArmRightBlendAmount = ArmRightBlend.BlendAmount; + LayeringState.ArmRightAdditiveBlendAmount = ArmRightBlend.AdditiveBlendAmount; + LayeringState.ArmRightSlotBlendAmount = ArmRightBlend.SlotBlendAmount; + LayeringState.ArmRightLocalSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmRightMeshSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 1.f : 0.f; + + LayeringState.HandLeftBlendAmount = HandLeftBlend.BlendAmount; + LayeringState.HandRightBlendAmount = HandRightBlend.BlendAmount; + + LayeringState.SpineBlendAmount = SpineBlend.BlendAmount; + LayeringState.SpineAdditiveBlendAmount = SpineBlend.AdditiveBlendAmount; + LayeringState.SpineSlotBlendAmount = SpineBlend.SlotBlendAmount; + LayeringState.PelvisBlendAmount = PelvisBlend.BlendAmount; + LayeringState.PelvisSlotBlendAmount = PelvisBlend.SlotBlendAmount; + LayeringState.LegsBlendAmount = LegsBlend.BlendAmount; + LayeringState.LegsSlotBlendAmount = LegsBlend.SlotBlendAmount; + } +} + +void FGMS_AnimData_BodyPose_Upper::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.HeadBlendAmount = HeadBlend.BlendAmount; + LayeringState.HeadAdditiveBlendAmount = HeadBlend.AdditiveBlendAmount; + LayeringState.HeadSlotBlendAmount = HeadBlend.SlotBlendAmount; + + LayeringState.ArmLeftBlendAmount = ArmLeftBlend.BlendAmount; + LayeringState.ArmLeftAdditiveBlendAmount = ArmLeftBlend.AdditiveBlendAmount; + LayeringState.ArmLeftSlotBlendAmount = ArmLeftBlend.SlotBlendAmount; + LayeringState.ArmLeftLocalSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmLeftMeshSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 1.f : 0.f; + + LayeringState.ArmRightBlendAmount = ArmRightBlend.BlendAmount; + LayeringState.ArmRightAdditiveBlendAmount = ArmRightBlend.AdditiveBlendAmount; + LayeringState.ArmRightSlotBlendAmount = ArmRightBlend.SlotBlendAmount; + LayeringState.ArmRightLocalSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmRightMeshSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 1.f : 0.f; + + LayeringState.HandLeftBlendAmount = HandLeftBlend.BlendAmount; + LayeringState.HandRightBlendAmount = HandRightBlend.BlendAmount; + + LayeringState.SpineBlendAmount = SpineBlend.BlendAmount; + LayeringState.SpineAdditiveBlendAmount = SpineBlend.AdditiveBlendAmount; + LayeringState.SpineSlotBlendAmount = SpineBlend.SlotBlendAmount; + } +} + +void FGMS_AnimData_BodyPose_Head::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.HeadBlendAmount = Blend.BlendAmount; + LayeringState.HeadAdditiveBlendAmount = Blend.AdditiveBlendAmount; + LayeringState.HeadSlotBlendAmount = Blend.SlotBlendAmount; + } +} + +void FGMS_AnimData_BodyPose_ArmLeft::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.ArmLeftBlendAmount = Blend.BlendAmount; + LayeringState.ArmLeftAdditiveBlendAmount = Blend.AdditiveBlendAmount; + LayeringState.ArmLeftSlotBlendAmount = Blend.SlotBlendAmount; + LayeringState.ArmLeftLocalSpaceBlendAmount = Blend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmLeftMeshSpaceBlendAmount = Blend.bMeshSpace ? 1.f : 0.f; + LayeringState.HandLeftBlendAmount = HandBlend.BlendAmount; + } +} + +void FGMS_AnimData_BodyPose_ArmRight::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.ArmRightBlendAmount = Blend.BlendAmount; + LayeringState.ArmRightAdditiveBlendAmount = Blend.AdditiveBlendAmount; + LayeringState.ArmRightSlotBlendAmount = Blend.SlotBlendAmount; + LayeringState.ArmRightLocalSpaceBlendAmount = Blend.bMeshSpace ? 0.f : 1.f; + LayeringState.ArmRightMeshSpaceBlendAmount = Blend.bMeshSpace ? 1.f : 0.f; + LayeringState.HandRightBlendAmount = HandBlend.BlendAmount; + } +} + +void FGMS_AnimData_BodyPose_Lower::UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const +{ + if (Pose != nullptr) + { + LayeringState.PelvisBlendAmount = PelvisBlend.BlendAmount; + LayeringState.PelvisSlotBlendAmount = PelvisBlend.SlotBlendAmount; + LayeringState.LegsBlendAmount = LegsBlend.BlendAmount; + LayeringState.LegsSlotBlendAmount = LegsBlend.SlotBlendAmount; + } +} + +// FGMS_BodyPartOverridePolicy 实现 +FGMS_BodyPartOverridePolicy::FGMS_BodyPartOverridePolicy() +{ + FallbackChain = { + {EGMS_BodyMask::Head, {EGMS_BodyMask::UpperBody, EGMS_BodyMask::FullBody}}, + // {EGMS_BodyMask::HandLeft, {EGMS_BodyMask::ArmLeft, EGMS_BodyMask::UpperBody, EGMS_BodyMask::FullBody}}, + // {EGMS_BodyMask::HandRight, {EGMS_BodyMask::ArmRight, EGMS_BodyMask::UpperBody, EGMS_BodyMask::FullBody}}, + {EGMS_BodyMask::ArmLeft, {EGMS_BodyMask::UpperBody, EGMS_BodyMask::FullBody}}, + {EGMS_BodyMask::ArmRight, {EGMS_BodyMask::UpperBody, EGMS_BodyMask::FullBody}}, + {EGMS_BodyMask::UpperBody, {EGMS_BodyMask::FullBody}}, + {EGMS_BodyMask::LowerBody, {EGMS_BodyMask::FullBody}}, + {EGMS_BodyMask::FullBody, {}}, + }; +} + + +#if WITH_EDITORONLY_DATA +void FGMS_AnimData_BodyPose::PreSave() +{ + bool bValid = Pose != nullptr && PoseExplicitTime >= 0 && Pose->GetSkeleton() != nullptr; + EditorFriendlyName = bValid ? GetNameSafe(Pose) : TEXT("Invalid Pose!"); +} + +void FGMS_OverlayModeSetting_ParallelPoseStack::PreSave() +{ + for (auto& Pose : FullBodyPoses) + { + Pose.GetMutable().PreSave(); + } +} + +void UGMS_AnimLayerSetting_Overlay_ParallelPoseStack::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + AcceleratedOverlayModes.Empty(); + for (FGMS_OverlayModeSetting_ParallelPoseStack& Mode : OverlayModes) + { + Mode.PreSave(); + AcceleratedOverlayModes.Emplace(Mode.Tag, Mode); + } +} +#endif + +bool FGMS_BodyPartOverridePolicy::CanOverride(EGMS_BodyMask NewPart, EGMS_BodyMask ExistingPart, int32 NewPriority, int32 ExistingPriority) const +{ + return static_cast(NewPart) < static_cast(ExistingPart) || + (NewPart == ExistingPart && NewPriority < ExistingPriority); +} + +void FGMS_BodyPartOverridePolicy::ApplyCoverage(EGMS_BodyMask BodyPart, TArray>& SelectedPoses, + const TInstancedStruct& NewPose, int32 NewPriority) const +{ + int32 BodyPartIndex = static_cast(BodyPart); + SelectedPoses[BodyPartIndex] = NewPose; +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ + check(IsValid(Setting)); + if (CurrentSetting != Setting || CurrentOverlayMode != GetParent()->OverlayMode) + { + ResetSetting(); + } + + if (const UGMS_AnimLayerSetting_Overlay_ParallelPoseStack* DS = Cast(Setting)) + { + if (CurrentSetting == DS && CurrentOverlayMode == GetParent()->OverlayMode) + { + return; + } + if (!DS->AcceleratedOverlayModes.Contains(GetParent()->OverlayMode)) + { + return; + } + const FGMS_OverlayModeSetting_ParallelPoseStack& ModeSetting = DS->AcceleratedOverlayModes[GetParent()->OverlayMode]; + if (ModeSetting.BasePose == nullptr) + { + bHasValidSetting = false; + UE_LOG(LogGMS, Warning, TEXT("BasePose is null for overlay mode %s"), *GetParent()->OverlayMode.ToString()); + return; + } + CurrentSetting = DS; + CurrentOverlayMode = GetParent()->OverlayMode; + bHasValidSetting = true; + + BasePose = ModeSetting.BasePose; + } +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::ResetSetting_Implementation() +{ + bHasValidSetting = false; + CurrentOverlayMode = FGameplayTag::EmptyTag; + CurrentSetting = nullptr; + BasePose = nullptr; + for (auto& Pose : SelectedBodyPoses) + { + Pose.GetMutable().Reset(); + } + LayeringState.ZeroOut(); +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); + // 预分配 SelectedBodyPoses + SelectedBodyPoses.SetNum(static_cast(EGMS_BodyMask::MAX)); + // 保证数组足够。 + SelectedBodyPoses = { + TInstancedStruct::Make(FGMS_AnimData_BodyPose_Head()), + TInstancedStruct::Make(FGMS_AnimData_BodyPose_ArmLeft()), + TInstancedStruct::Make(FGMS_AnimData_BodyPose_ArmRight()), + TInstancedStruct::Make(FGMS_AnimData_BodyPose_Upper()), + TInstancedStruct::Make(FGMS_AnimData_BodyPose_Lower()), + TInstancedStruct::Make(FGMS_AnimData_BodyPose_Full()), + }; +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::NativeThreadSafeUpdateAnimation(float DeltaSeconds) +{ + Super::NativeThreadSafeUpdateAnimation(DeltaSeconds); + + if (!bHasValidSetting) + { + LayeringState.ZeroOut(); + for (auto& Pose : SelectedBodyPoses) + { + Pose.GetMutable().Reset(); + } + return; + } + + // Check if tags or nodes have changed + SelectPoses(GetParent()->OwnedTags, GetParent()->NodeRelevanceTags); + + // Update LayeringState based on selected poses + UpdateLayeringState(DeltaSeconds); + UpdateLayeringSmoothState(DeltaSeconds); +} + +#define SELECT_POSE(PoseArray, PoseType, TargetPose, Nodes, Tags) \ +for (int32 Index = 0; Index < PoseArray.Num(); ++Index) \ +{ \ +const TInstancedStruct& PoseInst = PoseArray[Index];; \ +const PoseType& Pose = PoseInst.Get(); \ +if (!Pose.IsValid()) \ +{ \ +continue; \ +} \ +if (!Pose.RelevanceQuery.IsEmpty() && !Pose.RelevanceQuery.Matches(Nodes)) \ +{ \ +continue; \ +} \ +if (!Pose.TagQuery.IsEmpty() && !Pose.TagQuery.Matches(Tags)) \ +{ \ +continue; \ +} \ +if (!TargetPose.GetMutable().IsValid() || Index < TargetPose.GetMutable().Priority) \ +{ \ +TargetPose = PoseInst; \ +TargetPose.GetMutable().Priority = Index; \ +} \ +if (!TargetPose.GetMutable().IsValid()) \ +{ \ +TargetPose.GetMutable().Reset(); \ +} \ +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::SelectPoses(const FGameplayTagContainer& Tags, const FGameplayTagContainer& Nodes) +{ + //Reset SelectedBodyPoses + for (auto& Pose : SelectedBodyPoses) + { + Pose.GetMutable().Reset(); + } + + SELECT_POSE(GetOverlayModeSetting().FullBodyPoses, FGMS_AnimData_BodyPose_Full, SelectedBodyPoses[static_cast(EGMS_BodyMask::FullBody)], Nodes, Tags) + SELECT_POSE(GetOverlayModeSetting().UpperBodyPoses, FGMS_AnimData_BodyPose_Upper, SelectedBodyPoses[static_cast(EGMS_BodyMask::UpperBody)], Nodes, Tags) + SELECT_POSE(GetOverlayModeSetting().ArmLeftPoses, FGMS_AnimData_BodyPose_ArmLeft, SelectedBodyPoses[static_cast(EGMS_BodyMask::ArmLeft)], Nodes, Tags) + SELECT_POSE(GetOverlayModeSetting().ArmRightPoses, FGMS_AnimData_BodyPose_ArmRight, SelectedBodyPoses[static_cast(EGMS_BodyMask::ArmRight)], Nodes, Tags) + SELECT_POSE(GetOverlayModeSetting().HeadPoses, FGMS_AnimData_BodyPose_Head, SelectedBodyPoses[static_cast(EGMS_BodyMask::Head)], Nodes, Tags) +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::UpdateLayeringState(float DeltaSeconds) +{ + // Initialize LayeringState + // LayeringState.ZeroOut(); + LayeringState.HeadBlendAmount = 0.0f; + LayeringState.HeadAdditiveBlendAmount = 0.0f; + LayeringState.HeadSlotBlendAmount = 0.0f; + + LayeringState.ArmLeftBlendAmount = 0.0f; + LayeringState.ArmLeftAdditiveBlendAmount = 0.0f; + LayeringState.ArmLeftSlotBlendAmount = 0.0f; + + //Make this layer always enabled.(and override later) + // LayeringState.ArmLeftLocalSpaceBlendAmount = 1.0f; + // LayeringState.ArmLeftMeshSpaceBlendAmount = 1.0f; + + LayeringState.ArmRightBlendAmount = 0.0f; + LayeringState.ArmRightAdditiveBlendAmount = 0.0f; + LayeringState.ArmRightSlotBlendAmount = 0.0f; + + //Make this layer always enabled.(and override later) + // LayeringState.ArmRightLocalSpaceBlendAmount = 1.0f; + // LayeringState.ArmRightMeshSpaceBlendAmount = 1.0f; + + LayeringState.HandLeftBlendAmount = 0.0f; + LayeringState.HandRightBlendAmount = 0.0f; + + LayeringState.SpineBlendAmount = 0.0f; + LayeringState.SpineAdditiveBlendAmount = 0.0f; + LayeringState.SpineSlotBlendAmount = 0.0f; + + LayeringState.PelvisBlendAmount = 0.0f; + LayeringState.PelvisSlotBlendAmount = 0.0f; + + LayeringState.LegsBlendAmount = 0.0f; + LayeringState.LegsSlotBlendAmount = 0.0f; + + for (int32 i = SelectedBodyPoses.Num() - 1; i >= 0; i--) + { + if (!SelectedBodyPoses[i].IsValid()) + { + continue; + } + const FGMS_AnimData_BodyPose& Pose = SelectedBodyPoses[i].Get(); + Pose.UpdateLayeringState(LayeringState); + } +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::UpdateLayeringSmoothState(float DeltaSeconds) +{ + static constexpr float Speed = 2.0f; + + LayeringSmoothState.HeadBlendAmount = LayeringState.HeadBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.HeadBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.HeadBlendAmount; + + LayeringSmoothState.ArmLeftBlendAmount = LayeringState.ArmLeftBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.ArmLeftBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.ArmLeftBlendAmount; + + LayeringSmoothState.ArmRightBlendAmount = LayeringState.ArmRightBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.ArmRightBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.ArmRightBlendAmount; + + LayeringSmoothState.SpineBlendAmount = LayeringState.SpineBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.SpineBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.SpineBlendAmount; + + + LayeringSmoothState.PelvisBlendAmount = LayeringState.PelvisBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.PelvisBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.PelvisBlendAmount; + + LayeringSmoothState.LegsBlendAmount = LayeringState.LegsBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.LegsBlendAmount, 0.0f, DeltaSeconds, Speed) + : LayeringState.LegsBlendAmount; +} + +const FGMS_OverlayModeSetting_ParallelPoseStack& UGMS_AnimLayer_Overlay_ParallelPoseStack::GetOverlayModeSetting() const +{ + return CurrentSetting->AcceleratedOverlayModes[CurrentOverlayMode]; +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::UpdateAnim(const FAnimUpdateContext& Context, const FAnimNodeReference& Node, const EGMS_BodyMask& BodyMask, const FGMS_AnimData_BodyPose& BodyPose) +{ + if (BodyPose.Pose == nullptr) + { + GMS_LOG(Warning, "Trying to update an empty pose for BodyMask %s.", *UEnum::GetValueAsString(BodyMask)); + return; + } + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Succeeded && USequenceEvaluatorLibrary::GetSequence(SequenceEvaluator) != BodyPose.Pose) + { + GMS_LOG(Verbose, "Updated pose %s for BodyMask %s ", *GetNameSafe(BodyPose.Pose), *UEnum::GetValueAsString(BodyMask)); + USequenceEvaluatorLibrary::SetExplicitTime(SequenceEvaluator, BodyPose.PoseExplicitTime); + USequenceEvaluatorLibrary::SetSequenceWithInertialBlending(Context, SequenceEvaluator, BodyPose.Pose, 0.1f); + } +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::BasePose_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + if (!bHasValidSetting) + { + return; + } + const FGMS_OverlayModeSetting_ParallelPoseStack& ModeSetting = GetOverlayModeSetting(); + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + USequenceEvaluatorLibrary::SetExplicitTime(SequenceEvaluator, 0.0f); + USequenceEvaluatorLibrary::SetSequenceWithInertialBlending(Context, SequenceEvaluator, ModeSetting.BasePose); + } +} + +void UGMS_AnimLayer_Overlay_ParallelPoseStack::BodyPart_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node, EGMS_BodyMask BodyMask) +{ + if (!bHasValidSetting) + { + return; + } + + int32 BodyPartIndex = static_cast(BodyMask); + + if (SelectedBodyPoses[BodyPartIndex].Get().IsValid()) + { + UpdateAnim(Context, Node, BodyMask, SelectedBodyPoses[BodyPartIndex].Get()); + return; + } + + // 回退逻辑 + if (const TArray* FallbackParts = OverridePolicy.FallbackChain.Find(BodyMask)) + { + for (EGMS_BodyMask FallbackPart : *FallbackParts) + { + int32 FallbackIndex = static_cast(FallbackPart); + if (SelectedBodyPoses[FallbackIndex].Get().IsValid()) + { + UpdateAnim(Context, Node, BodyMask, SelectedBodyPoses[FallbackIndex].Get()); + return; + } + } + } +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.cpp new file mode 100644 index 0000000..3596fab --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.cpp @@ -0,0 +1,237 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "UObject/ObjectSaveContext.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Overlay_ParallelSequenceStack) + + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::NativeBeginPlay() +{ + Super::NativeBeginPlay(); + if (OverlayStackStates.IsEmpty()) + { + OverlayStackStates.AddDefaulted(MaxLayers); + } +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::NativeUpdateAnimation(float DeltaSeconds) +{ + Super::NativeUpdateAnimation(DeltaSeconds); +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::NativeThreadSafeUpdateAnimation(float DeltaSeconds) +{ + Super::NativeThreadSafeUpdateAnimation(DeltaSeconds); + RefreshRelevance(); + RefreshBlend(DeltaSeconds); +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ + check(IsValid(Setting)) + //setting changes or invalid, reset. + if (PrevSetting != Setting || PrevOverlayMode != GetParent()->OverlayMode) + { + PrevOverlayMode = FGameplayTag::EmptyTag; + ResetSetting(); + PrevSetting = nullptr; + } + + // Apply new data to anim instance. + if (const UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack* DS = Cast(Setting)) + { + //same setting and overlay. + if (PrevSetting == DS && PrevOverlayMode == GetParent()->OverlayMode) + { + return; + } + + PrevSetting = DS; + PrevOverlayMode = GetParent()->OverlayMode; + + if (OverlayStackStates.IsEmpty()) + { + OverlayStackStates.AddDefaulted(MaxLayers); + } + + if (!DS->AcceleratedOverlayModes.Contains(GetParent()->OverlayMode)) + { + return; + } + + const TArray& Stacks = DS->AcceleratedOverlayModes[GetParent()->OverlayMode].Stacks; + + for (int32 i = 0; i < OverlayStackStates.Num(); i++) + { + if (Stacks.IsValidIndex(i)) + { + OverlayStacks.EmplaceAt(i, Stacks[i]); + } + else + { + OverlayStacks.EmplaceAt(i, FGMS_ParallelSequenceStack()); + } + } + } +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::ResetSetting_Implementation() +{ + OverlayStacks.Reset(MaxLayers); + for (FGMS_ParallelSequenceStackState& OverlayStackState : OverlayStackStates) + { + OverlayStackState.Overlay.bValid = false; + } +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::RefreshRelevance() +{ + if (IsValid(GetParent())) + { + for (int32 i = 0; i < OverlayStackStates.Num(); i++) + { + if (OverlayStacks.IsValidIndex(i)) + { + OverlayStackStates[i].bRelevant = GetParent()->NodeRelevanceTags.HasAny(OverlayStacks[i].TargetAnimNodes); + } + else + { + OverlayStackStates[i].bRelevant = false; + } + } + } +} + +void UGMS_AnimLayer_Overlay_ParallelSequenceStack::RefreshBlend(float DeltaSeconds) +{ + if (IsValid(GetParent())) + { + const FGameplayTagContainer& Tags = GetParent()->OwnedTags; + + //Refresh current overlay. + for (int32 i = 0; i < OverlayStackStates.Num(); i++) + { + int32 foundJ = INDEX_NONE; + + if (OverlayStacks.IsValidIndex(i)) + { + for (int32 j = 0; j < OverlayStacks[i].Overlays.Num(); j++) + { + const FGMS_ParallelSequenceStackEntry& Overlay = OverlayStacks[i].Overlays[j]; + if (Overlay.bValid && (Overlay.TagQuery.IsEmpty() || Overlay.TagQuery.Matches(Tags))) + { + foundJ = j; + break; + } + } + } + + if (foundJ != INDEX_NONE) + { + if (OverlayStacks[i].Overlays[foundJ] != OverlayStackStates[i].Overlay) + { + OverlayStackStates[i].Overlay = OverlayStacks[i].Overlays[foundJ]; + OverlayStackStates[i].BlendOutSpeed = OverlayStacks[i].Overlays[foundJ].BlendOutSpeed; + } + } + else + { + OverlayStackStates[i].Overlay.bValid = false; + } + } + + //Refresh blends. + for (int32 i = 0; i < OverlayStackStates.Num(); i++) + { + FGMS_ParallelSequenceStackState& CurrentState = OverlayStackStates[i]; + if (CurrentState.Overlay.bValid && CurrentState.bRelevant) + { + if (CurrentState.Overlay.BlendInSpeed > 0) + { + CurrentState.BlendWeight = FMath::FInterpTo(CurrentState.BlendWeight, CurrentState.Overlay.BlendWeight, DeltaSeconds, CurrentState.Overlay.BlendInSpeed); + } + else + { + CurrentState.BlendWeight = CurrentState.Overlay.BlendWeight; + } + } + else + { + if (CurrentState.BlendOutSpeed > 0) + { + CurrentState.BlendWeight = FMath::FInterpTo(CurrentState.BlendWeight, 0.0f, DeltaSeconds, CurrentState.BlendOutSpeed); + } + else + { + CurrentState.BlendWeight = 0.0f; + } + } + } + } +} + + +#if WITH_EDITORONLY_DATA + +void FGMS_ParallelSequenceStackEntry::Validate() +{ + bValid = true; + if (Sequence == nullptr) + { + bValid = false; + EditorMessage = TEXT("Invalid Overlay,No valid sequence"); + return; + } + if (BlendMode == EGMS_LayeredBoneBlendMode::BlendMask) + { + if (Sequence->GetSkeleton() == nullptr || BlendMaskName == NAME_None) + { + bValid = false; + EditorMessage = TEXT("Invalid Overlay,No valid blend mask name!"); + return; + } + + UBlendProfile* BlendMask = Sequence->GetSkeleton()->GetBlendProfile(BlendMaskName); + if (BlendMask == nullptr) + { + bValid = false; + EditorMessage = TEXT("Invalid Overlay,The skeleton of animation doesn't have specified BlendMask"); + return; + } + } + + if (bValid) + { + EditorMessage = FString::Format(TEXT("Play ({0}) on ({1}) with condition({2})"), {Sequence->GetName(), BlendMaskName.ToString(), TagQuery.GetDescription()}); + } + else + { + EditorMessage = TEXT("Invalid Overlay"); + } +} + +void UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack::PreSave(FObjectPreSaveContext SaveContext) +{ + AcceleratedOverlayModes.Empty(); + for (FGMS_OverlayModeSetting_ParallelSequenceStack& Mode : OverlayModes) + { + for (FGMS_ParallelSequenceStack& Stack : Mode.Stacks) + { + for (FGMS_ParallelSequenceStackEntry& Overlay : Stack.Overlays) + { + Overlay.Validate(); + } + Stack.EditorFriendlyName = Stack.TargetAnimNodes.IsEmpty() + ? TEXT("Invalid Overlay") + : FString::Format(TEXT("Play overlay for states:[{0}]"), {Stack.TargetAnimNodes.ToStringSimple(false).Replace(TEXT("GMS.SM."), TEXT(""))}); + } + AcceleratedOverlayModes.Emplace(Mode.Tag, Mode); + } + + Super::PreSave(SaveContext); +} + +#endif diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_PoseStack.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_PoseStack.cpp new file mode 100644 index 0000000..4819ab7 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_PoseStack.cpp @@ -0,0 +1,387 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_Overlay_PoseStack.h" +#include "AnimationWarpingLibrary.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Utility/GMS_Log.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Overlay_PoseStack) + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGMS_AnimLayerSetting_Overlay_PoseStack::RunDataMigration(bool bResetDeprecatedSettings) +{ + Modify(); + PRAGMA_DISABLE_DEPRECATION_WARNINGS + for (FGMS_OverlayModeSetting_PoseStack& Mode : OverlayModes) + { + Mode.Poses.Empty(); + + // migrate simple pose. + if (Mode.PoseOverlaySettingType == EGMS_PoseOverlaySettingType::Simple) + { + if (Mode.SimplePoseSetting.IdlePose) + { + FGMS_PoseStackEntry Entry; + Entry.Pose = Mode.SimplePoseSetting.IdlePose; + Entry.ExplicitTime = Mode.SimplePoseSetting.IdlePoseExplicitTime; + Entry.AimingSweepPose = Mode.SimplePoseSetting.AimingSweepPose; + Entry.PoseBlend.ApplyFromSequence(Mode.SimplePoseSetting.IdlePose, Mode.SimplePoseSetting.IdlePoseExplicitTime); + Entry.RelevanceQuery = FGameplayTagQuery::BuildQuery(FGameplayTagQueryExpression().AnyTagsMatch().AddTag(FGameplayTag::RequestGameplayTag(TEXT("GMS.SM.Grounded.Idle")))); + Mode.Poses.Add(Entry); + } + if (Mode.SimplePoseSetting.MovingPose) + { + FGMS_PoseStackEntry Entry; + Entry.Pose = Mode.SimplePoseSetting.MovingPose; + Entry.ExplicitTime = Mode.SimplePoseSetting.MovingPoseExplicitTime; + Entry.AimingSweepPose = Mode.SimplePoseSetting.AimingSweepPose; + Entry.PoseBlend.ApplyFromSequence(Mode.SimplePoseSetting.MovingPose, Mode.SimplePoseSetting.MovingPoseExplicitTime); + Mode.Poses.Add(Entry); + } + } + // migrate layered pose. + if (Mode.PoseOverlaySettingType == EGMS_PoseOverlaySettingType::Layered) + { + for (const FGMS_AnimData_PoseOverlay_Layered& OldSetting : Mode.LayeredPoseSetting) + { + if (OldSetting.MovingPose) + { + // Has no idle tag. + FGameplayTagQueryExpression RelevanceExp = FGameplayTagQueryExpression().NoTagsMatch().AddTag(FGameplayTag::RequestGameplayTag(TEXT("GMS.SM.Grounded.Idle"))); + + FGMS_PoseStackEntry Entry; + Entry.Pose = OldSetting.MovingPose; + Entry.ExplicitTime = OldSetting.MovingPoseExplicitTime; + Entry.AimingSweepPose = OldSetting.AimingSweepPose; + Entry.PoseBlend.ApplyFromSequence(OldSetting.MovingPose, OldSetting.MovingPoseExplicitTime); + Entry.TagQuery = OldSetting.TagQuery; + Entry.RelevanceQuery = FGameplayTagQuery::BuildQuery(RelevanceExp); + Mode.Poses.Add(Entry); + } + + if (OldSetting.IdlePose) + { + // Must have idle tag. + FGameplayTagQueryExpression RelevanceExp = FGameplayTagQueryExpression().AllTagsMatch().AddTag(FGameplayTag::RequestGameplayTag(TEXT("GMS.SM.Grounded.Idle"))); + FGameplayTagQueryExpression Exp2; + OldSetting.TagQuery.GetQueryExpr(Exp2); + + FGMS_PoseStackEntry Entry; + Entry.Pose = OldSetting.IdlePose; + Entry.ExplicitTime = OldSetting.IdlePoseExplicitTime; + Entry.AimingSweepPose = OldSetting.AimingSweepPose; + Entry.PoseBlend.ApplyFromSequence(OldSetting.IdlePose, OldSetting.IdlePoseExplicitTime); + Entry.RelevanceQuery = FGameplayTagQuery::BuildQuery(FGameplayTagQueryExpression().AllExprMatch().AddExpr(RelevanceExp).AddExpr(Exp2)); + Mode.Poses.Add(Entry); + } + } + } + + if (bResetDeprecatedSettings) + { + Mode.SimplePoseSetting = FGMS_AnimData_PoseOverlay_Simple(); + Mode.LayeredPoseSetting.Empty(); + } + } + + PRAGMA_ENABLE_DEPRECATION_WARNINGS +} + + +void UGMS_AnimLayerSetting_Overlay_PoseStack::RunDataMigrationFromDefinition(UGMS_MovementDefinition* InDefinition, bool bResetDeprecatedSettings) +{ + if (IsValid(InDefinition)) + { + InDefinition->Modify(); + + for (TPair& MovementSet : InDefinition->MovementSets) + { + FGMS_MovementSetSetting& MSS = MovementSet.Value; + + if (MSS.bUseInstancedOverlaySetting && MSS.AnimLayerSetting_Overlay) + { + if (auto Overlay = Cast(MSS.AnimLayerSetting_Overlay)) + { + Overlay->RunDataMigration(bResetDeprecatedSettings); + } + } + else if (!MSS.bUseInstancedOverlaySetting && MSS.DA_AnimLayerSetting_Overlay) + { + if (auto Overlay = Cast(MSS.DA_AnimLayerSetting_Overlay)) + { + Overlay->RunDataMigration(bResetDeprecatedSettings); + } + } + } + } +} + +void UGMS_AnimLayerSetting_Overlay_PoseStack::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + + for (FGMS_OverlayModeSetting_PoseStack& OverlayMode : OverlayModes) + { + for (FGMS_PoseStackEntry& Entry : OverlayMode.Poses) + { + Entry.EditorFriendlyName = FString::Format(TEXT("{0} at {1}"), { + GetNameSafe(Entry.Pose), Entry.ExplicitTime + }); + } + } + + AcceleratedOverlayModes.Empty(); + for (const FGMS_OverlayModeSetting_PoseStack& PoseOverlay : OverlayModes) + { + AcceleratedOverlayModes.Emplace(PoseOverlay.Tag, PoseOverlay); + } +} +#endif + + +void FGMS_PerBodyPoseBlendSetting::ApplyFromSequence(const UAnimSequence* InSequence, float ExplicitTime) +{ + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHead", ExplicitTime, HeadBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHeadAdditive", ExplicitTime, HeadBlend.AdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHeadSlot", ExplicitTime, HeadBlend.SlotBlendAmount); + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeft", ExplicitTime, ArmLeftBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftAdditive", ExplicitTime, ArmLeftBlend.AdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftSlot", ExplicitTime, ArmLeftBlend.SlotBlendAmount); + float ArmLeftLocalSpaceBlendAmount = 0; + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftLocalSpace", ExplicitTime, ArmLeftLocalSpaceBlendAmount); + ArmLeftBlend.bMeshSpace = !FAnimWeight::IsFullWeight(ArmLeftLocalSpaceBlendAmount); + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRight", ExplicitTime, ArmRightBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightAdditive", ExplicitTime, ArmRightBlend.AdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightSlot", ExplicitTime, ArmRightBlend.SlotBlendAmount); + float ArmRightLocalSpaceBlendAmount = 0; + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightLocalSpace", ExplicitTime, ArmRightLocalSpaceBlendAmount); + ArmRightBlend.bMeshSpace = !FAnimWeight::IsFullWeight(ArmRightLocalSpaceBlendAmount); + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHandLeft", ExplicitTime, HandLeftBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHandRight", ExplicitTime, HandLeftBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpine", ExplicitTime, SpineBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpineAdditive", ExplicitTime, SpineBlend.AdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpineSlot", ExplicitTime, SpineBlend.SlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerPelvis", ExplicitTime, PelvisBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerPelvisSlot", ExplicitTime, PelvisBlend.SlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerLegs", ExplicitTime, LegsBlend.BlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerLegsSlot", ExplicitTime, LegsBlend.BlendAmount); +} + +void FGMS_PerBodyPoseBlendSetting::ApplyToLayeringState(FGMS_AnimState_Layering& InLayeringState) const +{ + InLayeringState.HeadBlendAmount = HeadBlend.BlendAmount; + InLayeringState.HeadAdditiveBlendAmount = HeadBlend.AdditiveBlendAmount; + InLayeringState.HeadSlotBlendAmount = HeadBlend.SlotBlendAmount; + + InLayeringState.ArmLeftBlendAmount = ArmLeftBlend.BlendAmount; + InLayeringState.ArmLeftAdditiveBlendAmount = ArmLeftBlend.AdditiveBlendAmount; + InLayeringState.ArmLeftSlotBlendAmount = ArmLeftBlend.SlotBlendAmount; + InLayeringState.ArmLeftLocalSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 0.f : 1.f; + InLayeringState.ArmLeftMeshSpaceBlendAmount = ArmLeftBlend.bMeshSpace ? 1.f : 0.f; + + InLayeringState.ArmRightBlendAmount = ArmRightBlend.BlendAmount; + InLayeringState.ArmRightAdditiveBlendAmount = ArmRightBlend.AdditiveBlendAmount; + InLayeringState.ArmRightSlotBlendAmount = ArmRightBlend.SlotBlendAmount; + InLayeringState.ArmRightLocalSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 0.f : 1.f; + InLayeringState.ArmRightMeshSpaceBlendAmount = ArmRightBlend.bMeshSpace ? 1.f : 0.f; + + InLayeringState.HandLeftBlendAmount = HandLeftBlend.BlendAmount; + InLayeringState.HandRightBlendAmount = HandRightBlend.BlendAmount; + + InLayeringState.SpineBlendAmount = SpineBlend.BlendAmount; + InLayeringState.SpineAdditiveBlendAmount = SpineBlend.AdditiveBlendAmount; + InLayeringState.SpineSlotBlendAmount = SpineBlend.SlotBlendAmount; + InLayeringState.PelvisBlendAmount = PelvisBlend.BlendAmount; + InLayeringState.PelvisSlotBlendAmount = PelvisBlend.SlotBlendAmount; + InLayeringState.LegsBlendAmount = LegsBlend.BlendAmount; + InLayeringState.LegsSlotBlendAmount = LegsBlend.SlotBlendAmount; +} + +void FGMS_PerBodyPoseBlendSetting::ApplyFromLayeringState(const FGMS_AnimState_Layering& InLayeringState) +{ + HeadBlend.BlendAmount = InLayeringState.HeadBlendAmount; + HeadBlend.AdditiveBlendAmount = InLayeringState.HeadAdditiveBlendAmount; + HeadBlend.SlotBlendAmount = InLayeringState.HeadSlotBlendAmount; + + ArmLeftBlend.BlendAmount = InLayeringState.ArmLeftBlendAmount; + ArmLeftBlend.AdditiveBlendAmount = InLayeringState.ArmLeftAdditiveBlendAmount; + ArmLeftBlend.SlotBlendAmount = InLayeringState.ArmLeftSlotBlendAmount; + ArmLeftBlend.bMeshSpace = InLayeringState.ArmLeftMeshSpaceBlendAmount > 0.f; + + ArmRightBlend.BlendAmount = InLayeringState.ArmRightBlendAmount; + ArmRightBlend.AdditiveBlendAmount = InLayeringState.ArmRightAdditiveBlendAmount; + ArmRightBlend.SlotBlendAmount = InLayeringState.ArmRightSlotBlendAmount; + ArmRightBlend.bMeshSpace = InLayeringState.ArmRightMeshSpaceBlendAmount > 0.f; + + HandLeftBlend.BlendAmount = InLayeringState.HandLeftBlendAmount; + HandRightBlend.BlendAmount = InLayeringState.HandRightBlendAmount; + + SpineBlend.BlendAmount = InLayeringState.SpineBlendAmount; + SpineBlend.AdditiveBlendAmount = InLayeringState.SpineAdditiveBlendAmount; + SpineBlend.SlotBlendAmount = InLayeringState.SpineSlotBlendAmount; + + PelvisBlend.BlendAmount = InLayeringState.PelvisBlendAmount; + PelvisBlend.SlotBlendAmount = InLayeringState.PelvisSlotBlendAmount; + + LegsBlend.BlendAmount = InLayeringState.LegsBlendAmount; + LegsBlend.SlotBlendAmount = InLayeringState.LegsSlotBlendAmount; +} + +bool UGMS_AnimLayerSetting_Overlay_PoseStack::IsValidForOverlayMode(const FGameplayTag& NewOverlayMode) const +{ + return AcceleratedOverlayModes.Contains(NewOverlayMode) && AcceleratedOverlayModes[NewOverlayMode].BasePose != nullptr; +} + +void UGMS_AnimLayer_Overlay_PoseStack::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ + check(IsValid(Setting)); + if (CurrentSetting != Setting || CurrentOverlayMode != GetParent()->OverlayMode) + { + ResetSetting(); + } + if (const UGMS_AnimLayerSetting_Overlay_PoseStack* DS = Cast(Setting)) + { + if (CurrentSetting == DS && CurrentOverlayMode == GetParent()->OverlayMode) + { + return; + } + if (!DS->AcceleratedOverlayModes.Contains(GetParent()->OverlayMode)) + { + return; + } + const FGMS_OverlayModeSetting_PoseStack& ModeSetting = DS->AcceleratedOverlayModes[GetParent()->OverlayMode]; + if (ModeSetting.BasePose == nullptr) + { + bHasValidSetting = false; + UE_LOG(LogGMS, Warning, TEXT("BasePose is null for overlay mode %s"), *GetParent()->OverlayMode.ToString()); + return; + } + CurrentSetting = DS; + CurrentOverlayMode = GetParent()->OverlayMode; + bHasValidSetting = true; + BasePose = ModeSetting.BasePose; + } +} + +void UGMS_AnimLayer_Overlay_PoseStack::ResetSetting_Implementation() +{ + bHasValidSetting = false; + CurrentOverlayMode = FGameplayTag::EmptyTag; + CurrentSetting = nullptr; + BasePose = nullptr; + LayeringState.ZeroOut(); +} + +bool UGMS_AnimLayer_Overlay_PoseStack::SelectPose() +{ + const FGMS_OverlayModeSetting_PoseStack& OS = GetOverlayModeSetting(); + BasePose = OS.BasePose; + + for (int32 i = 0; i < OS.Poses.Num(); i++) + { + const FGMS_PoseStackEntry& Entry = OS.Poses[i]; + bool bMatchesOwnedTags = Entry.TagQuery.IsEmpty() || Entry.TagQuery.Matches(GetParent()->OwnedTags); + bool bMatchesRelevanceTags = Entry.RelevanceQuery.IsEmpty() || Entry.RelevanceQuery.Matches(GetParent()->NodeRelevanceTags); + if (bMatchesOwnedTags && bMatchesRelevanceTags) + { + Pose = Entry.Pose; + ExplicitTime = Entry.ExplicitTime; + AimingSweepPose = Entry.AimingSweepPose; + bValidAimingPose = AimingSweepPose != nullptr; + Entry.PoseBlend.ApplyToLayeringState(LayeringState); + return true; + } + } + return false; +} + +void UGMS_AnimLayer_Overlay_PoseStack::NativeThreadSafeUpdateAnimation(float DeltaSeconds) +{ + Super::NativeThreadSafeUpdateAnimation(DeltaSeconds); + + if (!bHasValidSetting) + { + return; + } + + if (!SelectPose()) + { + Pose = nullptr; + ExplicitTime = 0.0f; + bValidPose = false; + AimingSweepPose = nullptr; + bValidAimingPose = false; + LayeringState.ZeroOut(); + } + bValidPose = Pose ? true : false; + UpdateLayeringSmoothState(DeltaSeconds); +} + +const FGMS_OverlayModeSetting_PoseStack& UGMS_AnimLayer_Overlay_PoseStack::GetOverlayModeSetting() const +{ + return CurrentSetting->AcceleratedOverlayModes[CurrentOverlayMode]; +} + +void UGMS_AnimLayer_Overlay_PoseStack::UpdateLayeringSmoothState(float DeltaSeconds) +{ + if (LayeringSmoothSpeed <= 0.0f) + { + LayeringSmoothState.HeadBlendAmount = LayeringState.HeadBlendAmount; + + LayeringSmoothState.ArmLeftBlendAmount = LayeringState.ArmLeftBlendAmount; + + LayeringSmoothState.ArmRightBlendAmount = LayeringState.ArmRightBlendAmount; + + LayeringSmoothState.SpineBlendAmount = LayeringState.SpineBlendAmount; + + + LayeringSmoothState.PelvisBlendAmount = LayeringState.PelvisBlendAmount; + + LayeringSmoothState.LegsBlendAmount = LayeringState.LegsBlendAmount; + return; + } + + LayeringSmoothState.HeadBlendAmount = LayeringState.HeadBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.HeadBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.HeadBlendAmount; + + LayeringSmoothState.ArmLeftBlendAmount = LayeringState.ArmLeftBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.ArmLeftBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.ArmLeftBlendAmount; + + LayeringSmoothState.ArmRightBlendAmount = LayeringState.ArmRightBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.ArmRightBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.ArmRightBlendAmount; + + LayeringSmoothState.SpineBlendAmount = LayeringState.SpineBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.SpineBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.SpineBlendAmount; + + + LayeringSmoothState.PelvisBlendAmount = LayeringState.PelvisBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.PelvisBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.PelvisBlendAmount; + + LayeringSmoothState.LegsBlendAmount = LayeringState.LegsBlendAmount <= 0.0f + ? FMath::FInterpConstantTo(LayeringSmoothState.LegsBlendAmount, 0.0f, DeltaSeconds, LayeringSmoothSpeed) + : LayeringState.LegsBlendAmount; + + // LayeringSmoothState.HeadBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.HeadBlendAmount, LayeringState.HeadBlendAmount, DeltaSeconds, LayeringSmoothSpeed); + // + // LayeringSmoothState.ArmLeftBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.ArmLeftBlendAmount, LayeringState.ArmLeftBlendAmount, DeltaSeconds, LayeringSmoothSpeed); + // + // LayeringSmoothState.ArmRightBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.ArmRightBlendAmount, LayeringState.ArmRightBlendAmount, DeltaSeconds, LayeringSmoothSpeed); + // + // LayeringSmoothState.SpineBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.SpineBlendAmount, LayeringState.SpineBlendAmount, DeltaSeconds, LayeringSmoothSpeed); + // + // + // LayeringSmoothState.PelvisBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.PelvisBlendAmount, LayeringState.PelvisBlendAmount, DeltaSeconds, LayeringSmoothSpeed); + // + // LayeringSmoothState.LegsBlendAmount = FMath::FInterpConstantTo(LayeringSmoothState.LegsBlendAmount, LayeringState.LegsBlendAmount, DeltaSeconds, LayeringSmoothSpeed); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.cpp new file mode 100644 index 0000000..ea3cdaa --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.cpp @@ -0,0 +1,207 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h" + +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Utility/GMS_Log.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_Overlay_SequenceStack) +#if WITH_EDITOR +#include "Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.h" +#include "UObject/ObjectSaveContext.h" + +void UGMS_AnimLayerSetting_Overlay_SequenceStack::ConvertToSequenceStack(const UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack* Src) +{ + if (!IsValid(Src)) + { + return; + } + + for (auto& Pair : Src->AcceleratedOverlayModes) + { + auto& OldMode = Pair.Value; + FGMS_OverlayModeSetting_SequenceStack NewMode; + NewMode.Tag = OldMode.Tag; + for (int32 i = OldMode.Stacks.Num() - 1; i >= 0; i--) + { + const FGMS_ParallelSequenceStack& OldStack = OldMode.Stacks[i]; + for (int32 j = 0; j < OldStack.Overlays.Num(); j++) + { + const FGMS_ParallelSequenceStackEntry& OldEntry = OldStack.Overlays[j]; + UAnimSequence* OldSequence = Cast(OldEntry.Sequence); + if (OldSequence == nullptr) + { + continue; + } + + FGMS_SequenceStackEntry NewEntry; + NewEntry.RelevanceQuery = FGameplayTagQuery::BuildQuery(FGameplayTagQueryExpression().AnyTagsMatch().AddTags(OldStack.TargetAnimNodes)); + NewEntry.TagQuery = OldEntry.TagQuery; + NewEntry.Sequence = OldSequence; + NewEntry.PlayMode = OldEntry.PlayMode; + NewEntry.BlendWeight = OldEntry.BlendWeight; + NewEntry.MeshSpaceBlend = OldEntry.MeshSpaceBlend; + NewEntry.AnimationTime = OldEntry.PlayMode == EGMS_OverlayPlayMode::SequenceEvaluator ? OldEntry.ExplicitTime : OldEntry.StartPosition; + NewEntry.BlendMode = OldEntry.BlendMode; + NewEntry.BlendMaskName = OldEntry.BlendMaskName; + NewEntry.BranchFilters = OldEntry.BranchFilters; + NewEntry.BlendTime = 0.2f; + NewMode.Sequences.Add(NewEntry); + } + } + OverlayModes.Add(NewMode); + } +} + +void UGMS_AnimLayerSetting_Overlay_SequenceStack::ConvertToSequenceStackFromDefinition(UGMS_MovementDefinition* InDefinition) +{ + if (IsValid(InDefinition)) + { + InDefinition->Modify(); + + for (TPair& MovementSet : InDefinition->MovementSets) + { + FGMS_MovementSetSetting& MSS = MovementSet.Value; + + if (MSS.bUseInstancedOverlaySetting && MSS.AnimLayerSetting_Overlay) + { + if (auto Src = Cast(MSS.AnimLayerSetting_Overlay)) + { + auto NewSequenceStack = NewObject(InDefinition, StaticClass()); + NewSequenceStack->ConvertToSequenceStack(Src);; + MSS.AnimLayerSetting_Overlay = NewSequenceStack; + } + } + else if (!MSS.bUseInstancedOverlaySetting && MSS.DA_AnimLayerSetting_Overlay) + { + if (auto Src = Cast(MSS.DA_AnimLayerSetting_Overlay)) + { + auto NewSequenceStack = NewObject(InDefinition, StaticClass()); + NewSequenceStack->ConvertToSequenceStack(Src);; + MSS.DA_AnimLayerSetting_Overlay = nullptr; + MSS.bUseInstancedOverlaySetting = true; + MSS.AnimLayerSetting_Overlay = NewSequenceStack; + } + } + } + } +} + +void UGMS_AnimLayerSetting_Overlay_SequenceStack::PreSave(FObjectPreSaveContext SaveContext) +{ + AcceleratedOverlayModes.Empty(); + for (FGMS_OverlayModeSetting_SequenceStack& Mode : OverlayModes) + { + for (FGMS_SequenceStackEntry& Entry : Mode.Sequences) + { + Entry.EditorFriendlyName = FString::Format(TEXT("{0} "), { + GetNameSafe(Entry.Sequence) + }); + } + AcceleratedOverlayModes.Emplace(Mode.Tag, Mode); + } + + Super::PreSave(SaveContext); +} +#endif + + +void UGMS_AnimLayer_Overlay_SequenceStack::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ + check(IsValid(Setting)); + if (CurrentSetting != Setting || CurrentOverlayMode != GetParent()->OverlayMode) + { + ResetSetting(); + } + if (const UGMS_AnimLayerSetting_Overlay_SequenceStack* DS = Cast(Setting)) + { + if (CurrentSetting == DS && CurrentOverlayMode == GetParent()->OverlayMode) + { + return; + } + if (!DS->AcceleratedOverlayModes.Contains(GetParent()->OverlayMode)) + { + return; + } + const FGMS_OverlayModeSetting_SequenceStack& ModeSetting = DS->AcceleratedOverlayModes[GetParent()->OverlayMode]; + CurrentSetting = DS; + CurrentOverlayMode = GetParent()->OverlayMode; + bHasValidSetting = true; + } +} + +void UGMS_AnimLayer_Overlay_SequenceStack::ResetSetting_Implementation() +{ + Definition = FGMS_SequenceStackEntry(); + BlendWeight = 0.0f; + BlendProfile = nullptr; + + bHasValidSetting = false; + CurrentOverlayMode = FGameplayTag::EmptyTag; + CurrentSetting = nullptr; +} + +bool UGMS_AnimLayer_Overlay_SequenceStack::SelectSequence() +{ + const FGMS_OverlayModeSetting_SequenceStack& OS = GetOverlayModeSetting(); + + for (int32 i = 0; i < OS.Sequences.Num(); i++) + { + const FGMS_SequenceStackEntry& Entry = OS.Sequences[i]; + bool bMatchesOwnedTags = Entry.TagQuery.IsEmpty() || Entry.TagQuery.Matches(GetParent()->OwnedTags); + bool bMatchesRelevanceTags = Entry.RelevanceQuery.IsEmpty() || Entry.RelevanceQuery.Matches(GetParent()->NodeRelevanceTags); + if (bMatchesOwnedTags && bMatchesRelevanceTags) + { + Definition = Entry; + BlendOutSpeed = Definition.BlendOutSpeed; + BlendProfile = GetParent()->GetNamedBlendProfile(Definition.BlendProfile); + bHasValidDefinition = true; + + return true; + } + } + return false; +} + +void UGMS_AnimLayer_Overlay_SequenceStack::NativeThreadSafeUpdateAnimation(float DeltaSeconds) +{ + Super::NativeThreadSafeUpdateAnimation(DeltaSeconds); + if (!bHasValidSetting) + { + bHasValidDefinition = false; + return; + } + + if (!SelectSequence()) + { + bHasValidDefinition = false; + } + if (bHasValidDefinition) + { + if (Definition.BlendInSpeed > 0) + { + BlendWeight = FMath::FInterpTo(BlendWeight, Definition.BlendWeight, DeltaSeconds, Definition.BlendInSpeed); + } + else + { + BlendWeight = Definition.BlendWeight; + } + } + else + { + if (BlendOutSpeed > 0) + { + BlendWeight = FMath::FInterpTo(BlendWeight, 0.0f, DeltaSeconds, BlendOutSpeed); + } + else + { + BlendWeight = 0.0f; + } + } +} + +const FGMS_OverlayModeSetting_SequenceStack& UGMS_AnimLayer_Overlay_SequenceStack::GetOverlayModeSetting() const +{ + return CurrentSetting->AcceleratedOverlayModes[CurrentOverlayMode]; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_SkeletalControls.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_SkeletalControls.cpp new file mode 100644 index 0000000..a6b6346 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_SkeletalControls.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_SkeletalControls.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_SkeletalControls) diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States.cpp new file mode 100644 index 0000000..5259dea --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States.cpp @@ -0,0 +1,12 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_States.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_States) + + + +void FGMS_AnimData::Validate() +{ +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.cpp new file mode 100644 index 0000000..cd4c675 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.cpp @@ -0,0 +1,1791 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h" +#include "Runtime/Launch/Resources/Version.h" +#include "AnimationStateMachineLibrary.h" +#include "GameFramework/Pawn.h" +#include "AnimCharacterMovementLibrary.h" +#include "AnimDistanceMatchingLibrary.h" +#include "GMS_MovementSystemComponent.h" +#include "KismetAnimationLibrary.h" +#include "Animation/AnimSequence.h" +#include "SequenceEvaluatorLibrary.h" +#include "SequencePlayerLibrary.h" +#include "AnimNodes/AnimNode_SequenceEvaluator.h" +#include "BlendStack/AnimNode_BlendStack.h" +#include "BlendStack/BlendStackAnimNodeLibrary.h" +#include "BlendStack/BlendStackInputAnimNodeLibrary.h" +#include "Kismet/KismetMathLibrary.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Misc/DataValidation.h" +#include "Utility/GMS_Log.h" +#include "Utility/GMS_Math.h" +#include "Utility/GMS_Rotation.h" +#include "Utility/GMS_Utility.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_States_DefaultLocomotion) + +#pragma region Setting + + +void FGMS_AnimData_Jump::Validate() +{ + Super::Validate(); + bValidJumpStartLoop = IsValid(JumpStartLoop); + bValidJumpApex = IsValid(JumpApex); + bValidJumpFallLoop = IsValid(JumpFallLoop); + bValidJumpFallLand = IsValid(JumpFallLand); + // bValid = bValidJumpStartLoop && bValidJumpApex && bValidJumpFallLand && IsValid(JumpStart); + if (JumpStartType == EGMS_JumpStartAnimType::Single) + { + bValid = IsValid(JumpStart); + } + if (JumpStartType == EGMS_JumpStartAnimType::Direction) + { + bValid = JumpStarts.ValidAnimations(); + } +} + + +void FGMS_AnimData_Idle::Validate() +{ + Super::Validate(); + bValid = Idle != nullptr; + bValidCrouchAnim = CrouchEntry != nullptr && CrouchExit != nullptr; +} + +void FGMS_AnimData_Start_ViewDirection::Validate() +{ + Super::Validate(); + bValid = Animations.Forward && Animations.Backward && Animations.Left && Animations.Right; +} + +void FGMS_AnimData_Start_VelocityDirection::Validate() +{ + Super::Validate(); + if (AnimType == EGMS_StartAnimType_VelocityDir::Reface) + { + bValid = Animations.StartForward && Animations.StartForwardL90 && Animations.StartForwardL180 && Animations.StartForwardR90 && Animations.StartForwardR180; + } + if (AnimType == EGMS_StartAnimType_VelocityDir::Single) + { + bValid = Animation != nullptr; + } +} + +void FGMS_AnimData_Cycle::Validate() +{ + Super::Validate(); + + if (AnimType == EGMS_CycleAnimType::Direction_4) + { + bValid = Animations.ValidAnimations(); + if (bValid) + { + // bHasRootMotion = Animations.HasRootMotion(); + } + } + + if (AnimType == EGMS_CycleAnimType::Direction_8) + { + bValid = Animations_8Direction.ValidAnimations(); + if (bValid) + { + // bHasRootMotion = Animations_8Direction.HasRootMotion(); + } + } + + if (AnimType == EGMS_CycleAnimType::Single) + { + bValid = Animation != nullptr; + if (bValid) + { + // bHasRootMotion = Animation->HasRootMotion(); + } + } +} + +void FGMS_AnimData_Stop::Validate() +{ + Super::Validate(); + + if (AnimType == EGMS_StopAnimType::Single) + { + bValid = Animation != nullptr; + } + if (AnimType == EGMS_StopAnimType::Direction_4) + { + bValid = Animations.Forward && Animations.Backward && Animations.Left && Animations.Right; + } + if (AnimType == EGMS_StopAnimType::Direction_8) + { + bValid = Animations_8Direction.Forward && Animations_8Direction.Backward && Animations_8Direction.Left && Animations_8Direction.Right + && Animations_8Direction.ForwardLeft && Animations_8Direction.ForwardRight && Animations_8Direction.BackwardLeft && Animations_8Direction.BackwardRight; + } +} + +void FGMS_AnimData_Pivot::Validate() +{ + Super::Validate(); + bValid = Animations.Forward && Animations.Backward && Animations.Left && Animations.Right; +} + + +void FGMS_AnimData_Land::Validate() +{ + Super::Validate(); + bValid = !Lands.IsEmpty(); + +#if WITH_EDITORONLY_DATA + for (FGMS_AnimationWithDistance& Anim : Lands) + { + if (Anim.Animation != nullptr) + { + Anim.EditorFriendlyName = FString::Format(TEXT("Play {0} With Fall Velocity:{1}"), {Anim.Animation->GetName(), Anim.Distance}); + } + else + { + Anim.EditorFriendlyName = TEXT("Empty Anim"); + } + } +#endif +} + + +void FGMS_AnimData_Lean::Validate() +{ + Super::Validate(); + bValid = BlendSpace != nullptr; +} + +void FGMS_AnimData_TurnInPlace::Validate() +{ + Super::Validate(); + bValid = Left != nullptr && Right != nullptr; +} + +bool UGMS_AnimLayerSetting_States_Default::GetOverrideAnimLayerClass_Implementation(TSubclassOf& OutLayerClass) const +{ + FSoftClassPath AnimLayerClassPath = FSoftClassPath(TEXT("/Game/GenericGame/MovementSystem/Core/AnimLayers/ABPT_GMS_Layer_States_Default.ABPT_GMS_Layer_States_Default_C")); + if (TSubclassOf LoadedClass = AnimLayerClassPath.TryLoadClass()) + { + OutLayerClass = LoadedClass; + return true; + } + GMS_LOG(Warning, "Failed to load anim layer class for %s at path:%s", *GetClass()->GetName(), *AnimLayerClassPath.ToString()); + return false; +} + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGMS_AnimLayerSetting_States_Default::PreSave(FObjectPreSaveContext SaveContext) +{ + if (Idle_Inst.IsValid()) + { + if (FGMS_AnimData* Data = Idle_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + + if (Turn_Inst.IsValid()) + { + if (FGMS_AnimData* Data = Turn_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + + if (Jump_Inst.IsValid()) + { + if (FGMS_AnimData* Data = Jump_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + + if (Land_Inst.IsValid()) + { + if (FGMS_AnimData* Data = Land_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + + if (Lean_Inst.IsValid()) + { + if (FGMS_AnimData* Data = Lean_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + AcceleratedMovingStates.Empty(); + + for (FGMS_AnimData_MovingStates& GS : MovingStates) + { + if (GS.Start_ViewDir_Inst.IsValid()) + { + if (FGMS_AnimData* Data = GS.Start_ViewDir_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + if (GS.Start_VelocityDir_Inst.IsValid()) + { + if (FGMS_AnimData* Data = GS.Start_VelocityDir_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + if (GS.Cycle_Inst.IsValid()) + { + if (FGMS_AnimData* Data = GS.Cycle_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + if (GS.Stop_Inst.IsValid()) + { + if (FGMS_AnimData* Data = GS.Stop_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + if (GS.Pivot_Inst.IsValid()) + { + if (FGMS_AnimData* Data = GS.Pivot_Inst.GetMutablePtr()) + { + Data->Validate(); + } + } + AcceleratedMovingStates.Emplace(GS.Tag, GS); + } + Super::PreSave(SaveContext); +} + +EDataValidationResult UGMS_AnimLayerSetting_States_Default::IsDataValid(class FDataValidationContext& Context) const +{ + if (MovingStates.IsEmpty()) + { + Context.AddError(FText::FromString(TEXT("Moving states can't be empty!"))); + return EDataValidationResult::Invalid; + } + return Super::IsDataValid(Context); +} + + +#endif + +#pragma endregion + +UGMS_AnimLayer_States_DefaultLocomotion::UGMS_AnimLayer_States_DefaultLocomotion() +{ + RootMotionMode = ERootMotionMode::RootMotionFromMontagesOnly; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::NativeThreadSafeUpdateAnimation(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_AnimLayer_States_DefaultLocomotion::NativeThreadSafeUpdateAnimation"), STAT_GMS_DefaultLocomotion_NativeThreadSafeUpdateAnimation, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + Super::NativeThreadSafeUpdateAnimation(DeltaTime); + + if (!PawnOwner || !IsValid(MovementSystem) || !IsValid(GetParent())) + { + return; + } + + PivotState.Direction2D = UKismetMathLibrary::VLerp(PivotState.Direction2D, GetParent()->MovementIntent.GetSafeNormal2D(), 0.5).GetSafeNormal(); + PivotState.DesiredDirection = GetOppositeCardinalDirection(SelectCardinalDirectionFromAngle( + UKismetAnimationLibrary::CalculateDirection(PivotState.Direction2D, GetParent()->LocomotionState.Rotation), + 10, EGMS_MovementDirection::Forward, false)); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::NativePostEvaluateAnimation() +{ + Super::NativePostEvaluateAnimation(); + + TurnInPlaceState.bUpdatedThisFrame = false; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::ApplySetting_Implementation(const UGMS_AnimLayerSetting* NewSetting) +{ + if (GetParent() == nullptr) + { + return; + } + + const UGMS_AnimLayerSetting_States_Default* DS = Cast(NewSetting); + + if (!DS) + { + ResetSetting(); + return; + } + + Setting = DS; + + if (IsValid(MovementSystem->AnimGraphSetting)) + { + OWSettings = MovementSystem->AnimGraphSetting->OrientationWarping; + } + + if (DS->Idle_Inst.IsValid()) + { + AnimData_Idle = DS->Idle_Inst.Get(); + } + else + { + AnimData_Idle = FGMS_AnimData_Idle(); + } + + if (DS->Turn_Inst.IsValid()) + { + AnimData_TurnInPlace = DS->Turn_Inst.Get(); + } + else + { + AnimData_TurnInPlace = FGMS_AnimData_TurnInPlace(); + } + + if (DS->Jump_Inst.IsValid()) + { + AnimData_Jump = DS->Jump_Inst.Get(); + bEnableJump = AnimData_Jump.bValid; + bEnableFall = AnimData_Jump.bValidJumpFallLoop; + } + else + { + AnimData_Jump = FGMS_AnimData_Jump(); + bEnableJump = false; + bEnableFall = false; + } + + GetParent()->bEnableGroundPrediction = AnimData_Jump.bValidJumpFallLand && AnimData_Jump.bEnableGroundPrediction; + + if (DS->Land_Inst.IsValid()) + { + AnimData_Land = DS->Land_Inst.Get(); + bEnableLand = AnimData_Land.bValid; + } + else + { + AnimData_Land = FGMS_AnimData_Land(); + bEnableLand = false; + } + + if (DS->Lean_Inst.IsValid()) + { + AnimData_GroundedLean = DS->Lean_Inst.Get(); + } + else + { + AnimData_GroundedLean = FGMS_AnimData_Lean(); + } + + checkf(!DS->MovingStates.IsEmpty(), TEXT("Moving states on %s can't be empty!"), *DS->GetName()); + + //Apply gounrded states. + { + AnimData_MovingStates = DS->AcceleratedMovingStates.Contains(GetParent()->MovementState) ? DS->AcceleratedMovingStates[GetParent()->MovementState] : DS->MovingStates.Last(); + + { + if (GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection) + { + if (AnimData_MovingStates.Start_ViewDir_Inst.IsValid()) + { + AnimData_Start_ViewDirection = AnimData_MovingStates.Start_ViewDir_Inst.Get(); + bEnableStart = AnimData_Start_ViewDirection.bValid; + } + else + { + AnimData_Start_ViewDirection = FGMS_AnimData_Start_ViewDirection(); + bEnableStart = false; + } + } + + if (GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection) + { + if (AnimData_MovingStates.Start_VelocityDir_Inst.IsValid()) + { + AnimData_Start_VelocityDirection = AnimData_MovingStates.Start_VelocityDir_Inst.Get(); + bEnableStart = AnimData_Start_VelocityDirection.bValid; + } + else + { + AnimData_Start_VelocityDirection = FGMS_AnimData_Start_VelocityDirection(); + bEnableStart = false; + } + } + } + + if (AnimData_MovingStates.Cycle_Inst.IsValid()) + { + AnimData_Cycle = AnimData_MovingStates.Cycle_Inst.Get(); + } + else + { + GMS_ANIMATION_CLOG(Error, "Missing AnimData_Cycle for movement state(%s)!", *GetParent()->MovementState.ToString()) + AnimData_Cycle = FGMS_AnimData_Cycle(); + } + + if (AnimData_MovingStates.Stop_Inst.IsValid()) + { + AnimData_Stop = AnimData_MovingStates.Stop_Inst.Get(); + bEnableStop = AnimData_Stop.bValid; + } + else + { + AnimData_Stop = FGMS_AnimData_Stop(); + bEnableStop = false; + } + + if (AnimData_MovingStates.Pivot_Inst.IsValid()) + { + AnimData_Pivot = AnimData_MovingStates.Pivot_Inst.Get(); + bEnablePivot = AnimData_Pivot.bValid; + } + else + { + AnimData_Pivot = FGMS_AnimData_Pivot(); + bEnablePivot = false; + } + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::ResetSetting_Implementation() +{ + OWSettings = FGMS_OrientationWarpingSettings(); + AnimData_Idle = FGMS_AnimData_Idle(); + AnimData_TurnInPlace = FGMS_AnimData_TurnInPlace(); + AnimData_Start_ViewDirection = FGMS_AnimData_Start_ViewDirection(); + AnimData_Start_VelocityDirection = FGMS_AnimData_Start_VelocityDirection(); + AnimData_Cycle = FGMS_AnimData_Cycle(); + AnimData_GroundedLean = FGMS_AnimData_Lean(); + AnimData_Stop = FGMS_AnimData_Stop(); + AnimData_Pivot = FGMS_AnimData_Pivot(); + AnimData_Jump = FGMS_AnimData_Jump(); + AnimData_Land = FGMS_AnimData_Land(); + + bEnableStart = false; + bEnableLand = false; + bEnablePivot = false; + bEnableStop = false; + if (IsValid(GetParent())) + { + GetParent()->bEnableGroundPrediction = false; + } + bEnableJump = false; + bEnableFall = false; +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsMovingPerpendicularToInitialPivot() const +{ + //We stay in a pivot when pivoting along a line (e.g. triggering a left-right pivot while playing a right-left pivot), but break out if the character makes a perpendicular change in direction. + EGMS_MovementDirection CurrentDirection = GetParent()->LocomotionState.LocalVelocityDirection; + bool A = PivotState.InitialDirection == EGMS_MovementDirection::Forward || PivotState.InitialDirection == EGMS_MovementDirection::Backward; + bool B = !(CurrentDirection == EGMS_MovementDirection::Forward || CurrentDirection == EGMS_MovementDirection::Backward); + bool C = PivotState.InitialDirection == EGMS_MovementDirection::Left || PivotState.InitialDirection == EGMS_MovementDirection::Right; + bool D = !(CurrentDirection == EGMS_MovementDirection::Left || CurrentDirection == EGMS_MovementDirection::Right); + return (A && B) || (C && D); +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsPivoting_Implementation() const +{ + return GetParent()->LocomotionMode == GMS_MovementModeTags::Grounded && UKismetMathLibrary::Dot_VectorVector(GetParent()->LocomotionState.Velocity.GetSafeNormal2D(), + GetParent()->MovementIntent.GetSafeNormal2D()) < 0.0f; +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsStarting_Implementation() const +{ + return GetParent()->LocomotionState.bHasInput || GetParent()->LocomotionState.bHasVelocity; +} + + +void UGMS_AnimLayer_States_DefaultLocomotion::OnLinked_Implementation() +{ + Super::OnLinked_Implementation(); + if (IsValid(MovementSystem)) + { + MovementSystem->OnRotationModeChangedEvent.AddDynamic(this, &ThisClass::OnRotationModeChanged); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::OnUnlinked_Implementation() +{ + if (IsValid(MovementSystem)) + { + MovementSystem->OnRotationModeChangedEvent.RemoveDynamic(this, &ThisClass::OnRotationModeChanged); + } + Super::OnUnlinked_Implementation(); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::OnRotationModeChanged(const FGameplayTag& PrevMode) +{ +} + +EGMS_MovementDirection UGMS_AnimLayer_States_DefaultLocomotion::SelectCardinalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection CurrentDirection, bool bUseCurrentDirection) const +{ + const float AbsAngle = FMath::Abs(Angle); + float FwdDeadZone = DeadZone; + float BwdDeadZone = DeadZone; + if (bUseCurrentDirection) + { + if (CurrentDirection == EGMS_MovementDirection::Forward) + { + FwdDeadZone *= 2; + } + if (CurrentDirection == EGMS_MovementDirection::Backward) + { + BwdDeadZone *= 2; + } + } + + if (AbsAngle <= 45 + FwdDeadZone) + { + return EGMS_MovementDirection::Forward; + } + + if (AbsAngle >= 135 - BwdDeadZone) + { + return EGMS_MovementDirection::Backward; + } + if (Angle > 0) + { + return EGMS_MovementDirection::Right; + } + + return EGMS_MovementDirection::Left; +} + +EGMS_MovementDirection_8Way UGMS_AnimLayer_States_DefaultLocomotion::SelectOctagonalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection_8Way CurrentDirection, + bool bUseCurrentDirection) const +{ + const float AbsAngle = FMath::Abs(Angle); + float FwdDeadZone = DeadZone; + float BwdDeadZone = DeadZone; + if (bUseCurrentDirection) + { + if (CurrentDirection == EGMS_MovementDirection_8Way::Forward) + { + FwdDeadZone *= 2; + } + if (CurrentDirection == EGMS_MovementDirection_8Way::Backward) + { + BwdDeadZone *= 2; + } + } + + if (AbsAngle <= 22.5f + FwdDeadZone) + { + return EGMS_MovementDirection_8Way::Forward; + } + if (AbsAngle >= 157.5f - BwdDeadZone) + { + return EGMS_MovementDirection_8Way::Backward; + } + if (Angle >= 22.5f && Angle < 67.5f) + { + return EGMS_MovementDirection_8Way::ForwardRight; + } + if (Angle >= 67.5f && Angle < 112.5f) + { + return EGMS_MovementDirection_8Way::Right; + } + if (Angle >= 112.5f && Angle < 157.5f) + { + return EGMS_MovementDirection_8Way::BackwardRight; + } + if (Angle >= -157.5f && Angle < -112.5f) + { + return EGMS_MovementDirection_8Way::BackwardLeft; + } + if (Angle >= -112.5f && Angle < -67.5f) + { + return EGMS_MovementDirection_8Way::Left; + } + return EGMS_MovementDirection_8Way::ForwardLeft; +} + +EGMS_MovementDirection UGMS_AnimLayer_States_DefaultLocomotion::GetOppositeCardinalDirection(EGMS_MovementDirection CurrentDirection) const +{ + switch (CurrentDirection) + { + case EGMS_MovementDirection::Forward: + return EGMS_MovementDirection::Backward; + case EGMS_MovementDirection::Backward: + return EGMS_MovementDirection::Forward; + case EGMS_MovementDirection::Left: + return EGMS_MovementDirection::Right; + case EGMS_MovementDirection::Right: + return EGMS_MovementDirection::Left; + default: + return CurrentDirection; + } +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsViewDirection() const +{ + return GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection; +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsVelocityDirection() const +{ + return GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection; +} + +const TInstancedStruct& UGMS_AnimLayer_States_DefaultLocomotion::GetViewDirectionSetting() const +{ + return GetParent()->GetMovementSystemComponent()->GetControlSetting()->ViewDirectionSetting; +} + +const TInstancedStruct& UGMS_AnimLayer_States_DefaultLocomotion::GetVelocityDirectionSetting() const +{ + return GetParent()->GetMovementSystemComponent()->GetControlSetting()->VelocityDirectionSetting; +} + +#pragma region Idle + +void UGMS_AnimLayer_States_DefaultLocomotion::Idle_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result; + FSequencePlayerReference SequencePlayer = USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + + USequencePlayerLibrary::SetSequenceWithInertialBlending(Context, SequencePlayer, AnimData_Idle.Idle, AnimData_Idle.BlendTime); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::InitializeIdleBreak_Implementation() +{ + if (!GetParent()) + { + return; + } + if (AnimData_Idle.IdleBreakDelayTime > 0) + { + IdleBreakState.IdleBreakDelayTime = AnimData_Idle.IdleBreakDelayTime; + } + else + { + IdleBreakState.IdleBreakDelayTime = 6 + FMath::TruncToInt(FMath::Abs(GetParent()->LocomotionState.Location.X + GetParent()->LocomotionState.Location.Y)) % 10; + } + IdleBreakState.TimeUntilNextIdleBreak = IdleBreakState.IdleBreakDelayTime; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::RefreshIdleBreak_Implementation() +{ + if (IsIdleBreakAllowed()) + { + IdleBreakState.TimeUntilNextIdleBreak -= GetDeltaSeconds(); + } + else + { + IdleBreakState.TimeUntilNextIdleBreak = IdleBreakState.IdleBreakDelayTime; + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::IdleBreak_AnimRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + if (!IsIdleBreakAllowed()) + { + return; + } + bool Result = false; + FSequencePlayerReference SequencePlayer; + USequencePlayerLibrary::ConvertToSequencePlayerPure(Node, SequencePlayer, Result); + if (!Result) + { + return; + } + + // clamp index in range(in case swapped animation during last update.) + if (IdleBreakState.CurrentIdleBreakIndex >= AnimData_Idle.Idle_Breaks.Num()) + { + IdleBreakState.CurrentIdleBreakIndex = 0; + } + + USequencePlayerLibrary::SetSequence(SequencePlayer, AnimData_Idle.Idle_Breaks[IdleBreakState.CurrentIdleBreakIndex]); + + //推进IdleBreak动画. + IdleBreakState.CurrentIdleBreakIndex = IdleBreakState.CurrentIdleBreakIndex + 1; + if (IdleBreakState.CurrentIdleBreakIndex >= AnimData_Idle.Idle_Breaks.Num()) + { + IdleBreakState.CurrentIdleBreakIndex = 0; + } +} +#pragma endregion + +#pragma region IdleBreak + +bool UGMS_AnimLayer_States_DefaultLocomotion::IsIdleBreakAllowed_Implementation() const +{ + if (AnimData_Idle.bDisableIdleBreaks) + { + return false; + } + if (AnimData_Idle.Idle_Breaks.IsEmpty()) + { + return false; + } + if (IsViewDirection() && (!GetViewDirectionSetting().IsValid() || GetViewDirectionSetting().GetScriptStruct() == FGMS_ViewDirectionSetting_Aiming::StaticStruct())) + { + return false; + } + return true; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Idle_StateUpdate(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FAnimationStateResultReference AnimationStateResult; + UAnimationStateMachineLibrary::ConvertToAnimationStateResult(Node, AnimationStateResult, Result); + + // try to enable root rotation offset. + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + if (!UAnimationStateMachineLibrary::IsStateBlendingOut(Context, AnimationStateResult)) + { + RefreshTurnInPlaceMode(); + } + } +} + +#pragma endregion + +#pragma region TurnInPlace + +void UGMS_AnimLayer_States_DefaultLocomotion::SetupTurnInPlace(float TurnAngle) +{ + bool bHas180 = AnimData_TurnInPlace.Left180 && AnimData_TurnInPlace.Right180; + if (FMath::Abs(TurnAngle) < AnimData_TurnInPlace.Turn180AngleThreshold || !bHas180) + { + TurnInPlaceState.Animation = TurnAngle <= 0.0f || TurnAngle > 180.0f - UGMS_Rotation::CounterClockwiseRotationAngleThreshold + ? AnimData_TurnInPlace.Left + : AnimData_TurnInPlace.Right; + TurnInPlaceState.b180 = false; + } + else + { + TurnInPlaceState.Animation = TurnAngle <= 0.0f || + TurnAngle > 180.0f - UGMS_Rotation::CounterClockwiseRotationAngleThreshold + ? AnimData_TurnInPlace.Left180 + : AnimData_TurnInPlace.Right180; + TurnInPlaceState.b180 = true; + } + + TurnInPlaceState.TriggeredAngle = TurnAngle; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::RefreshTurnInPlaceMode() +{ +#if WITH_EDITOR + if (!IsValid(GetWorld()) || !GetWorld()->IsGameWorld()) + { + return; + } +#endif + + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_AnimLayer_States_DefaultLocomotion::RefreshTurnInPlaceMode"), STAT_GMS_DefaultLocomotion_RefreshTurnInPlaceMode, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + if (TurnInPlaceState.bUpdatedThisFrame) + { + return; + } + TurnInPlaceState.bUpdatedThisFrame = true; + + // has no animation or has core state changes. + if (!AnimData_TurnInPlace.bValid || GetParent()->HasCoreStateChanges()) + { + TurnInPlaceState.bShouldTurn = false; + TurnInPlaceState.ActivationDelay = 0.0f; + return; + } + + RefreshTurnInPlaceInVelocityDirection(); + RefreshTurnInPlaceInViewDirection(); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::RefreshTurnInPlaceInViewDirection() +{ + static constexpr auto PlayRateInterpolationSpeed{5.0f}; + if (!IsViewDirection() || !GetViewDirectionSetting().IsValid()) + { + return; + } + + bool bIsAiming = GetViewDirectionSetting().GetScriptStruct() == FGMS_ViewDirectionSetting_Aiming::StaticStruct(); + + // Refresh root rotation offset mode. + { + if (GetViewDirectionSetting().Get().bEnableRotationWhenNotMoving) + { + if (bIsAiming && !AnimData_TurnInPlace.bTurnWhenAimingInViewDirection) + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Release); + } + else + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Accumulate); + } + } + else + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Release); + } + } + + bool bHasRootBoneRotationOffset = GetParent()->GetOffsetRootBoneRotationMode() == EOffsetRootBoneMode::Accumulate; + + float TurnYawAngle = bHasRootBoneRotationOffset ? -GetParent()->RootState.YawOffset : GetParent()->ViewState.YawAngle; + + bool bWantsToTurn = FMath::Abs(TurnYawAngle) >= AnimData_TurnInPlace.ViewYawAngleThreshold; + + bool bShouldDelay = AnimData_TurnInPlace.ViewYawAngleToActivationDelay.X > 0 && !bIsAiming; + + if (!bWantsToTurn && !TurnInPlaceState.bShouldTurn) + { + TurnInPlaceState.ActivationDelay = 0.0f; + } + + if (!bWantsToTurn && TurnInPlaceState.bShouldTurn) + { + TurnInPlaceState.bShouldTurn = false; + } + + //更新转身过程的状态。 + if (TurnInPlaceState.bShouldTurn) + { + if (!bShouldDelay) + { + if (bHasRootBoneRotationOffset) + { + TurnInPlaceState.PlayRate = AnimData_TurnInPlace.PlayRate; + TurnInPlaceState.ScaledPlayRate = TurnInPlaceState.PlayRate; + } + else + { + const auto NewPlayRate{ + FMath::GetMappedRangeValueClamped(AnimData_TurnInPlace.ReferenceViewYawSpeed, AnimData_TurnInPlace.PlayRateRange, GetParent()->ViewState.YawSpeed) + }; + + TurnInPlaceState.PlayRate = FMath::FInterpTo(TurnInPlaceState.PlayRate, NewPlayRate, GetDeltaSeconds(), PlayRateInterpolationSpeed); + TurnInPlaceState.ScaledPlayRate = TurnInPlaceState.PlayRate; + } + } + else + { + TurnInPlaceState.PlayRate = AnimData_TurnInPlace.PlayRate; + TurnInPlaceState.ScaledPlayRate = AnimData_TurnInPlace.bScaleTurnRate + ? TurnInPlaceState.PlayRate * FMath::Abs(TurnInPlaceState.TriggeredAngle / (TurnInPlaceState.b180 ? 180 : 90)) + : TurnInPlaceState.PlayRate; + } + } + + + // Not start + if (bWantsToTurn && !TurnInPlaceState.bShouldTurn && bShouldDelay) + { + const auto& DesiredDelayTime = bShouldDelay + ? FMath::GetMappedRangeValueClamped({AnimData_TurnInPlace.ViewYawAngleThreshold, 180.0f}, + AnimData_TurnInPlace.ViewYawAngleToActivationDelay, FMath::Abs(TurnYawAngle)) + : 0.0f; + TurnInPlaceState.ActivationDelay += GetDeltaSeconds(); + if (!TurnInPlaceState.bShouldTurn && TurnInPlaceState.ActivationDelay <= DesiredDelayTime) + { + return; + } + } + + // Trigger turn-in-place. + if (bWantsToTurn && !TurnInPlaceState.bShouldTurn) + { + SetupTurnInPlace(TurnYawAngle); + TurnInPlaceState.bShouldTurn = true; + GMS_ANIMATION_CLOG(VeryVerbose, "turn-in-place in view direction, Anim:%s, Angle:%f", *(TurnInPlaceState.Animation?TurnInPlaceState.Animation->GetName():TEXT("Null")), + TurnYawAngle); + return; + } + + TurnInPlaceState.bShouldTurn = bWantsToTurn; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::RefreshTurnInPlaceInVelocityDirection() +{ + if (!IsVelocityDirection() || !GetVelocityDirectionSetting().IsValid()) + { + return; + } + + bool bHasRootYawOffset = !UKismetMathLibrary::NearlyEqual_FloatFloat(GetParent()->RootState.YawOffset, 0.00001); + // has any root offset. + if (bHasRootYawOffset && AnimData_TurnInPlace.bTurnInVelocityDirection) + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Accumulate); + } + else + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Release); + } + + bool bWantsToTurn = bHasRootYawOffset && FMath::Abs(GetParent()->RootState.YawOffset) >= AnimData_TurnInPlace.RootYawAngleThreshold && AnimData_TurnInPlace.bTurnInVelocityDirection; + + // cancel turn. + if (!bWantsToTurn && TurnInPlaceState.bShouldTurn) + { + TurnInPlaceState.bShouldTurn = false; + } + + // Trigger turn-in-place. + if (bWantsToTurn && !TurnInPlaceState.bShouldTurn) + { + SetupTurnInPlace(GetParent()->RootState.YawOffset * -1); + TurnInPlaceState.PlayRate = AnimData_TurnInPlace.PlayRate; + TurnInPlaceState.ScaledPlayRate = AnimData_TurnInPlace.bScaleTurnRate + ? AnimData_TurnInPlace.PlayRate * FMath::Abs(TurnInPlaceState.TriggeredAngle / (TurnInPlaceState.b180 ? 180 : 90)) + : AnimData_TurnInPlace.PlayRate; + TurnInPlaceState.bShouldTurn = true; + GMS_ANIMATION_CLOG(VeryVerbose, "turn-in-place in velocity direction, Anim:%s, Angle:%f", *(TurnInPlaceState.Animation?TurnInPlaceState.Animation->GetName():TEXT("Null")), + GetParent()->RootState.YawOffset * -1); + } +} + +#pragma endregion + +#pragma region Start + +UAnimSequence* UGMS_AnimLayer_States_DefaultLocomotion::GetStartAnimation_Implementation() const +{ + UAnimSequence* Animation = nullptr; + + if (GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection) + { + float Delta = StartState.YawDeltaToAcceleration; + + if (AnimData_Start_VelocityDirection.AnimType == EGMS_StartAnimType_VelocityDir::Reface) + { + if (FMath::Abs(Delta) > 45 && FMath::Abs(Delta) < 135) + { + Animation = Delta <= 0.0f || Delta > 180.0f - UGMS_Rotation::CounterClockwiseRotationAngleThreshold + ? AnimData_Start_VelocityDirection.Animations.StartForwardL90 + : AnimData_Start_VelocityDirection.Animations.StartForwardR90; + } + else if (FMath::Abs(Delta) >= 135) + { + Animation = Delta <= 0.0f || Delta > 180.0f - UGMS_Rotation::CounterClockwiseRotationAngleThreshold + ? AnimData_Start_VelocityDirection.Animations.StartForwardL180 + : AnimData_Start_VelocityDirection.Animations.StartForwardR180; + } + else + { + Animation = AnimData_Start_VelocityDirection.Animations.StartForward; + } + } + if (AnimData_Start_VelocityDirection.AnimType == EGMS_StartAnimType_VelocityDir::Single) + { + Animation = AnimData_Start_VelocityDirection.Animation; + } + + if (Animation) + { + GMS_ANIMATION_CLOG(VeryVerbose, "Selected anim:%s with delta:%f", *Animation->GetName(), Delta); + } + else + { + GMS_ANIMATION_CLOG(Error, "Failed to select anim with delta:%f", Delta); + } + } + + if (GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection) + { + if (AnimData_Start_ViewDirection.AnimType == EGMS_StartAnimType_ViewDir::Direction_4) + { + switch (GetParent()->LocomotionState.LocalVelocityDirection) + { + case EGMS_MovementDirection::Forward: + Animation = AnimData_Start_ViewDirection.Animations.Forward; + break; + case EGMS_MovementDirection::Backward: + Animation = AnimData_Start_ViewDirection.Animations.Backward; + break; + case EGMS_MovementDirection::Left: + Animation = AnimData_Start_ViewDirection.Animations.Left; + break; + case EGMS_MovementDirection::Right: + Animation = AnimData_Start_ViewDirection.Animations.Right; + break; + } + } + + if (AnimData_Start_ViewDirection.AnimType == EGMS_StartAnimType_ViewDir::Direction_8) + { + switch (GetParent()->LocomotionState.LocalVelocityOctagonalDirection) + { + case EGMS_MovementDirection_8Way::Forward: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.Forward; + break; + case EGMS_MovementDirection_8Way::ForwardLeft: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.ForwardLeft; + break; + case EGMS_MovementDirection_8Way::ForwardRight: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.ForwardRight; + break; + case EGMS_MovementDirection_8Way::Backward: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.Backward; + break; + case EGMS_MovementDirection_8Way::BackwardLeft: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.BackwardLeft; + break; + case EGMS_MovementDirection_8Way::BackwardRight: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.BackwardRight; + break; + case EGMS_MovementDirection_8Way::Left: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.Left; + break; + case EGMS_MovementDirection_8Way::Right: + Animation = AnimData_Start_ViewDirection.Animations_8Direction.Right; + break; + } + } + } + + return Animation; +} + + +const FGMS_StrideWarpingSettings& UGMS_AnimLayer_States_DefaultLocomotion::GetStartStrideWarpingSettings() const +{ + return GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection + ? AnimData_Start_VelocityDirection.StrideWarping + : AnimData_Start_ViewDirection.StrideWarping; +} + +const FGMS_SteeringSettings& UGMS_AnimLayer_States_DefaultLocomotion::GetStartSteeringSettings() const +{ + if (IsVelocityDirection()) + { + return AnimData_Start_VelocityDirection.Steering; + } + const static FGMS_SteeringSettings DisabledSteering = FGMS_SteeringSettings(false, 0, 0); + return DisabledSteering; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Start_BlendStackRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + SetupStartState(); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Start_StateEntry_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + StartState.SmoothTargetRotation = GetParent()->MovementIntent.Rotation(); + StartState.TimeInState = 0.0f; + // when triggering start, use the ywa delta of acceleration direction and root bone direction to get start animation. + StartState.YawDeltaToAcceleration = UKismetMathLibrary::NormalizedDeltaRotator(GetParent()->MovementIntent.Rotation(), GetParent()->RootState.RootTransform.Rotator()).Yaw; + StartState.LocalVelocityDirection = GetParent()->LocomotionState.LocalVelocityDirection; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Start_StateUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FAnimationStateResultReference AnimationStateResult; + UAnimationStateMachineLibrary::ConvertToAnimationStateResult(Node, AnimationStateResult, Result); + + if (Result != EAnimNodeReferenceConversionResult::Succeeded) + { + return; + } + + if (!UAnimationStateMachineLibrary::IsStateBlendingOut(Context, AnimationStateResult)) + { + //Update root rotation mode. + if (IsVelocityDirection() && AnimData_Start_VelocityDirection.bValid && + AnimData_Start_VelocityDirection.AnimType == EGMS_StartAnimType_VelocityDir::Reface && AnimData_Start_VelocityDirection.Steering.bEnabled) + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Accumulate); + } + else + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Release); + } + + // try to enable root rotation offset. + if (IsVelocityDirection() && AnimData_Start_VelocityDirection.bValid && AnimData_Start_VelocityDirection.Steering.bEnabled) + { + GetParent()->SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode::Interpolate); + } + else + { + GetParent()->SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode::Release); + } + + StartState.TimeInState += GetDeltaSeconds(); + + StartState.YawDeltaToAcceleration = UKismetMathLibrary::NormalizedDeltaRotator(GetParent()->MovementIntent.Rotation(), GetParent()->RootState.RootTransform.Rotator()).Yaw; + + StartState.SmoothTargetRotation = FMath::RInterpTo(StartState.SmoothTargetRotation, GetParent()->MovementIntent.Rotation(), GetDeltaSeconds(), 5); + + const float MovementIntentDelta = UKismetMathLibrary::NormalizedDeltaRotator(StartState.SmoothTargetRotation, GetParent()->MovementIntent.Rotation()).Yaw; + + if (FMath::Abs(MovementIntentDelta) > 45 && StartState.TimeInState < 1.0f && StartState.TimeInState > 0.0f) + { + GMS_ANIMATION_CLOG(VeryVerbose, "Restart due to movement intent change within short amount of time.") + SetupStartState(); + StartState.TimeInState = 0.0f; + } + } + else + { + GetParent()->SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode::Release); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Start_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FBlendStackInputAnimNodeReference BlendStackInput = UBlendStackInputAnimNodeLibrary::ConvertToBlendStackInputNode(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + const float AccumulatedTime = UBlendStackAnimNodeLibrary::GetCurrentBlendStackAnimAssetTime(BlendStackInput); + auto& SW = GetStartStrideWarpingSettings(); + StartState.StrideWarpingAlpha = SW.bEnabled ? UKismetMathLibrary::MapRangeClamped(AccumulatedTime - SW.BlendInStartOffset, 0, SW.BlendInDurationScaled, 0, 1.f) : 0; + + const bool bDynamicPlayRate = GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection + ? AnimData_Start_VelocityDirection.bDynamicPlayRate + : AnimData_Start_ViewDirection.bDynamicPlayRate; + + if (bDynamicPlayRate) + { + const FVector2D DesiredPlayRateClamp = GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection + ? AnimData_Start_VelocityDirection.PlayRateClamp + : AnimData_Start_ViewDirection.PlayRateClamp; + + // When offset root bone translation enabled, the mesh will lag behind to ease the foot sliding, otherwise,slow down the playrate to ease the foot sliding. + StartState.PlayRateClamp = GetParent()->GeneralSetting.bEnableOffsetRootBoneTranslation + ? DesiredPlayRateClamp + : UKismetMathLibrary::MakeVector2D(UKismetMathLibrary::Lerp(0.2, DesiredPlayRateClamp.X, StartState.StrideWarpingAlpha), DesiredPlayRateClamp.Y); + + if (UAnimSequenceBase* Sequence = Cast(UBlendStackAnimNodeLibrary::GetCurrentBlendStackAnimAsset(BlendStackInput))) + { + float DesiredPlayRate = GetParent()->LocomotionState.DisplacementSpeed / UGMS_Utility::CalculateAnimatedSpeed(Sequence); + if (StartState.PlayRateClamp.X >= 0.0f && StartState.PlayRateClamp.X < StartState.PlayRateClamp.Y) + { + DesiredPlayRate = FMath::Clamp(DesiredPlayRate, StartState.PlayRateClamp.X, StartState.PlayRateClamp.Y); + } + StartState.PlayRate = DesiredPlayRate; + } + } + else + { + StartState.PlayRate = 1.0f; + } + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::SetupStartState() +{ + StartState.Animation = GetStartAnimation(); + StartState.StrideWarpingAlpha = 0; + StartState.PlayRate = 1; + StartState.BlendProfile = GetParent()->GetNamedBlendProfile(GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection + ? AnimData_Start_VelocityDirection.BlendProfile + : AnimData_Start_ViewDirection.BlendProfile); + + if (GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection) + { + StartState.OrientationAlpha = 0.0f; + } + if (GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection) + { + StartState.OrientationAlpha = AnimData_Start_ViewDirection.AnimType == EGMS_StartAnimType_ViewDir::Direction_4 ? 1.0f : 0.0f; + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Cycle_BlendStackUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FBlendStackAnimNodeReference BlendStackReference = UBlendStackAnimNodeLibrary::ConvertToBlendStackNode(Node, Result); + + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + CycleState.Animation = GetCycleAnimation(); + CycleState.BlendProfile = GetParent()->GetNamedBlendProfile(AnimData_Cycle.BlendProfile); + } +} + +#pragma endregion + +#pragma region Cycle + +void UGMS_AnimLayer_States_DefaultLocomotion::Cycle_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FBlendStackInputAnimNodeReference BlendStackInput = UBlendStackInputAnimNodeLibrary::ConvertToBlendStackInputNode(Node, Result); + + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + if (UAnimSequenceBase* Anim = Cast(UBlendStackAnimNodeLibrary::GetCurrentBlendStackAnimAsset(BlendStackInput))) + { + float AnimationSpeed = AnimData_Cycle.bHasRootMotion ? UGMS_Utility::CalculateAnimatedSpeed(Anim) : AnimData_Cycle.AnimatedSpeed; + float DesiredPlayRate = GetParent()->LocomotionState.DisplacementSpeed / AnimationSpeed; + + if (AnimData_Cycle.PlayRateClamp.X >= 0.0f && AnimData_Cycle.PlayRateClamp.X < AnimData_Cycle.PlayRateClamp.Y) + { + DesiredPlayRate = FMath::Clamp(DesiredPlayRate, AnimData_Cycle.PlayRateClamp.X, AnimData_Cycle.PlayRateClamp.Y); + } + + CycleState.PlayRate = DesiredPlayRate; + + CycleState.StrideWarpingAlpha = UKismetMathLibrary::FInterpTo(CycleState.StrideWarpingAlpha, AnimData_Cycle.bEnableStrideWarping ? (GetParent()->bBlocked ? 0.5f : 1.0f) : 0.0f, + Context.GetContext()->GetDeltaTime(), 10.0); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Cycle_StateEntry_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + GetParent()->SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode::Release); + if (GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection) + { + CycleState.OrientationAlpha = 1.0f; + } + if (GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection) + { + CycleState.OrientationAlpha = AnimData_Cycle.AnimType == EGMS_CycleAnimType::Direction_4 ? 1.0f : 0.0f; + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Cycle_StateUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + FAnimationStateResultReference AnimationStateResult; + UAnimationStateMachineLibrary::ConvertToAnimationStateResult(Node, AnimationStateResult, Result); + + if (Result == EAnimNodeReferenceConversionResult::Succeeded && !UAnimationStateMachineLibrary::IsStateBlendingOut(Context, AnimationStateResult)) + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::Release); + } +} + +UAnimSequence* UGMS_AnimLayer_States_DefaultLocomotion::GetCycleAnimation_Implementation() const +{ + UAnimSequence* OutAnim = nullptr; + + if (AnimData_Cycle.AnimType == EGMS_CycleAnimType::Single) + { + OutAnim = AnimData_Cycle.Animation; + } + + if (AnimData_Cycle.AnimType == EGMS_CycleAnimType::Direction_4) + { + switch (GetParent()->LocomotionState.LocalVelocityDirectionNoOffset) + { + case EGMS_MovementDirection::Forward: + OutAnim = AnimData_Cycle.Animations.Forward; + break; + case EGMS_MovementDirection::Backward: + OutAnim = AnimData_Cycle.Animations.Backward; + break; + case EGMS_MovementDirection::Left: + OutAnim = AnimData_Cycle.Animations.Left; + break; + case EGMS_MovementDirection::Right: + OutAnim = AnimData_Cycle.Animations.Right; + break; + default: ; + } + } + + if (AnimData_Cycle.AnimType == EGMS_CycleAnimType::Direction_8) + { + switch (GetParent()->LocomotionState.LocalVelocityOctagonalDirection) + { + case EGMS_MovementDirection_8Way::Forward: + OutAnim = AnimData_Cycle.Animations_8Direction.Forward; + break; + case EGMS_MovementDirection_8Way::ForwardLeft: + OutAnim = AnimData_Cycle.Animations_8Direction.ForwardLeft; + break; + case EGMS_MovementDirection_8Way::ForwardRight: + OutAnim = AnimData_Cycle.Animations_8Direction.ForwardRight; + break; + case EGMS_MovementDirection_8Way::Backward: + OutAnim = AnimData_Cycle.Animations_8Direction.Backward; + break; + case EGMS_MovementDirection_8Way::BackwardLeft: + OutAnim = AnimData_Cycle.Animations_8Direction.BackwardLeft; + break; + case EGMS_MovementDirection_8Way::BackwardRight: + OutAnim = AnimData_Cycle.Animations_8Direction.BackwardRight; + break; + case EGMS_MovementDirection_8Way::Left: + OutAnim = AnimData_Cycle.Animations_8Direction.Left; + break; + case EGMS_MovementDirection_8Way::Right: + OutAnim = AnimData_Cycle.Animations_8Direction.Right; + break; + default: ; + } + } + return OutAnim; +} + +UAnimSequence* UGMS_AnimLayer_States_DefaultLocomotion::GetStopAnimation_Implementation() const +{ + if (AnimData_Stop.AnimType == EGMS_StopAnimType::Single) + { + return AnimData_Stop.Animation; + } + if (AnimData_Stop.AnimType == EGMS_StopAnimType::Direction_4) + { + switch (GetParent()->LocomotionState.LocalVelocityDirection) + { + case EGMS_MovementDirection::Forward: + return AnimData_Stop.Animations.Forward; + case EGMS_MovementDirection::Backward: + return AnimData_Stop.Animations.Backward; + case EGMS_MovementDirection::Left: + return AnimData_Stop.Animations.Left; + case EGMS_MovementDirection::Right: + return AnimData_Stop.Animations.Right; + } + } + if (AnimData_Stop.AnimType == EGMS_StopAnimType::Direction_8) + { + switch (GetParent()->LocomotionState.LocalVelocityOctagonalDirection) + { + case EGMS_MovementDirection_8Way::Forward: + return AnimData_Stop.Animations_8Direction.Forward; + case EGMS_MovementDirection_8Way::ForwardLeft: + return AnimData_Stop.Animations_8Direction.ForwardLeft; + case EGMS_MovementDirection_8Way::ForwardRight: + return AnimData_Stop.Animations_8Direction.ForwardRight; + case EGMS_MovementDirection_8Way::Backward: + return AnimData_Stop.Animations_8Direction.Backward; + case EGMS_MovementDirection_8Way::BackwardLeft: + return AnimData_Stop.Animations_8Direction.BackwardLeft; + case EGMS_MovementDirection_8Way::BackwardRight: + return AnimData_Stop.Animations_8Direction.BackwardRight; + case EGMS_MovementDirection_8Way::Left: + return AnimData_Stop.Animations_8Direction.Left; + case EGMS_MovementDirection_8Way::Right: + return AnimData_Stop.Animations_8Direction.Right; + } + } + return nullptr; +} + + +#pragma endregion + +#pragma region Stop + +bool UGMS_AnimLayer_States_DefaultLocomotion::ShouldDistanceMatchStop() const +{ + return GetParent()->LocomotionState.bHasVelocity && !GetParent()->LocomotionState.bHasInput; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Stop_AnimRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + + UAnimSequence* Animation = GetStopAnimation(); + + if (IsValid(Animation)) + { + StopState.Animation = Animation; + + USequenceEvaluatorLibrary::SetSequence(SequenceEvaluator, Animation); + + // If we got here, and we can't distance match a stop on start, match to 0 distance + if (!ShouldDistanceMatchStop() && GetParent()->bAnyMontagePlaying == false) + { + // immediately match to stop point. + UAnimDistanceMatchingLibrary::DistanceMatchToTarget(SequenceEvaluator, 0.f, FName("Distance")); + } + } + else + { + GMS_ANIMATION_CLOG(Error, "Does not have an anim sequence to play"); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Stop_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + + if (USequenceEvaluatorLibrary::GetSequence(SequenceEvaluator) != nullptr) + { + if (ShouldDistanceMatchStop() && GetParent()->bAnyMontagePlaying == false) + { + float DistanceToMatch = GetDistanceToStopTarget(); + if (DistanceToMatch > 0.f) + { + if (GetParent()->bAnyMontagePlaying) + { + GMS_ANIMATION_CLOG(Warning, "Cancel Distance matching while montage active."); + return; + } + GMS_ANIMATION_CLOG(VeryVerbose, "Distance matching stop at %f", DistanceToMatch) + UAnimDistanceMatchingLibrary::DistanceMatchToTarget(SequenceEvaluator, DistanceToMatch, FName("Distance")); + return; + } + } + USequenceEvaluatorLibrary::AdvanceTime(Context, SequenceEvaluator, 1); + } + else + { + GMS_ANIMATION_CLOG(Error, "Does not have an anim sequence to play."); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Stop_StateRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + GetParent()->SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode::Release); + + StopState.Animation = nullptr; + if (GetParent()->RotationMode == GMS_RotationModeTags::VelocityDirection) + { + StopState.OrientationAlpha = 1.0f; + } + if (GetParent()->RotationMode == GMS_RotationModeTags::ViewDirection) + { + StopState.OrientationAlpha = AnimData_Stop.AnimType == EGMS_StopAnimType::Direction_4 ? 1.0f : 0.0f; + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Stop_StateUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + FAnimationStateResultReference AnimationStateResult; + UAnimationStateMachineLibrary::ConvertToAnimationStateResult(Node, AnimationStateResult, Result); + + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + if (!UAnimationStateMachineLibrary::IsStateBlendingOut(Context, AnimationStateResult)) + { + GetParent()->SetOffsetRootBoneRotationMode(EOffsetRootBoneMode::LockOffsetIncreaseAndConsumeAnimation); + } + } +} + +float UGMS_AnimLayer_States_DefaultLocomotion::GetDistanceToStopTarget() const +{ + FGMS_PredictGroundMovementStopLocationParams Params = MovementSystem->GetPredictGroundMovementStopLocationParams(); + return UKismetMathLibrary::VSizeXY(UAnimCharacterMovementLibrary::PredictGroundMovementStopLocation( + Params.Velocity, + Params.bUseSeparateBrakingFriction, + Params.BrakingFriction, + Params.GroundFriction, + Params.BrakingFriction, + Params.BrakingDecelerationWalking)); +} + +#pragma endregion + +#pragma region Pivot + +void UGMS_AnimLayer_States_DefaultLocomotion::Pivot_AnimRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + PivotState.StartingAcceleration = GetParent()->LocomotionState.LocalAcceleration2D; + + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + + UAnimSequence* DesiredAnim = nullptr; + switch (PivotState.DesiredDirection) + { + case EGMS_MovementDirection::Forward: + DesiredAnim = AnimData_Pivot.Animations.Forward; + break; + case EGMS_MovementDirection::Backward: + DesiredAnim = AnimData_Pivot.Animations.Backward; + break; + case EGMS_MovementDirection::Left: + DesiredAnim = AnimData_Pivot.Animations.Left; + break; + case EGMS_MovementDirection::Right: + DesiredAnim = AnimData_Pivot.Animations.Right; + break; + } + + USequenceEvaluatorLibrary::SetSequence(SequenceEvaluator, DesiredAnim); + PivotState.Animation = DesiredAnim; + + USequenceEvaluatorLibrary::SetExplicitTime(SequenceEvaluator, 0.f); + + PivotState.RemainingCooldown = AnimData_Pivot.PivotCooldown; + PivotState.TimeAtPivotStop = 0; + PivotState.AccumulatedTime = 0; + + GMS_ANIMATION_CLOG(VeryVerbose, "Selected anim:%s", *PivotState.Animation.GetName()); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Pivot_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + const FSequenceEvaluatorReference SequenceEvaluator = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Failed) + { + return; + } + + PivotState.AccumulatedTime = USequenceEvaluatorLibrary::GetAccumulatedTime(SequenceEvaluator); + + if (PivotState.RemainingCooldown > 0) + { + UAnimSequence* NewDesiredAnim = nullptr; + + switch (PivotState.DesiredDirection) + { + case EGMS_MovementDirection::Forward: + NewDesiredAnim = AnimData_Pivot.Animations.Forward; + break; + case EGMS_MovementDirection::Backward: + NewDesiredAnim = AnimData_Pivot.Animations.Backward; + break; + case EGMS_MovementDirection::Left: + NewDesiredAnim = AnimData_Pivot.Animations.Left; + break; + case EGMS_MovementDirection::Right: + NewDesiredAnim = AnimData_Pivot.Animations.Right; + break; + default: ; + } + + if (NewDesiredAnim != USequenceEvaluatorLibrary::GetSequence(SequenceEvaluator)) + { + USequenceEvaluatorLibrary::SetSequenceWithInertialBlending(Context, SequenceEvaluator, NewDesiredAnim, 0.2f); + PivotState.StartingAcceleration = GetParent()->LocomotionState.LocalAcceleration2D; + PivotState.Animation = NewDesiredAnim; + GMS_ANIMATION_CLOG(VeryVerbose, "Selected new anim:%s", *PivotState.Animation.GetName()); + } + } + + //Does acceleration oppose velocity? + if (FVector::DotProduct(GetParent()->LocomotionState.LocalVelocity2D, GetParent()->LocomotionState.LocalAcceleration2D) < 0) + { + //While acceleration opposes velocity, the character is still approaching the pivot point, so we distance match to that point. + FGMS_PredictGroundMovementPivotLocationParams Params = MovementSystem->GetPredictGroundMovementPivotLocationParams(); + const float DistanceToTarget = UAnimCharacterMovementLibrary::PredictGroundMovementPivotLocation(Params.Acceleration, Params.Velocity, Params.GroundFriction).Size2D(); + + UAnimDistanceMatchingLibrary::DistanceMatchToTarget(SequenceEvaluator, DistanceToTarget, FName("Distance")); + PivotState.TimeAtPivotStop = PivotState.AccumulatedTime; + } + else + { + //Alpha = (ExplicitTime - StopTime - Offset)/Duration We want the blend in to start after we've already stopped, and just started accelerating + PivotState.StrideWarpingAlpha = AnimData_Pivot.StrideWarping.bEnabled + ? UKismetMathLibrary::MapRangeClamped(PivotState.AccumulatedTime - PivotState.TimeAtPivotStop - AnimData_Pivot.StrideWarping.BlendInStartOffset, + 0.f, + AnimData_Pivot.StrideWarping.BlendInDurationScaled, + 0.f, + 1.f) + : 0; + + // Smoothly increase the minimum playrate speed, as we blend in stride warping + const float PlayRateClamp_X = UKismetMathLibrary::Lerp(0.2f, AnimData_Pivot.PlayRateClamp.X, PivotState.StrideWarpingAlpha); + PivotState.PlayRateClamp = UKismetMathLibrary::MakeVector2D(PlayRateClamp_X, AnimData_Pivot.PlayRateClamp.Y); + + if (USequenceEvaluatorLibrary::GetSequence(SequenceEvaluator) != nullptr) + { + UAnimDistanceMatchingLibrary::AdvanceTimeByDistanceMatching(Context, SequenceEvaluator, GetParent()->LocomotionState.PreviousDisplacement, FName("Distance"), PivotState.PlayRateClamp); + } + else + { + GMS_ANIMATION_CLOG(Error, "Does not have an valid anim sequence to play"); + } + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Pivot_StateRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + PivotState.InitialDirection = GetParent()->LocomotionState.LocalVelocityDirection; + + GMS_ANIMATION_CLOG(VeryVerbose, "Selected direction:%d", PivotState.InitialDirection); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Pivot_StateUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + if (PivotState.RemainingCooldown > 0) + { + PivotState.RemainingCooldown -= Context.GetContext()->GetDeltaTime(); + } + + PivotState.bMovingPerpendicularToInitialDirection = IsMovingPerpendicularToInitialPivot(); +} + +#pragma endregion + +#pragma region Jump + +UAnimSequence* UGMS_AnimLayer_States_DefaultLocomotion::GetJumpStartAnimation_Implementation() const +{ + UAnimSequence* Animation = nullptr; + + if (AnimData_Jump.JumpStartType == EGMS_JumpStartAnimType::Direction) + { + switch (GetParent()->LocomotionState.LocalVelocityDirection) + { + case EGMS_MovementDirection::Forward: + Animation = AnimData_Jump.JumpStarts.Forward; + break; + case EGMS_MovementDirection::Backward: + Animation = AnimData_Jump.JumpStarts.Backward; + break; + case EGMS_MovementDirection::Left: + Animation = AnimData_Jump.JumpStarts.Left; + break; + case EGMS_MovementDirection::Right: + Animation = AnimData_Jump.JumpStarts.Right; + break; + } + } + else + { + Animation = AnimData_Jump.JumpStart; + } + return Animation; +} + +void UGMS_AnimLayer_States_DefaultLocomotion::JumpStart_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequencePlayerReference SequencePlayerReference = USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result); + if (Result == EAnimNodeReferenceConversionResult::Succeeded) + { + UAnimSequenceBase* Animation = GetJumpStartAnimation(); + + USequencePlayerLibrary::SetSequenceWithInertialBlending(Context, SequencePlayerReference, Animation, 0.2f); + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::JumpStartLoop_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequencePlayerReference SequencePlayerReference = USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result); + + UAnimSequenceBase* Animation = AnimData_Jump.JumpStartLoop; + + USequencePlayerLibrary::SetSequenceWithInertialBlending(Context, SequencePlayerReference, Animation, 0.2f); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::JumpApex_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequencePlayerReference SequencePlayerReference = USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result); + + UAnimSequenceBase* Animation = AnimData_Jump.JumpApex; + + USequencePlayerLibrary::SetSequenceWithInertialBlending(Context, SequencePlayerReference, Animation, 0.2f); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::FallLoop_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequencePlayerReference SequencePlayerReference = USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result); + + UAnimSequenceBase* Animation = AnimData_Jump.JumpFallLoop; + + USequencePlayerLibrary::SetSequenceWithInertialBlending(Context, SequencePlayerReference, Animation, 0.2f); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::FallLand_AnimRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + UAnimSequenceBase* Animation = AnimData_Jump.JumpFallLand; + + const FSequenceEvaluatorReference Reference = USequenceEvaluatorLibrary::SetSequence(USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result), Animation); + + USequenceEvaluatorLibrary::SetExplicitTime(Reference, 0.f); +} + +void UGMS_AnimLayer_States_DefaultLocomotion::FallLand_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + const FSequenceEvaluatorReference Reference = USequenceEvaluatorLibrary::ConvertToSequenceEvaluator(Node, Result); + + UAnimDistanceMatchingLibrary::DistanceMatchToTarget(Reference, GetParent()->InAirState.GroundDistance, FName("GroundDistance")); +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::Rule_JumpApex_Implementation() const +{ + if (IsValid(GetParent())) + { + return GetParent()->InAirState.TimeToJumpApex <= 0.4 && AnimData_Jump.bValidJumpApex; + } + return false; +} + +bool UGMS_AnimLayer_States_DefaultLocomotion::Rule_FallToFallLand_Implementation() const +{ + return AnimData_Jump.bValidJumpFallLand && GetParent()->InAirState.bValidGround && GetParent()->InAirState.GroundDistance < 200.0f; +} +#pragma endregion + +#pragma region Land + +void UGMS_AnimLayer_States_DefaultLocomotion::Land_AnimRelevant_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + EAnimNodeReferenceConversionResult Result = EAnimNodeReferenceConversionResult::Succeeded; + + float LandedSpeed = FMath::Abs(GetParent()->InAirState.VerticalSpeed); + if (UAnimSequence* Animation = UGMS_Utility::SelectAnimationWithFloat(AnimData_Land.Lands, LandedSpeed)) + { + const FSequencePlayerReference Reference = USequencePlayerLibrary::SetSequence(USequencePlayerLibrary::ConvertToSequencePlayer(Node, Result), Animation); + + USequencePlayerLibrary::SetAccumulatedTime(Reference, 0.f); + GMS_ANIMATION_CLOG(VeryVerbose, "Selected animation:%s at VerticalSpeed of %f", *Animation->GetName(), LandedSpeed); + } + else + { + GMS_ANIMATION_CLOG(Error, "Failed to select animation at VerticalSpeed of %f", LandedSpeed) + } +} + +void UGMS_AnimLayer_States_DefaultLocomotion::Land_AnimUpdate_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ +} +#pragma endregion diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View.cpp new file mode 100644 index 0000000..b618aa0 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_View.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_View) diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View_Default.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View_Default.cpp new file mode 100644 index 0000000..5000ce9 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimLayer_View_Default.cpp @@ -0,0 +1,29 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimLayer_View_Default.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimLayer_View_Default) + + +void UGMS_AnimLayer_View_Default::ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) +{ + if (const UGMS_AnimLayerSetting_View_Default* DS = Cast(Setting)) + { + ResetSetting(); + BlendSpace = DS->BlendSpace; + YawAngleOffset = DS->YawAngleOffset; + YawAngleLimit = DS->YawAngleLimit; + SmoothInterpSpeed = DS->SmoothInterpSpeed; + bValidBlendSpace = BlendSpace != nullptr; + } +} + +void UGMS_AnimLayer_View_Default::ResetSetting_Implementation() +{ + bValidBlendSpace = false; + YawAngleOffset = 0.0f; + YawAngleLimit = FVector2D(-90.0f, 90.0f); + SmoothInterpSpeed = 0.0f; + BlendSpace = nullptr; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimState.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimState.cpp new file mode 100644 index 0000000..0cf5659 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_AnimState.cpp @@ -0,0 +1,73 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_AnimState.h" + +#include "AnimationWarpingLibrary.h" +#include "Animation/AnimSequenceBase.h" +#include "Animation/AnimSequence.h" +#include "Utility/GMS_Constants.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimState) + + +void FGMS_AnimState_Layering::ApplyValueFromSequence(const UAnimSequence* InSequence, float ExplicitTime) +{ + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHead", ExplicitTime, HeadBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHeadAdditive", ExplicitTime, HeadAdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHeadSlot", ExplicitTime, HeadSlotBlendAmount); + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeft", ExplicitTime, ArmLeftBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftAdditive", ExplicitTime, ArmLeftAdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftSlot", ExplicitTime, ArmLeftSlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmLeftLocalSpace", ExplicitTime, ArmLeftLocalSpaceBlendAmount); + ArmLeftMeshSpaceBlendAmount = 1 - ArmLeftLocalSpaceBlendAmount; + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRight", ExplicitTime, ArmRightBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightAdditive", ExplicitTime, ArmRightAdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightSlot", ExplicitTime, ArmRightSlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerArmRightLocalSpace", ExplicitTime, ArmRightLocalSpaceBlendAmount); + ArmRightMeshSpaceBlendAmount = 1 - ArmRightLocalSpaceBlendAmount; + + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHandLeft", ExplicitTime, HandLeftBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerHandRight", ExplicitTime, HandRightBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpine", ExplicitTime, SpineBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpineAdditive", ExplicitTime, SpineAdditiveBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerSpineSlot", ExplicitTime, SpineSlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerPelvis", ExplicitTime, PelvisBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerPelvisSlot", ExplicitTime, PelvisSlotBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerLegs", ExplicitTime, LegsBlendAmount); + UAnimationWarpingLibrary::GetCurveValueFromAnimation(InSequence, "LayerLegsSlot", ExplicitTime, LegsSlotBlendAmount); +} + +void FGMS_AnimState_Layering::ZeroOut() +{ + HeadBlendAmount = 0.0f; + HeadAdditiveBlendAmount = 0.0f; + HeadSlotBlendAmount = 0.0f; + + ArmLeftBlendAmount = 0.0f; + ArmLeftAdditiveBlendAmount = 0.0f; + ArmLeftSlotBlendAmount = 0.0f; + ArmLeftLocalSpaceBlendAmount = 0.0f; + ArmLeftMeshSpaceBlendAmount = 0.0f; + + ArmRightBlendAmount = 0.0f; + ArmRightAdditiveBlendAmount = 0.0f; + ArmRightSlotBlendAmount = 0.0f; + ArmRightLocalSpaceBlendAmount = 0.0f; + ArmRightMeshSpaceBlendAmount = 0.0f; + + HandLeftBlendAmount = 0.0f; + HandRightBlendAmount = 0.0f; + + SpineBlendAmount = 0.0f; + SpineAdditiveBlendAmount = 0.0f; + SpineSlotBlendAmount = 0.0f; + + PelvisBlendAmount = 0.0f; + PelvisSlotBlendAmount = 0.0f; + + LegsBlendAmount = 0.0f; + LegsSlotBlendAmount = 0.0f; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_LocomotionStructLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_LocomotionStructLibrary.cpp new file mode 100644 index 0000000..f6f1c32 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_LocomotionStructLibrary.cpp @@ -0,0 +1,40 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Locomotions/GMS_LocomotionStructLibrary.h" +#include "Animation/AimOffsetBlendSpace.h" +#include "Animation/AnimSequenceBase.h" +#include "Animation/AnimSequence.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_LocomotionStructLibrary) + +bool FGMS_Animations_4Direction::ValidAnimations() const +{ + return Forward && Backward && Left && Right; +} + +bool FGMS_Animations_4Direction::HasRootMotion() const +{ + if (ValidAnimations()) + { + return Forward->HasRootMotion() && Backward->HasRootMotion() && Left->HasRootMotion() && Right->HasRootMotion(); + } + return false; +} + +bool FGMS_Animations_8Direction::ValidAnimations() const +{ + return Forward && ForwardLeft && ForwardRight && Backward && BackwardLeft && BackwardRight && Left && Right; +} + +bool FGMS_Animations_8Direction::HasRootMotion() const +{ + if (ValidAnimations()) + { + return Forward->HasRootMotion() && ForwardLeft->HasRootMotion() && ForwardRight->HasRootMotion() && Backward->HasRootMotion() && BackwardLeft->HasRootMotion() && BackwardRight-> + HasRootMotion() && Left-> + HasRootMotion() && Right->HasRootMotion(); + } + return false; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_MainAnimInstance.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_MainAnimInstance.cpp new file mode 100644 index 0000000..d7fb8c2 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Locomotions/GMS_MainAnimInstance.cpp @@ -0,0 +1,958 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Locomotions/GMS_MainAnimInstance.h" +#include "AnimationWarpingLibrary.h" +#include "PoseSearch/PoseSearchTrajectoryPredictor.h" +#include "DrawDebugHelpers.h" +#include "GMS_CharacterMovementSystemComponent.h" +#include "GMS_MovementSystemComponent.h" +#include "KismetAnimationLibrary.h" +#include "Curves/CurveFloat.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Kismet/KismetMathLibrary.h" +#include "Locomotions/GMS_AnimLayer.h" +#include "Locomotions/GMS_AnimLayer_Additive.h" +#include "Locomotions/GMS_AnimLayer_Overlay.h" +#include "Locomotions/GMS_AnimLayer_States.h" +#include "Locomotions/GMS_AnimLayer_View.h" +#include "Locomotions/GMS_AnimLayer_SkeletalControls.h" +#include "PoseSearch/PoseSearchTrajectoryLibrary.h" +#include "Utility/GMS_Log.h" +#include "Utility/GMS_Math.h" +#include "Utility/GMS_Utility.h" +#include "Utility/GMS_Vector.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_MainAnimInstance) + +UGMS_MainAnimInstance::UGMS_MainAnimInstance() +{ + RootMotionMode = ERootMotionMode::RootMotionFromMontagesOnly; + MovementIntent = FVector::ZeroVector; +} + +UGMS_MovementSystemComponent* UGMS_MainAnimInstance::GetMovementSystemComponent() const +{ + return MovementSystem; +} + + +void UGMS_MainAnimInstance::RegisterStateNameToTagMapping(UAnimInstance* SourceAnimInstance, TArray Mapping) +{ + if (SourceAnimInstance && SourceAnimInstance->Blueprint_GetMainAnimInstance() == this && !Mapping.IsEmpty()) + { + FGMS_AnimStateNameToTagWrapper Wrapper; + Wrapper.AnimStateNameToTagMapping = Mapping; + RuntimeAnimStateNameToTagMappings.Emplace(SourceAnimInstance, Wrapper); + } +} + +void UGMS_MainAnimInstance::UnregisterStateNameToTagMapping(UAnimInstance* SourceAnimInstance) +{ + if (SourceAnimInstance && SourceAnimInstance->Blueprint_GetMainAnimInstance() == this && RuntimeAnimStateNameToTagMappings.Contains(SourceAnimInstance)) + { + TArray Tags; + for (const FGMS_AnimStateNameToTag& Mapping : RuntimeAnimStateNameToTagMappings[SourceAnimInstance].AnimStateNameToTagMapping) + { + Tags.Add(Mapping.Tag); + } + NodeRelevanceTags.RemoveTags(FGameplayTagContainer::CreateFromArray(Tags)); + RuntimeAnimStateNameToTagMappings.Remove(SourceAnimInstance); + } +} + +#pragma region Definition + + +void UGMS_MainAnimInstance::RefreshLayerSettings_Implementation() +{ + const FGMS_MovementSetSetting& MSSetting = MovementSystem->GetMovementSetSetting(); + + const auto& States = MSSetting.bUseInstancedStatesSetting ? MSSetting.AnimLayerSetting_States : MSSetting.DA_AnimLayerSetting_States; + + SetAnimLayerBySetting(States, StateLayerInstance); + + const auto& Overlay = MSSetting.bUseInstancedOverlaySetting ? MSSetting.AnimLayerSetting_Overlay : MSSetting.DA_AnimLayerSetting_Overlay; + + SetAnimLayerBySetting(Overlay, OverlayLayerInstance); + SetAnimLayerBySetting(MSSetting.AnimLayerSetting_View, ViewLayerInstance); + SetAnimLayerBySetting(MSSetting.AnimLayerSetting_Additive, AdditiveLayerInstance); + SetAnimLayerBySetting(MSSetting.AnimLayerSetting_SkeletalControls, SkeletonControlsLayerInstance); +} + +void UGMS_MainAnimInstance::SetOffsetRootBoneRotationMode_Implementation(EOffsetRootBoneMode NewRotationMode) +{ + if (GeneralSetting.bEnableOffsetRootBoneRotation && RootState.RotationMode != NewRotationMode) + { + RootState.RotationMode = NewRotationMode; + } +} + +EOffsetRootBoneMode UGMS_MainAnimInstance::GetOffsetRootBoneRotationMode_Implementation() const +{ + if (bAnyMontagePlaying) + { + return EOffsetRootBoneMode::Release; + } + // Temporal solution:prevent root rotation offset when standing at moving platform. + // if (GetMovementSystemComponent() && GetMovementSystemComponent()->GetMovementBase().bHasRelativeRotation) + // { + // return EOffsetRootBoneMode::Release; + // } + return GeneralSetting.bEnableOffsetRootBoneRotation ? RootState.RotationMode : EOffsetRootBoneMode::Release; +} + +void UGMS_MainAnimInstance::SetOffsetRootBoneTranslationMode_Implementation(EOffsetRootBoneMode NewTranslationMode) +{ + if (GeneralSetting.bEnableOffsetRootBoneTranslation && RootState.TranslationMode != NewTranslationMode) + { + RootState.TranslationMode = NewTranslationMode; + } +} + +EOffsetRootBoneMode UGMS_MainAnimInstance::GetOffsetRootBoneTranslationMode_Implementation() const +{ + if (bAnyMontagePlaying) + { + return EOffsetRootBoneMode::Release; + } + return GeneralSetting.bEnableOffsetRootBoneTranslation ? RootState.TranslationMode : EOffsetRootBoneMode::Release; +} + + +void UGMS_MainAnimInstance::OnLocomotionModeChanged_Implementation(const FGameplayTag& Prev) +{ + check(IsInGameThread()) + check(IsValid(MovementSystem)) + // GMS_ANIMATION_CLOG(Verbose, "Refresh layer settings due to LocomotionMode Changed.") + + LocomotionMode = MovementSystem->GetLocomotionMode(); + LocomotionModeContainer = LocomotionMode.GetSingleTagContainer(); + + if (Prev == GMS_MovementModeTags::InAir) + { + InAirState.bJumping = false; + InAirState.bFalling = false; + } + + bLocomotionModeChanged = true; + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + bLocomotionModeChanged = false; + }); +} + +void UGMS_MainAnimInstance::OnRotationModeChanged_Implementation(const FGameplayTag& Prev) +{ + check(IsInGameThread()) + check(IsValid(MovementSystem)) + check(IsValid(MovementSystem->GetControlSetting())) + // GMS_ANIMATION_CLOG(Verbose, "Refresh layer settings due to RotationMode Changed.") + + RotationMode = MovementSystem->GetRotationMode(); + RotationModeContainer = MovementSystem->GetRotationMode().GetSingleTagContainer(); + + RefreshLayerSettings(); + + bRotationModeChanged = true; + + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + bRotationModeChanged = false; + }); +} + +void UGMS_MainAnimInstance::OnMovementSetChanged_Implementation(const FGameplayTag& Prev) +{ + check(IsInGameThread()) + check(IsValid(MovementSystem)) + + // GMS_ANIMATION_CLOG(Verbose, "Refresh layer settings due to MovementSet Changed.") + + MovementSet = MovementSystem->GetMovementSet(); + MovementSetContainer = MovementSet.GetSingleTagContainer(); + + RefreshLayerSettings(); + + bMovementSetChanged = true; + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + bMovementSetChanged = false; + }); +} + +void UGMS_MainAnimInstance::OnMovementStateChanged_Implementation(const FGameplayTag& Prev) +{ + check(IsInGameThread()) + check(IsValid(MovementSystem)) + // GMS_ANIMATION_CLOG(Verbose, "Refresh layer settings due to MovementState Changed.") + MovementState = MovementSystem->GetMovementState(); + MovementStateContainer = MovementState.GetSingleTagContainer(); + + RefreshLayerSettings(); + + bMovementStateChanged = true; + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + bMovementStateChanged = false; + }); +} + +void UGMS_MainAnimInstance::OnOverlayModeChanged_Implementation(const FGameplayTag& Prev) +{ + check(IsInGameThread()) + check(IsValid(MovementSystem)) + // GMS_ANIMATION_CLOG(Verbose, "Refresh layer settings due to OverlayMode Changed.") + OverlayMode = MovementSystem->GetOverlayMode(); + OverlayModeContainer = MovementSystem->GetOverlayMode().GetSingleTagContainer(); + + RefreshLayerSettings(); + + bOverlayModeChanged = true; + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + bOverlayModeChanged = false; + }); +} + +#pragma endregion Definition + + +void UGMS_MainAnimInstance::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); + + PawnOwner = Cast(GetOwningActor()); + +#if WITH_EDITOR + if (GetWorld() && !GetWorld()->IsGameWorld() && !IsValid(PawnOwner)) + { + // Use default objects for editor preview. + + PawnOwner = GetMutableDefault(); + MovementSystem = PawnOwner->FindComponentByClass(); + } +#endif +} + +void UGMS_MainAnimInstance::NativeUninitializeAnimation() +{ + if (IsValid(MovementSystem)) + { + MovementSystem->OnLocomotionModeChangedEvent.RemoveDynamic(this, &ThisClass::OnLocomotionModeChanged); + MovementSystem->OnRotationModeChangedEvent.RemoveDynamic(this, &ThisClass::OnRotationModeChanged); + MovementSystem->OnMovementSetChangedEvent.RemoveDynamic(this, &ThisClass::OnMovementSetChanged); + MovementSystem->OnMovementStateChangedEvent.RemoveDynamic(this, &ThisClass::OnMovementStateChanged); + MovementSystem->OnOverlayModeChangedEvent.RemoveDynamic(this, &ThisClass::OnOverlayModeChanged); + } + if (InitialTimerHandle.IsValid()) + { + GetWorld()->GetTimerManager().ClearTimer(InitialTimerHandle); + } + Super::NativeUninitializeAnimation(); +} + +void UGMS_MainAnimInstance::NativeBeginPlay() +{ + Super::NativeBeginPlay(); + ensure(PawnOwner); + + MovementSystem = PawnOwner->FindComponentByClass(); + + ensure(MovementSystem); + + if (IsValid(MovementSystem)) + { + // TrajectoryPredictor = MovementSystem->GetTrajectoryPredictor(); + MovementSystem->MainAnimInstance = this; + MovementSystem->OnLocomotionModeChangedEvent.AddDynamic(this, &ThisClass::OnLocomotionModeChanged); + MovementSystem->OnRotationModeChangedEvent.AddDynamic(this, &ThisClass::OnRotationModeChanged); + MovementSystem->OnMovementSetChangedEvent.AddDynamic(this, &ThisClass::OnMovementSetChanged); + MovementSystem->OnMovementStateChangedEvent.AddDynamic(this, &ThisClass::OnMovementStateChanged); + MovementSystem->OnOverlayModeChangedEvent.AddDynamic(this, &ThisClass::OnOverlayModeChanged); + + //Grab latest info and intialize. + FTimerDelegate Delegate = FTimerDelegate::CreateLambda([this]() + { + InitialTimerHandle.Invalidate(); + const FGMS_MovementSetSetting& MSSetting = MovementSystem->GetMovementSetSetting(); + + LocomotionMode = MovementSystem->GetLocomotionMode(); + LocomotionModeContainer = LocomotionMode.GetSingleTagContainer(); + + MovementSet = MovementSystem->GetMovementSet(); + MovementSetContainer = MovementSet.GetSingleTagContainer(); + + MovementState = MovementSystem->GetMovementState(); + MovementStateContainer = MovementState.GetSingleTagContainer(); + + RotationMode = MovementSystem->GetRotationMode(); + RotationModeContainer = MovementSystem->GetRotationMode().GetSingleTagContainer(); + + OverlayMode = MovementSystem->GetOverlayMode(); + OverlayModeContainer = MovementSystem->GetOverlayMode().GetSingleTagContainer(); + + RefreshLayerSettings(); + }); + + GetWorld()->GetTimerManager().SetTimer(InitialTimerHandle, Delegate, 0.2f, false); + } + else + { + GMS_ANIMATION_CLOG(Error, "Missing Movement system component, This anim instance(%s) will not work properly!", *GetClass()->GetName()) + } +} + +void UGMS_MainAnimInstance::NativeUpdateAnimation(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_MainAnimInstance::NativeUpdateAnimation"), STAT_GMS_MainAnimInstance_NativeUpdateAnimation, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + Super::NativeUpdateAnimation(DeltaTime); + + if (!IsValid(PawnOwner) || !IsValid(MovementSystem)) + { + return; + } + + if (UGMS_CharacterMovementSystemComponent* CharacterMovementSystemComponent = Cast(MovementSystem)) + { + if (!IsValid(CharacterMovementSystemComponent->GetCharacterMovement())) + { + return; + } + } + + RefreshStateOnGameThread(); + RefreshRelevanceOnGameThread(); + + bAnyMontagePlaying = IsAnyMontagePlaying(); +} + +void UGMS_MainAnimInstance::NativeThreadSafeUpdateAnimation(const float DeltaTime) +{ + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_MainAnimInstance::NativeThreadSafeUpdateAnimation"), STAT_GMS_MainAnimInstance_NativeThreadSafeUpdateAnimation, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + Super::NativeThreadSafeUpdateAnimation(DeltaTime); + + if (!IsValid(PawnOwner) || !IsValid(MovementSystem)) + { + return; + } + + RefreshTrajectoryState(DeltaTime); + RefreshLocomotion(DeltaTime); + RefreshGrounded(); + RefreshInAir(); + RefreshView(DeltaTime); +} + +void UGMS_MainAnimInstance::SetAnimLayerBySetting(const UGMS_AnimLayerSetting* LayerSetting, TObjectPtr& LayerInstance) +{ + check(IsInGameThread() && IsValid(MovementSystem) && IsValid(MovementSystem->AnimGraphSetting)) + + //invalid setting + if (!IsValid(LayerSetting)) + { + if (IsValid(LayerInstance)) + { + UnlinkAnimClassLayers(LayerInstance->GetClass()); + LayerInstance->OnUnlinked(); + LayerInstance = nullptr; + } + return; + } + + TSubclassOf LayerClass = nullptr; + if (!LayerSetting->GetOverrideAnimLayerClass(LayerClass)) + { + bool bValidMapping = MovementSystem->AnimGraphSetting->AnimLayerSettingToInstanceMapping.Contains(LayerSetting->GetClass()) && MovementSystem->AnimGraphSetting-> + AnimLayerSettingToInstanceMapping[LayerSetting-> + GetClass()] != nullptr; + + if (!bValidMapping) + { + GMS_ANIMATION_CLOG(Error, "Can't find exising anim instance mapping for anim layer setting(%s) or mapped a invalid anim instance. Please check anim graph setting:%s", + *LayerSetting->GetClass()->GetName(), *MovementSystem->AnimGraphSetting->GetName()) + if (IsValid(LayerInstance)) + { + UnlinkAnimClassLayers(LayerInstance->GetClass()); + LayerInstance->OnUnlinked(); + LayerInstance = nullptr; + } + + return; + } + + LayerClass = MovementSystem->AnimGraphSetting->AnimLayerSettingToInstanceMapping[LayerSetting->GetClass()]; + } + + if (IsValid(LayerInstance) && LayerClass != LayerInstance->GetClass()) + { + UnlinkAnimClassLayers(LayerInstance->GetClass()); + LayerInstance->OnUnlinked(); + LayerInstance = nullptr; + } + + if (!IsValid(LayerInstance)) + { + LinkAnimClassLayers(LayerClass); + LayerInstance = Cast(GetLinkedAnimLayerInstanceByClass(LayerClass)); + if (LayerInstance) + { + LayerInstance->OnLinked(); + } + else + { + GMS_ANIMATION_CLOG(Error, "Failed to link anim layer by class(%s), It will happen if this class doesn't implement any anim layer interface required on main anim instance. ", + *LayerClass->GetName()); + } + } + + if (LayerInstance) + { + LayerInstance->ApplySetting(LayerSetting); + } +} + +void UGMS_MainAnimInstance::RefreshTrajectoryState(float DeltaTime) +{ + // if (TScriptInterface Predictor = MovementSystem->GetTrajectoryPredictor()) + // { + // UPoseSearchTrajectoryLibrary::PoseSearchGenerateTransformTrajectoryWithPredictor(Predictor, GetDeltaSeconds(), TrajectoryState.Trajectory, TrajectoryState.DesiredControllerYaw, + // TrajectoryState.Trajectory, -1.0f, 30, 0.1, 15); + // + // UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(TrajectoryState.Trajectory, -0.3f, -0.4f, TrajectoryState.PastVelocity, false); + // UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(TrajectoryState.Trajectory, 0.0f, 0.2f, TrajectoryState.CurrentVelocity, false); + // UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(TrajectoryState.Trajectory, 0.4f, 0.5f, TrajectoryState.FutureVelocity, false); + // } +} + +void UGMS_MainAnimInstance::RefreshView(const float DeltaTime) +{ + // ViewState.YawAngle = FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - LocomotionState.Rotation.Yaw - RootState.YawOffset)); + ViewState.YawAngle = FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT(ViewState.Rotation.Yaw - LocomotionState.Rotation.Yaw)); + ViewState.PitchAngle = FRotator3f::NormalizeAxis(UE_REAL_TO_FLOAT(ViewState.Rotation.Pitch - LocomotionState.Rotation.Pitch)); + + ViewState.PitchAmount = 0.5f - ViewState.PitchAngle / 180.0f; +} + +void UGMS_MainAnimInstance::RefreshLocomotion(const float DeltaTime) +{ + const auto& ActorTransform = GetOwningActor()->GetActorTransform(); + + const auto ActorDeltaTime{GetDeltaSeconds() * GetOwningActor()->CustomTimeDilation}; + + const auto bCanCalculateRateOfChange{ActorDeltaTime > UE_SMALL_NUMBER}; + + // update location data + LocomotionState.PreviousDisplacement = (GetOwningActor()->GetActorLocation() - LocomotionState.Location).Size2D(); + LocomotionState.Location = ActorTransform.GetLocation(); + + auto PreviousYawAngle{LocomotionState.Rotation.Yaw}; + if (MovementBase.bHasRelativeRotation) + { + // Offset the angle to keep it relative to the movement base. + PreviousYawAngle = FMath::UnwindDegrees(UE_REAL_TO_FLOAT(PreviousYawAngle + MovementBase.DeltaRotation.Yaw)); + } + + // update rotation data + LocomotionState.Rotation = ActorTransform.Rotator(); + LocomotionState.RotationQuaternion = ActorTransform.GetRotation(); + + FVector PreviousVelocity{ + MovementBase.bHasRelativeRotation + ? MovementBase.DeltaRotation.RotateVector(LocomotionState.Velocity) + : LocomotionState.Velocity + }; + + if (bFirstUpdate) + { + LocomotionState.PreviousDisplacement = 0.0f; + LocomotionState.DisplacementSpeed = 0.0f; + PreviousVelocity = FVector::ZeroVector; + } + + // update velocity data + LocomotionState.bHasInput = GameLocomotionState.bHasInput; + + LocomotionState.Speed = GameLocomotionState.Speed; + LocomotionState.DisplacementSpeed = GameLocomotionState.Speed; + LocomotionState.Velocity = GameLocomotionState.Velocity; + LocomotionState.VelocityAcceleration = bCanCalculateRateOfChange ? (LocomotionState.Velocity - PreviousVelocity) / DeltaTime : FVector::ZeroVector; + + bool bWasMovingLastUpdate = !LocomotionState.LocalVelocity2D.IsZero(); + + LocomotionState.LocalVelocity2D = LocomotionState.RotationQuaternion.UnrotateVector({LocomotionState.Velocity.X, LocomotionState.Velocity.Y, 0.0f}); + + LocomotionState.bHasVelocity = !FMath::IsNearlyZero(LocomotionState.LocalVelocity2D.SizeSquared2D()); + + LocomotionState.LocalVelocityYawAngle = UKismetAnimationLibrary::CalculateDirection(LocomotionState.Velocity.GetSafeNormal2D(), LocomotionState.Rotation); + + LocomotionState.LocalVelocityYawAngleWithOffset = LocomotionState.LocalVelocityYawAngle - RootState.YawOffset; + + //take root yaw offset in account. 考虑到Offset的方向 + LocomotionState.LocalVelocityDirection = SelectCardinalDirectionFromAngle(LocomotionState.LocalVelocityYawAngleWithOffset, 10, LocomotionState.LocalVelocityDirection, + bWasMovingLastUpdate); + + LocomotionState.LocalVelocityDirectionNoOffset = SelectCardinalDirectionFromAngle(LocomotionState.LocalVelocityYawAngle, 10, LocomotionState.LocalVelocityDirectionNoOffset, + bWasMovingLastUpdate); + + LocomotionState.LocalVelocityOctagonalDirection = SelectOctagonalDirectionFromAngle(LocomotionState.LocalVelocityYawAngleWithOffset, 10, LocomotionState.LocalVelocityOctagonalDirection, + bWasMovingLastUpdate); + + LocomotionState.LocalAcceleration2D = UKismetMathLibrary::LessLess_VectorRotator({MovementIntent.X, MovementIntent.Y, 0.0f}, LocomotionState.Rotation); + LocomotionState.bMoving = GameLocomotionState.bMoving; + + + LocomotionState.YawVelocity = bCanCalculateRateOfChange + ? FMath::UnwindDegrees(UE_REAL_TO_FLOAT( + LocomotionState.Rotation.Yaw - PreviousYawAngle)) / ActorDeltaTime + : 0.0f; + + bFirstUpdate = false; +} + +void UGMS_MainAnimInstance::RefreshBlock() +{ + bBlocked = UKismetMathLibrary::VSizeXY(MovementIntent) > 0.1 && LocomotionState.Speed < 200.0f && + UKismetMathLibrary::InRange_FloatFloat(FVector::DotProduct(MovementIntent.GetSafeNormal(0.0001), LocomotionState.Velocity.GetSafeNormal(0.0001)), -0.6f, 0.6, true, true); +} + +void UGMS_MainAnimInstance::RefreshStateOnGameThread() +{ + LocomotionMode = MovementSystem->GetLocomotionMode(); + LocomotionModeContainer = LocomotionMode.GetSingleTagContainer(); + + MovementSet = MovementSystem->GetMovementSet(); + MovementSetContainer = MovementSet.GetSingleTagContainer(); + + MovementState = MovementSystem->GetMovementState(); + MovementStateContainer = MovementState.GetSingleTagContainer(); + + RotationMode = MovementSystem->GetRotationMode(); + RotationModeContainer = MovementSystem->GetRotationMode().GetSingleTagContainer(); + + OverlayMode = MovementSystem->GetOverlayMode(); + OverlayModeContainer = MovementSystem->GetOverlayMode().GetSingleTagContainer(); + + OwnedTags = MovementSystem->GetGameplayTags(); + + ControlSetting = MovementSystem->GetControlSetting(); + + GeneralSetting = MovementSystem->GetMovementSetSetting().AnimDataSetting_General; + + // Apply latest root setting if diff. + if (!GeneralSetting.bEnableOffsetRootBoneRotation && RootState.RotationMode != EOffsetRootBoneMode::Release) + { + RootState.RotationMode = EOffsetRootBoneMode::Release; + } + if (!GeneralSetting.bEnableOffsetRootBoneTranslation && RootState.TranslationMode != EOffsetRootBoneMode::Release) + { + RootState.TranslationMode = EOffsetRootBoneMode::Release; + } + + const auto& View{MovementSystem->GetViewState()}; + + ViewState.Rotation = View.Rotation; + ViewState.YawSpeed = View.YawSpeed; + + MovementBase = MovementSystem->GetMovementBase(); + + GameLocomotionState = MovementSystem->GetLocomotionState(); + + MovementIntent = MovementSystem->GetMovementIntent(); + + LocomotionState.MaxAcceleration = MovementSystem->GetMaxAcceleration(); + LocomotionState.MaxBrakingDeceleration = MovementSystem->GetMaxBrakingDeceleration(); + LocomotionState.WalkableFloorZ = MovementSystem->GetWalkableFloorZ(); + + LocomotionState.Scale = UE_REAL_TO_FLOAT(GetSkelMeshComponent()->GetComponentScale().Z); + + LocomotionState.CapsuleRadius = MovementSystem->GetScaledCapsuleRadius(); + LocomotionState.CapsuleHalfHeight = MovementSystem->GetScaledCapsuleHalfHeight(); +} + +void UGMS_MainAnimInstance::RefreshRelevanceOnGameThread() +{ + if (!IsValid(this)) + { + return; + } + + FGameplayTagContainer TagsToAdd; + + for (int i = 0; i < AnimStateNameToTagMapping.Num(); ++i) + { + if (AnimStateNameToTagMapping[i].State.IsRelevant(*this)) + { + TagsToAdd.AddTagFast(AnimStateNameToTagMapping[i].Tag); + } + } + + for (const TTuple, FGMS_AnimStateNameToTagWrapper>& Pair : RuntimeAnimStateNameToTagMappings) + { + if (IsValid(Pair.Key)) + { + for (int i = 0; i < Pair.Value.AnimStateNameToTagMapping.Num(); ++i) + { + if (Pair.Value.AnimStateNameToTagMapping[i].State.IsRelevant(*Pair.Key)) + { + TagsToAdd.AddTagFast(Pair.Value.AnimStateNameToTagMapping[i].Tag); + } + } + } + } + + NodeRelevanceTags = TagsToAdd; +} + + +void UGMS_MainAnimInstance::RefreshGrounded() +{ +#if WITH_EDITOR + if (!IsValid(GetWorld()) || !GetWorld()->IsGameWorld()) + { + return; + } +#endif + + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_MainAnimInstance::RefreshGrounded"), STAT_GMS_MainAnimInstance_RefreshGrounded, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + if (LocomotionMode != GMS_MovementModeTags::Grounded) + { + return; + } + + RefreshBlock(); + RefreshGroundedLean(); +} + +void UGMS_MainAnimInstance::RefreshGroundedLean() +{ + const auto TargetLeanAmount{GetRelativeAccelerationAmount()}; + + const auto DeltaTime{GetDeltaSeconds()}; + + LeanState.RightAmount = FMath::FInterpTo(LeanState.RightAmount, TargetLeanAmount.Y, + DeltaTime, GeneralSetting.LeanInterpolationSpeed); + + LeanState.ForwardAmount = FMath::FInterpTo(LeanState.ForwardAmount, TargetLeanAmount.X, + DeltaTime, GeneralSetting.LeanInterpolationSpeed); +} + +FVector2f UGMS_MainAnimInstance::GetRelativeAccelerationAmount() const +{ + // This value represents the current amount of acceleration / deceleration relative to the + // character rotation. It is normalized to a range of -1 to 1 so that -1 equals the max + // braking deceleration and 1 equals the max acceleration of the character movement component. + + const auto MaxAcceleration{ + (MovementIntent | LocomotionState.Velocity) >= 0.0f + ? LocomotionState.MaxAcceleration + : LocomotionState.MaxBrakingDeceleration + }; + + // relative to root bone transform. + const FVector3f RelativeAcceleration{ + RootState.RootTransform.GetRotation().UnrotateVector(LocomotionState.VelocityAcceleration) + }; + + + return FVector2f{UGMS_Vector::ClampMagnitude01(RelativeAcceleration / MaxAcceleration)}; +} + + +void UGMS_MainAnimInstance::RefreshInAir() +{ +#if WITH_EDITOR + if (!IsValid(GetWorld()) || !GetWorld()->IsGameWorld()) + { + return; + } +#endif + + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGMS_MainAnimInstance::RefreshInAir"), STAT_GMS_MainAnimInstance_RefreshInAir, STATGROUP_GMS) + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + + // InAirState.bJumping = false; + // InAirState.bFalling = false; + + if (LocomotionMode != GMS_MovementModeTags::InAir) + { + // InAirState.VerticalSpeed = 0.0f; Land calculation need it. + return; + } + + // A separate variable for vertical speed is used to determine at what speed the character landed on the ground. + + if (LocomotionState.Velocity.Z > 0) + { + InAirState.bJumping = true; + InAirState.TimeToJumpApex = (0 - LocomotionState.Velocity.Z) / MovementSystem->GetGravityZ(); + InAirState.FallingTime = 0; + } + else + { + InAirState.bFalling = true; + InAirState.TimeToJumpApex = 0; + InAirState.FallingTime += GetDeltaSeconds(); + } + + InAirState.VerticalSpeed = UE_REAL_TO_FLOAT(LocomotionState.Velocity.Z); + + RefreshGroundPrediction(); + + RefreshInAirLean(); +} + +void UGMS_MainAnimInstance::RefreshGroundPrediction() +{ + if (!bEnableGroundPrediction) + { + return; + } + + static constexpr auto VerticalVelocityThreshold{-200.0f}; + + if (InAirState.VerticalSpeed > VerticalVelocityThreshold) + { + InAirState.bValidGround = false; + InAirState.GroundDistance = -1.0f; + return; + } + + const auto SweepStartLocation{LocomotionState.Location}; + + static constexpr auto MinVerticalVelocity{-4000.0f}; + static constexpr auto MaxVerticalVelocity{-200.0f}; + + auto VelocityDirection{LocomotionState.Velocity}; + VelocityDirection.Z = FMath::Clamp(VelocityDirection.Z, MinVerticalVelocity, MaxVerticalVelocity); + VelocityDirection.Normalize(); + + static constexpr auto MinSweepDistance{150.0f}; + static constexpr auto MaxSweepDistance{2000.0f}; + + const auto SweepVector{ + VelocityDirection * FMath::GetMappedRangeValueClamped(FVector2f{MaxVerticalVelocity, MinVerticalVelocity}, + {MinSweepDistance, MaxSweepDistance}, + InAirState.VerticalSpeed) * LocomotionState.Scale + }; + + FHitResult Hit; + GetWorld()->SweepSingleByChannel(Hit, SweepStartLocation, SweepStartLocation + SweepVector, + FQuat::Identity, GeneralSetting.GroundPredictionSweepChannel, + FCollisionShape::MakeCapsule(LocomotionState.CapsuleRadius, LocomotionState.CapsuleHalfHeight), + {__FUNCTION__, false, PawnOwner}, GeneralSetting.GroundPredictionSweepResponses); + + const auto bGroundValid{Hit.IsValidBlockingHit() && Hit.ImpactNormal.Z >= LocomotionState.WalkableFloorZ}; + + InAirState.bValidGround = bGroundValid; + InAirState.GroundDistance = Hit.Distance; +} + +void UGMS_MainAnimInstance::RefreshInAirLean() +{ + if (GeneralSetting.InAirLeanAmountCurve == nullptr) + { + return; + } + + // Use the relative velocity direction and amount to determine how much the character should lean + // while in air. The lean amount curve gets the vertical velocity and is used as a multiplier to + // smoothly reverse the leaning direction when transitioning from moving upwards to moving downwards. + + static constexpr auto ReferenceSpeed{350.0f}; + + const auto RelativeVelocity{ + FVector3f{LocomotionState.RotationQuaternion.UnrotateVector(LocomotionState.Velocity)} / + ReferenceSpeed * GeneralSetting.InAirLeanAmountCurve->GetFloatValue(InAirState.VerticalSpeed) + }; + + const auto DeltaTime{GetDeltaSeconds()}; + + LeanState.RightAmount = FMath::FInterpTo(LeanState.RightAmount, RelativeVelocity.Y, + DeltaTime, GeneralSetting.LeanInterpolationSpeed); + + LeanState.ForwardAmount = FMath::FInterpTo(LeanState.ForwardAmount, RelativeVelocity.X, + DeltaTime, GeneralSetting.LeanInterpolationSpeed); +} + +void UGMS_MainAnimInstance::RefreshOffsetRootBone_Implementation(FAnimUpdateContext& Context, FAnimNodeReference& Node) +{ + if (GetMovementSystemComponent() && GetMovementSystemComponent()->GetControlSetting()) + { + // 获取 OffsetRootBone 节点的根骨骼变换(世界空间) + auto RootBoneTransform = UAnimationWarpingLibrary::GetOffsetRootTransform(Node); + + // 始终将 RootState.RootTransform 设置为世界空间变换(应用 +90 度 Yaw 调整) + FRotator RootBoneRotation = FRotator(RootBoneTransform.Rotator().Pitch, RootBoneTransform.Rotator().Yaw + 90.0f, RootBoneTransform.Rotator().Roll); + + RootState.RootTransform = FTransform(RootBoneRotation, RootBoneTransform.GetTranslation(), RootBoneTransform.GetScale3D()); + + // 直接使用世界空间旋转计算 YawOffset + RootState.YawOffset = UKismetMathLibrary::NormalizeAxis(RootBoneRotation.Yaw - LocomotionState.Rotation.Yaw); + + if (RotationMode == GMS_RotationModeTags::ViewDirection) + { + if (const FGMS_ViewDirectionSetting_Aiming* Setting = GetMovementSystemComponent()->GetControlSetting()->ViewDirectionSetting.GetPtr()) + { + if (FMath::Abs(RootState.YawOffset) <= Setting->MinAimingYawAngleLimit + UE_KINDA_SMALL_NUMBER) + { + RootState.MaxRotationError = Setting->MinAimingYawAngleLimit; + return; + } + } + } + + // no limit. + RootState.MaxRotationError = -1.0f; + } +} + +float UGMS_MainAnimInstance::GetCurveValueClamped01(const FName& CurveName) const +{ + return UGMS_Math::Clamp01(GetCurveValue(CurveName)); +} + +UBlendProfile* UGMS_MainAnimInstance::GetNamedBlendProfile(const FName& BlendProfileName) const +{ + if (CurrentSkeleton) + { + return CurrentSkeleton->GetBlendProfile(BlendProfileName); + } + return nullptr; +} + +FGameplayTagContainer UGMS_MainAnimInstance::GetAggregatedTags() const +{ + FGameplayTagContainer Result = OwnedTags; + Result.AppendTags(NodeRelevanceTags); + return Result; +} + +float UGMS_MainAnimInstance::GetAOYawValue() const +{ + if (RootState.RotationMode == EOffsetRootBoneMode::Release) + { + return ViewState.YawAngle; + } + return -RootState.YawOffset; +} + + +EGMS_MovementDirection UGMS_MainAnimInstance::SelectCardinalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection CurrentDirection, bool bUseCurrentDirection) const +{ + const float AbsAngle = FMath::Abs(Angle); + float FwdDeadZone = DeadZone; + float BwdDeadZone = DeadZone; + if (bUseCurrentDirection) + { + if (CurrentDirection == EGMS_MovementDirection::Forward) + { + FwdDeadZone *= 2; + } + if (CurrentDirection == EGMS_MovementDirection::Backward) + { + BwdDeadZone *= 2; + } + } + + if (AbsAngle <= 45 + FwdDeadZone) + { + return EGMS_MovementDirection::Forward; + } + + if (AbsAngle >= 135 - BwdDeadZone) + { + return EGMS_MovementDirection::Backward; + } + if (Angle > 0) + { + return EGMS_MovementDirection::Right; + } + + return EGMS_MovementDirection::Left; +} + +EGMS_MovementDirection_8Way UGMS_MainAnimInstance::SelectOctagonalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection_8Way CurrentDirection, + bool bUseCurrentDirection) const +{ + const float AbsAngle = FMath::Abs(Angle); + float FwdDeadZone = DeadZone; + float BwdDeadZone = DeadZone; + if (bUseCurrentDirection) + { + if (CurrentDirection == EGMS_MovementDirection_8Way::Forward) + { + FwdDeadZone *= 2; + } + if (CurrentDirection == EGMS_MovementDirection_8Way::Backward) + { + BwdDeadZone *= 2; + } + } + + if (AbsAngle <= 22.5f + FwdDeadZone) + { + return EGMS_MovementDirection_8Way::Forward; + } + if (AbsAngle >= 157.5f - BwdDeadZone) + { + return EGMS_MovementDirection_8Way::Backward; + } + if (Angle >= 22.5f && Angle < 67.5f) + { + return EGMS_MovementDirection_8Way::ForwardRight; + } + if (Angle >= 67.5f && Angle < 112.5f) + { + return EGMS_MovementDirection_8Way::Right; + } + if (Angle >= 112.5f && Angle < 157.5f) + { + return EGMS_MovementDirection_8Way::BackwardRight; + } + if (Angle >= -157.5f && Angle < -112.5f) + { + return EGMS_MovementDirection_8Way::BackwardLeft; + } + if (Angle >= -112.5f && Angle < -67.5f) + { + return EGMS_MovementDirection_8Way::Left; + } + return EGMS_MovementDirection_8Way::ForwardLeft; +} + +EGMS_MovementDirection UGMS_MainAnimInstance::GetOppositeCardinalDirection(EGMS_MovementDirection CurrentDirection) const +{ + switch (CurrentDirection) + { + case EGMS_MovementDirection::Forward: + return EGMS_MovementDirection::Backward; + case EGMS_MovementDirection::Backward: + return EGMS_MovementDirection::Forward; + case EGMS_MovementDirection::Left: + return EGMS_MovementDirection::Right; + case EGMS_MovementDirection::Right: + return EGMS_MovementDirection::Left; + default: + return CurrentDirection; + } +} + +bool UGMS_MainAnimInstance::HasCoreStateChanges() const +{ + return bMovementSetChanged || bMovementStateChanged || bLocomotionModeChanged || bOverlayModeChanged || bRotationModeChanged; +} + +bool UGMS_MainAnimInstance::CheckCoreStateChanges(bool bCheckLocomotionMode, bool bCheckMovementSet, bool bCheckRotationMode, bool bCheckMovementState, bool bCheckOverlayMode) const +{ + return (bCheckLocomotionMode && bLocomotionModeChanged) || + (bCheckMovementSet && bMovementSetChanged) || + (bCheckRotationMode && bRotationModeChanged) || + (bCheckMovementState && bMovementStateChanged) || + (bCheckOverlayMode && bOverlayModeChanged); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Movement/GMS_CharacterMovementComponent.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Movement/GMS_CharacterMovementComponent.cpp new file mode 100644 index 0000000..0747850 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Movement/GMS_CharacterMovementComponent.cpp @@ -0,0 +1,478 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// +// #include "Movement/GMS_CharacterMovementComponent.h" +// +// #include "GMS_CharacterMovementSystemComponent.h" +// #include "GMS_MovementSystemComponent.h" +// #include "GameFramework/Character.h" +// #include "Locomotions/GMS_MainAnimInstance.h" +// #include "Settings/GMS_SettingObjectLibrary.h" +// #include "Utility/GMS_Constants.h" +// #include "Utility/GMS_Log.h" +// #include "Utility/GMS_Math.h" +// +// #include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_CharacterMovementComponent) +// +// UGMS_CharacterMovementComponent::UGMS_CharacterMovementComponent(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) +// { +// } +// +// void UGMS_CharacterMovementComponent::InitializeComponent() +// { +// Super::InitializeComponent(); +// if (CharacterOwner) +// { +// UGMS_CharacterMovementSystemComponent* NewMovementSystem = CharacterOwner->FindComponentByClass(); +// if (NewMovementSystem == nullptr) +// { +// GMS_CLOG(Warning, "Requires GMS Character Movement System Component to function!") +// return; +// } +// MovementSystem = NewMovementSystem; +// } +// } +// +// void UGMS_CharacterMovementComponent::SetUpdatedComponent(USceneComponent* NewUpdatedComponent) +// { +// Super::SetUpdatedComponent(NewUpdatedComponent); +// if (CharacterOwner) +// { +// UGMS_CharacterMovementSystemComponent* NewMovementSystem = CharacterOwner->FindComponentByClass(); +// if (NewMovementSystem == nullptr) +// { +// GMS_CLOG(Warning, "Requires GMS Character Movement System Component to function!") +// return; +// } +// MovementSystem = NewMovementSystem; +// } +// } +// +// bool UGMS_CharacterMovementComponent::HasValidData() const +// { +// return Super::HasValidData() && IsValid(MovementSystem); +// } +// +// void UGMS_CharacterMovementComponent::TickCharacterPose(float DeltaTime) +// { +// Super::TickCharacterPose(DeltaTime); +// } +// +// void UGMS_CharacterMovementComponent::PhysicsRotation(float DeltaTime) +// { +// if (bUseNativeRotation) +// { +// Super::PhysicsRotation(DeltaTime); +// } +// else +// { +// GMS_PhysicsRotation(DeltaTime); +// } +// } +// +// void UGMS_CharacterMovementComponent::GMS_TurnToDesiredRotation(const FRotator& CurrentRotation, FRotator DesiredRotation, FRotator DeltaRot) +// { +// const bool bWantsToBeVertical = ShouldRemainVertical(); +// +// if (bWantsToBeVertical) +// { +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeDesiredRotation = (GetWorldToGravityTransform() * DesiredRotation.Quaternion()).Rotator(); +// GravityRelativeDesiredRotation.Pitch = 0.f; +// GravityRelativeDesiredRotation.Yaw = FRotator::NormalizeAxis(GravityRelativeDesiredRotation.Yaw); +// GravityRelativeDesiredRotation.Roll = 0.f; +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// DesiredRotation.Pitch = 0.f; +// DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.Yaw); +// DesiredRotation.Roll = 0.f; +// } +// } +// else +// { +// DesiredRotation.Normalize(); +// } +// +// // Accumulate a desired new rotation. +// constexpr float AngleTolerance = 1e-3f; +// +// if (!CurrentRotation.Equals(DesiredRotation, AngleTolerance)) +// { +// // If we'd be prevented from becoming vertical, override the non-yaw rotation rates to allow the character to snap upright +// +// static bool PreventNonVerticalOrientationBlock = true; +// if (PreventNonVerticalOrientationBlock && bWantsToBeVertical) +// { +// if (FMath::IsNearlyZero(DeltaRot.Pitch)) +// { +// DeltaRot.Pitch = 360.0; +// } +// if (FMath::IsNearlyZero(DeltaRot.Roll)) +// { +// DeltaRot.Roll = 360.0; +// } +// } +// +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeCurrentRotation = (GetWorldToGravityTransform() * CurrentRotation.Quaternion()).Rotator(); +// FRotator GravityRelativeDesiredRotation = (GetWorldToGravityTransform() * DesiredRotation.Quaternion()).Rotator(); +// +// // PITCH +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, AngleTolerance)) +// { +// // GravityRelativeDesiredRotation.Pitch = UGMS_Math::ExponentialDecayAngle(GravityRelativeCurrentRotation.Pitch, +// // GravityRelativeDesiredRotation.Pitch, DeltaTime, DeltaRot.Pitch); +// GravityRelativeDesiredRotation.Pitch = FMath::FixedTurn(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, AngleTolerance)) +// { +// // GravityRelativeDesiredRotation.Yaw = UGMS_Math::ExponentialDecayAngle(GravityRelativeCurrentRotation.Yaw, +// // GravityRelativeDesiredRotation.Yaw, DeltaTime, DeltaRot.Yaw); +// GravityRelativeDesiredRotation.Yaw = FMath::FixedTurn(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, AngleTolerance)) +// { +// // GravityRelativeDesiredRotation.Roll = UGMS_Math::ExponentialDecayAngle(GravityRelativeCurrentRotation.Roll, +// // GravityRelativeDesiredRotation.Roll, DeltaTime, DeltaRot.Roll); +// GravityRelativeDesiredRotation.Roll = FMath::FixedTurn(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, DeltaRot.Roll); +// } +// +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// // PITCH +// if (!FMath::IsNearlyEqual(CurrentRotation.Pitch, DesiredRotation.Pitch, AngleTolerance)) +// { +// // DesiredRotation.Pitch = UGMS_Math::ExponentialDecayAngle(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaTime, DeltaRot.Pitch); +// DesiredRotation.Pitch = FMath::FixedTurn(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(CurrentRotation.Yaw, DesiredRotation.Yaw, AngleTolerance)) +// { +// // DesiredRotation.Yaw = UGMS_Math::ExponentialDecayAngle(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaTime, DeltaRot.Yaw); +// DesiredRotation.Yaw = FMath::FixedTurn(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(CurrentRotation.Roll, DesiredRotation.Roll, AngleTolerance)) +// { +// // DesiredRotation.Roll = UGMS_Math::ExponentialDecayAngle(CurrentRotation.Roll, DesiredRotation.Roll, DeltaTime, DeltaRot.Yaw); +// DesiredRotation.Roll = FMath::FixedTurn(CurrentRotation.Roll, DesiredRotation.Roll, DeltaRot.Roll); +// } +// } +// +// // Set the new rotation. +// DesiredRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): DesiredRotation")); +// MoveUpdatedComponent(FVector::ZeroVector, DesiredRotation, /*bSweep*/ false); +// } +// } +// +// void UGMS_CharacterMovementComponent::GMS_TurnToDesiredRotationWithRotationRate(const FRotator& CurrentRotation, FRotator DesiredRotation, FRotator DeltaRot) +// { +// const bool bWantsToBeVertical = ShouldRemainVertical(); +// +// if (bWantsToBeVertical) +// { +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeDesiredRotation = (GetWorldToGravityTransform() * DesiredRotation.Quaternion()).Rotator(); +// GravityRelativeDesiredRotation.Pitch = 0.f; +// GravityRelativeDesiredRotation.Yaw = FRotator::NormalizeAxis(GravityRelativeDesiredRotation.Yaw); +// GravityRelativeDesiredRotation.Roll = 0.f; +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// DesiredRotation.Pitch = 0.f; +// DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.Yaw); +// DesiredRotation.Roll = 0.f; +// } +// } +// else +// { +// DesiredRotation.Normalize(); +// } +// +// // Accumulate a desired new rotation. +// constexpr float AngleTolerance = 1e-3f; +// +// if (!CurrentRotation.Equals(DesiredRotation, AngleTolerance)) +// { +// // If we'd be prevented from becoming vertical, override the non-yaw rotation rates to allow the character to snap upright +// +// static bool PreventNonVerticalOrientationBlock = true; +// if (PreventNonVerticalOrientationBlock && bWantsToBeVertical) +// { +// if (FMath::IsNearlyZero(DeltaRot.Pitch)) +// { +// DeltaRot.Pitch = 360.0; +// } +// if (FMath::IsNearlyZero(DeltaRot.Roll)) +// { +// DeltaRot.Roll = 360.0; +// } +// } +// +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeCurrentRotation = (GetWorldToGravityTransform() * CurrentRotation.Quaternion()).Rotator(); +// FRotator GravityRelativeDesiredRotation = (GetWorldToGravityTransform() * DesiredRotation.Quaternion()).Rotator(); +// +// // PITCH +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Pitch = FMath::FixedTurn(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Yaw = FMath::FixedTurn(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Roll = FMath::FixedTurn(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, DeltaRot.Roll); +// } +// +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// // PITCH +// if (!FMath::IsNearlyEqual(CurrentRotation.Pitch, DesiredRotation.Pitch, AngleTolerance)) +// { +// DesiredRotation.Pitch = FMath::FixedTurn(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(CurrentRotation.Yaw, DesiredRotation.Yaw, AngleTolerance)) +// { +// DesiredRotation.Yaw = FMath::FixedTurn(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(CurrentRotation.Roll, DesiredRotation.Roll, AngleTolerance)) +// { +// DesiredRotation.Roll = FMath::FixedTurn(CurrentRotation.Roll, DesiredRotation.Roll, DeltaRot.Roll); +// } +// } +// +// // Set the new rotation. +// DesiredRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): DesiredRotation")); +// MoveUpdatedComponent(FVector::ZeroVector, DesiredRotation, /*bSweep*/ false); +// } +// } +// +// void UGMS_CharacterMovementComponent::GMS_PhysicsRotation_Implementation(float DeltaTime) +// { +// if (!(bOrientRotationToMovement || bUseControllerDesiredRotation)) +// { +// return; +// } +// +// if (!HasValidData() || (!CharacterOwner->Controller && !bRunPhysicsWithNoController)) +// { +// return; +// } +// +// FRotator CurrentRotation = UpdatedComponent->GetComponentRotation(); // Normalized +// CurrentRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): CurrentRotation")); +// +// FRotator DeltaRot = GetDeltaRotation(DeltaTime); +// DeltaRot.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): GetDeltaRotation")); +// +// FRotator DesiredRotation = CurrentRotation; +// if (bOrientRotationToMovement) +// { +// DesiredRotation = GMS_ComputeOrientToDesiredMovementRotation(CurrentRotation, DeltaTime, DeltaRot); +// } +// else if (CharacterOwner->Controller && bUseControllerDesiredRotation) +// { +// DesiredRotation = CharacterOwner->Controller->GetDesiredRotation(); +// // DesiredRotation = MovementSystem->GetViewState().Rotation; +// } +// else if (!CharacterOwner->Controller && bRunPhysicsWithNoController && bUseControllerDesiredRotation) +// { +// if (AController* ControllerOwner = Cast(CharacterOwner->GetOwner())) +// { +// DesiredRotation = ControllerOwner->GetDesiredRotation(); +// } +// } +// else +// { +// return; +// } +// +// const bool bWantsToBeVertical = ShouldRemainVertical(); +// +// if (bWantsToBeVertical) +// { +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeDesiredRotation = (GetGravityToWorldTransform() * DesiredRotation.Quaternion()).Rotator(); +// GravityRelativeDesiredRotation.Pitch = 0.f; +// GravityRelativeDesiredRotation.Yaw = FRotator::NormalizeAxis(GravityRelativeDesiredRotation.Yaw); +// GravityRelativeDesiredRotation.Roll = 0.f; +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// DesiredRotation.Pitch = 0.f; +// DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.Yaw); +// DesiredRotation.Roll = 0.f; +// } +// } +// else +// { +// DesiredRotation.Normalize(); +// } +// +// // Accumulate a desired new rotation. +// constexpr float AngleTolerance = 1e-3f; +// +// if (!CurrentRotation.Equals(DesiredRotation, AngleTolerance)) +// { +// // If we'd be prevented from becoming vertical, override the non-yaw rotation rates to allow the character to snap upright +// if (true && bWantsToBeVertical) +// { +// if (FMath::IsNearlyZero(DeltaRot.Pitch)) +// { +// DeltaRot.Pitch = 360.0; +// } +// if (FMath::IsNearlyZero(DeltaRot.Roll)) +// { +// DeltaRot.Roll = 360.0; +// } +// } +// +// if (HasCustomGravity()) +// { +// FRotator GravityRelativeCurrentRotation = (GetGravityToWorldTransform() * CurrentRotation.Quaternion()).Rotator(); +// FRotator GravityRelativeDesiredRotation = (GetGravityToWorldTransform() * DesiredRotation.Quaternion()).Rotator(); +// +// // PITCH +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Pitch = FMath::FixedTurn(GravityRelativeCurrentRotation.Pitch, GravityRelativeDesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Yaw = FMath::FixedTurn(GravityRelativeCurrentRotation.Yaw, GravityRelativeDesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, AngleTolerance)) +// { +// GravityRelativeDesiredRotation.Roll = FMath::FixedTurn(GravityRelativeCurrentRotation.Roll, GravityRelativeDesiredRotation.Roll, DeltaRot.Roll); +// } +// +// DesiredRotation = (GetWorldToGravityTransform() * GravityRelativeDesiredRotation.Quaternion()).Rotator(); +// } +// else +// { +// // PITCH +// if (!FMath::IsNearlyEqual(CurrentRotation.Pitch, DesiredRotation.Pitch, AngleTolerance)) +// { +// DesiredRotation.Pitch = FMath::FixedTurn(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaRot.Pitch); +// } +// +// // YAW +// if (!FMath::IsNearlyEqual(CurrentRotation.Yaw, DesiredRotation.Yaw, AngleTolerance)) +// { +// DesiredRotation.Yaw = FMath::FixedTurn(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaRot.Yaw); +// } +// +// // ROLL +// if (!FMath::IsNearlyEqual(CurrentRotation.Roll, DesiredRotation.Roll, AngleTolerance)) +// { +// DesiredRotation.Roll = FMath::FixedTurn(CurrentRotation.Roll, DesiredRotation.Roll, DeltaRot.Roll); +// } +// } +// +// // Set the new rotation. +// DesiredRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): DesiredRotation")); +// MoveUpdatedComponent(FVector::ZeroVector, DesiredRotation, /*bSweep*/ false); +// } +// } +// +// FRotator UGMS_CharacterMovementComponent::GMS_ComputeOrientToDesiredViewRotation_Implementation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const +// { +// check(MovementSystem->GetControlSetting()); +// +// bool bMoving = MovementSystem->GetLocomotionState().bMoving; +// +// DeltaRotation.Yaw = DeltaRotation.Pitch = DeltaRotation.Roll = MovementSystem->GetControlSetting()->ViewDirectionSetting.Get().RotationInterpolationSpeed; +// +// float DeltaYawAngle{0.0f}; +// if (!bMoving && HasAnimationRotationDeltaYawAngle(DeltaTime, DeltaYawAngle)) +// { +// auto NewRotation{CurrentRotation}; +// NewRotation.Yaw += DeltaYawAngle; +// DeltaRotation.Yaw = -1; +// return NewRotation; +// } +// +// FRotator ControllerDesiredRotation = CharacterOwner->Controller->GetDesiredRotation(); +// +// if (const FGMS_ViewDirectionSetting_Default* Setting = MovementSystem->GetControlSetting()->ViewDirectionSetting.GetPtr()) +// { +// if (bMoving || Setting->bEnableRotationWhenNotMoving) +// { +// auto NewRotation{CurrentRotation}; +// NewRotation.Yaw = ControllerDesiredRotation.Yaw; +// return NewRotation; +// } +// return CurrentRotation; +// } +// +// if (const FGMS_ViewDirectionSetting_Aiming* Setting = MovementSystem->GetControlSetting()->ViewDirectionSetting.GetPtr()) +// { +// if (!bMoving && Setting->bEnableRotationWhenNotMoving) +// { +// auto NewRotation{CurrentRotation}; +// NewRotation.Yaw = ControllerDesiredRotation.Yaw; +// return NewRotation; +// } +// } +// +// return CurrentRotation; +// } +// +// +// bool UGMS_CharacterMovementComponent::HasAnimationRotationDeltaYawAngle_Implementation(float DeltaTime, float& OutDeltaYawAngle) const +// { +// UAnimInstance* AnimInstance = CharacterOwner->GetMesh()->GetAnimInstance(); +// if (UGMS_MainAnimInstance* AnimInst = Cast(AnimInstance)) +// { +// if (AnimInst->GetOffsetRootBoneRotationMode() != EOffsetRootBoneMode::Release) +// { +// return false; +// } +// } +// +// const float CurveValue = AnimInstance->GetCurveValue(UGMS_Constants::RotationYawSpeedCurveName()); +// +// OutDeltaYawAngle = CurveValue * DeltaTime; +// +// return FMath::Abs(OutDeltaYawAngle) > UE_SMALL_NUMBER; +// } +// +// +// FRotator UGMS_CharacterMovementComponent::GMS_ComputeOrientToDesiredMovementRotation_Implementation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const +// { +// return Super::ComputeOrientToMovementRotation(CurrentRotation, DeltaTime, DeltaRotation); +// } diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Flying/GMS_FlyingMode.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Flying/GMS_FlyingMode.cpp new file mode 100644 index 0000000..e9c41b2 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Flying/GMS_FlyingMode.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/Flying/GMS_FlyingMode.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_FlyingMode) \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverSettingObjectLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverSettingObjectLibrary.cpp new file mode 100644 index 0000000..fde03a1 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverSettingObjectLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/GMS_MoverSettingObjectLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_MoverSettingObjectLibrary) \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverStructLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverStructLibrary.cpp new file mode 100644 index 0000000..5f3d146 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/GMS_MoverStructLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/GMS_MoverStructLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_MoverStructLibrary) \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Modifers/GMS_MovementStateModifer.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Modifers/GMS_MovementStateModifer.cpp new file mode 100644 index 0000000..11cb5f2 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Modifers/GMS_MovementStateModifer.cpp @@ -0,0 +1,150 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// +// #include "Mover/Modifers/GMS_MovementStateModifer.h" +// +// #include "MoverComponent.h" +// #include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h" +// #include "MoveLibrary/MovementUtils.h" +// #include "Mover/GMS_MoverStructLibrary.h" +// +// +// FGMS_MovementStateModifier::FGMS_MovementStateModifier() +// { +// DurationMs = -1.0f; +// } +// +// void FGMS_MovementStateModifier::OnStart(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) +// { +// const FGMS_MoverMovementControlInputs* Inputs = SyncState.SyncStateCollection.FindMutableDataByType(); +// +// if (UStanceSettings* StanceSettings = MoverComp->FindSharedSettings_Mutable()) +// { +// if (const UCapsuleComponent* CapsuleComponent = Cast(MoverComp->GetUpdatedComponent())) +// { +// float OldHalfHeight = CapsuleComponent->GetScaledCapsuleHalfHeight(); +// float NewHalfHeight = 0; +// float NewEyeHeight = 0; +// +// switch (ActiveStance) +// { +// default: +// case EStanceMode::Crouch: +// NewHalfHeight = StanceSettings->CrouchHalfHeight; +// NewEyeHeight = StanceSettings->CrouchedEyeHeight; +// break; +// +// // Prone isn't currently implemented +// case EStanceMode::Prone: +// UE_LOG(LogMover, Warning, TEXT("Stance got into prone stance - That stance is not currently implemented.")); +// // TODO: returning here so we don't apply any bad state to actor in case prone was set. Eventually, the return should be removed once prone is implemented properly +// DurationMs = 0; +// return; +// } +// +// ApplyMovementSettings(MoverComp); +// } +// } +// } +// +// void FGMS_MovementStateModifier::OnEnd(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) +// { +// const AActor* OwnerCDO = Cast(MoverComp->GetOwner()->GetClass()->GetDefaultObject()); +// +// if (UCapsuleComponent* CapsuleComponent = Cast(MoverComp->GetUpdatedComponent())) +// { +// if (const UCapsuleComponent* OriginalCapsule = UMovementUtils::GetOriginalComponentType(MoverComp->GetOwner())) +// { +// if (const APawn* OwnerCDOAsPawn = Cast(OwnerCDO)) +// { +// AdjustCapsule(MoverComp, CapsuleComponent->GetScaledCapsuleHalfHeight(), OriginalCapsule->GetScaledCapsuleHalfHeight(), OwnerCDOAsPawn->BaseEyeHeight); +// RevertMovementSettings(MoverComp); +// } +// } +// } +// } +// +// void FGMS_MovementStateModifier::OnPreMovement(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep) +// { +// // TODO: Check for different inputs/state here and manage swapping between stances - use AdjustCapsule and Apply/Revert movement settings. +// +// // TODO: Prone isn't currently implemented - so we're just going to cancel the modifier if we got into that state +// if (ActiveStance == EStanceMode::Prone) +// { +// UE_LOG(LogMover, Warning, TEXT("Stance got into prone stance - That stance is not currently implemented.")); +// DurationMs = 0; +// } +// } +// +// void FGMS_MovementStateModifier::OnPostMovement(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) +// { +// FMovementModifierBase::OnPostMovement(MoverComp, TimeStep, SyncState, AuxState); +// } +// +// FMovementModifierBase* FGMS_MovementStateModifier::Clone() const +// { +// FGMS_MovementStateModifier* CopyPtr = new FGMS_MovementStateModifier(*this); +// return CopyPtr; +// } +// +// void FGMS_MovementStateModifier::NetSerialize(FArchive& Ar) +// { +// Super::NetSerialize(Ar); +// } +// +// UScriptStruct* FGMS_MovementStateModifier::GetScriptStruct() const +// { +// return StaticStruct(); +// } +// +// FString FGMS_MovementStateModifier::ToSimpleString() const +// { +// return FString::Printf(TEXT("Stance Modifier")); +// } +// +// void FGMS_MovementStateModifier::AddReferencedObjects(FReferenceCollector& Collector) +// { +// Super::AddReferencedObjects(Collector); +// } +// +// +// void FGMS_MovementStateModifier::ApplyMovementSettings(UMoverComponent* MoverComp) +// { +// switch (ActiveStance) +// { +// default: +// case EStanceMode::Crouch: +// if (UStanceSettings* StanceSettings = MoverComp->FindSharedSettings_Mutable()) +// { +// // Update relevant movement settings +// if (UCommonLegacyMovementSettings* MovementSettings = MoverComp->FindSharedSettings_Mutable()) +// { +// MovementSettings->Acceleration = StanceSettings->CrouchingMaxAcceleration; +// MovementSettings->MaxSpeed = StanceSettings->CrouchingMaxSpeed; +// } +// } +// +// break; +// +// // Prone isn't currently implemented properly so we're just doing nothing for now +// case EStanceMode::Prone: +// UE_LOG(LogMover, Warning, TEXT("Stance got into prone stance - That mode is not currently implemented fully.")); +// break; +// } +// } +// +// void FGMS_MovementStateModifier::RevertMovementSettings(UMoverComponent* MoverComp) +// { +// if (const UMoverComponent* CDOMoverComp = UMovementUtils::GetOriginalComponentType(MoverComp->GetOwner())) +// { +// const UCommonLegacyMovementSettings* OriginalMovementSettings = CDOMoverComp->FindSharedSettings(); +// UCommonLegacyMovementSettings* MovementSettings = MoverComp->FindSharedSettings_Mutable(); +// +// // Revert movement settings back to original settings +// if (MovementSettings && OriginalMovementSettings) +// { +// MovementSettings->Acceleration = OriginalMovementSettings->Acceleration; +// MovementSettings->MaxSpeed = OriginalMovementSettings->MaxSpeed; +// } +// } +// } diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Walking/GMS_WalkingMode.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Walking/GMS_WalkingMode.cpp new file mode 100644 index 0000000..04480e9 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Walking/GMS_WalkingMode.cpp @@ -0,0 +1,14 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/Walking/GMS_WalkingMode.h" + +#include "Mover/GMS_MoverSettingObjectLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_WalkingMode) + +UGMS_WalkingMode::UGMS_WalkingMode(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SharedSettingsClasses.Add(UGMS_MoverGroundedMovementSettings::StaticClass()); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineInterface.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineInterface.cpp new file mode 100644 index 0000000..0e29aaf --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineInterface.cpp @@ -0,0 +1,8 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/Zipline/GMS_ZiplineInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_ZiplineInterface) + +// Add default functionality here for any IGMS_ZiplineInterface functions that are not pure virtual. diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineModeTransition.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineModeTransition.cpp new file mode 100644 index 0000000..df2fab1 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZiplineModeTransition.cpp @@ -0,0 +1,93 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/Zipline/GMS_ZiplineModeTransition.h" +#include "GameFramework/Actor.h" +#include "DefaultMovementSet/CharacterMoverComponent.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Mover/GMS_MoverStructLibrary.h" +#include "Mover/Zipline/GMS_ZiplineInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_ZiplineModeTransition) + +// UGMS_ZiplineStartTransition ////////////////////////////// + +UGMS_ZiplineStartTransition::UGMS_ZiplineStartTransition(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +#if ENGINE_MINOR_VERSION >=6 +FTransitionEvalResult UGMS_ZiplineStartTransition::Evaluate_Implementation(const FSimulationTickParams& Params) const +#else +FTransitionEvalResult UGMS_ZiplineStartTransition::OnEvaluate(const FSimulationTickParams& Params) const +#endif +{ + FTransitionEvalResult EvalResult = FTransitionEvalResult::NoTransition; + + UCharacterMoverComponent* MoverComp = Cast(Params.MovingComps.MoverComponent.Get()); + + const FMoverSyncState& SyncState = Params.StartState.SyncState; + + if (MoverComp && MoverComp->IsAirborne() && SyncState.MovementMode != ZipliningModeName) + { + if (const FGMS_MoverTagInputs* AbilityInputs = Params.StartState.InputCmd.InputCollection.FindDataByType()) + { + if (ZipliningInputTag.IsValid() && AbilityInputs->Tags.HasTagExact(ZipliningInputTag)) + { + TArray OverlappingActors; + MoverComp->GetOwner()->GetOverlappingActors(OUT OverlappingActors); + + for (AActor* CandidateActor : OverlappingActors) + { + bool bIsZipline = UKismetSystemLibrary::DoesImplementInterface(CandidateActor, UGMS_ZiplineInterface::StaticClass()); + + if (bIsZipline) + { + EvalResult.NextMode = ZipliningModeName; + break; + } + } + } + } + } + + return EvalResult; +} + + +// UGMS_ZiplineEndTransition ////////////////////////////// + +UGMS_ZiplineEndTransition::UGMS_ZiplineEndTransition(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + + +#if ENGINE_MINOR_VERSION >=6 +FTransitionEvalResult UGMS_ZiplineEndTransition::Evaluate_Implementation(const FSimulationTickParams& Params) const +#else +FTransitionEvalResult UGMS_ZiplineEndTransition::OnEvaluate(const FSimulationTickParams& Params) const +#endif +{ + FTransitionEvalResult EvalResult = FTransitionEvalResult::NoTransition; + + if (const FCharacterDefaultInputs* DefaultInputs = Params.StartState.InputCmd.InputCollection.FindDataByType()) + { + if (DefaultInputs->bIsJumpJustPressed) + { + EvalResult.NextMode = AutoExitToMode; + } + } + + return EvalResult; +} + +#if ENGINE_MINOR_VERSION >=6 +void UGMS_ZiplineEndTransition::Trigger_Implementation(const FSimulationTickParams& Params) +#else +void UGMS_ZiplineEndTransition::OnTrigger(const FSimulationTickParams& Params) +#endif +{ + //TODO: create a small jump, using current directionality +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZipliningMode.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZipliningMode.cpp new file mode 100644 index 0000000..4284989 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Mover/Zipline/GMS_ZipliningMode.cpp @@ -0,0 +1,243 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Mover/Zipline/GMS_ZipliningMode.h" + +#include "MoverComponent.h" +#include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h" +#include "Kismet/KismetSystemLibrary.h" +#include "MoveLibrary/MovementUtils.h" +#include "Mover/Zipline/GMS_ZiplineInterface.h" +#include "Mover/Zipline/GMS_ZiplineModeTransition.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_ZipliningMode) + + +// FGMS_ZipliningState ////////////////////////////// + +FMoverDataStructBase* FGMS_ZipliningState::Clone() const +{ + FGMS_ZipliningState* CopyPtr = new FGMS_ZipliningState(*this); + return CopyPtr; +} + +bool FGMS_ZipliningState::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) +{ + Super::NetSerialize(Ar, Map, bOutSuccess); + + Ar << ZiplineActor; + Ar.SerializeBits(&bIsMovingAtoB, 1); + + bOutSuccess = true; + return true; +} + +void FGMS_ZipliningState::ToString(FAnsiStringBuilderBase& Out) const +{ + Super::ToString(Out); + + Out.Appendf("ZiplineActor: %s\n", *GetNameSafe(ZiplineActor)); + Out.Appendf("IsMovingAtoB: %d\n", bIsMovingAtoB); +} + +bool FGMS_ZipliningState::ShouldReconcile(const FMoverDataStructBase& AuthorityState) const +{ + const FGMS_ZipliningState* AuthorityZiplineState = static_cast(&AuthorityState); + + return (ZiplineActor != AuthorityZiplineState->ZiplineActor) || + (bIsMovingAtoB != AuthorityZiplineState->bIsMovingAtoB); +} + +void FGMS_ZipliningState::Interpolate(const FMoverDataStructBase& From, const FMoverDataStructBase& To, float Pct) +{ + const FGMS_ZipliningState* FromState = static_cast(&From); + const FGMS_ZipliningState* ToState = static_cast(&To); + + ZiplineActor = ToState->ZiplineActor; + bIsMovingAtoB = ToState->bIsMovingAtoB; +} + + +// UGMS_ZipliningMode ////////////////////////////// + +UGMS_ZipliningMode::UGMS_ZipliningMode(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + Transitions.Add(CreateDefaultSubobject(TEXT("ZiplineEndTransition"))); +} + +#if ENGINE_MINOR_VERSION >=6 +void UGMS_ZipliningMode::GenerateMove_Implementation(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const +#else +void UGMS_ZipliningMode::OnGenerateMove(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const +#endif +{ + UMoverComponent* MoverComp = GetMoverComponent(); + + // Ziplining is just following a path from A to B, so all movement is handled in OnSimulationTick + OutProposedMove = FProposedMove(); +} + + + +#if ENGINE_MINOR_VERSION >=6 +void UGMS_ZipliningMode::SimulationTick_Implementation(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) +#else +void UGMS_ZipliningMode::OnSimulationTick(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) +#endif +{ + // Are we continuing a move or starting fresh? + const FGMS_ZipliningState* StartingZipState = Params.StartState.SyncState.SyncStateCollection.FindDataByType(); + + FMoverDefaultSyncState& OutputSyncState = OutputState.SyncState.SyncStateCollection.FindOrAddMutableDataByType(); + FGMS_ZipliningState& OutZipState = OutputState.SyncState.SyncStateCollection.FindOrAddMutableDataByType(); + + USceneComponent* UpdatedComponent = Params.MovingComps.UpdatedComponent.Get(); + UMoverComponent* MoverComp = Params.MovingComps.MoverComponent.Get(); + AActor* MoverActor = MoverComp->GetOwner(); + + USceneComponent* StartPoint = nullptr; + USceneComponent* EndPoint = nullptr; + FVector ZipDirection; + FVector FlatFacingDir; + + const float DeltaSeconds = Params.TimeStep.StepMs * 0.001f; + + FVector ActorOrigin; + FVector BoxExtent; + MoverActor->GetActorBounds(true, OUT ActorOrigin, OUT BoxExtent); + const FVector ActorToZiplineOffset = MoverComp->GetUpDirection() * BoxExtent.Z; + + if (!StartingZipState) + { + // There is no existing zipline state... so let's find the target + // A) teleport to the closest starting point, set the zip direction + // B) choose the appropriate facing direction + // C) choose the appropriate initial velocity + TArray OverlappingActors; + MoverComp->GetOwner()->GetOverlappingActors(OUT OverlappingActors); + + for (AActor* CandidateActor : OverlappingActors) + { + bool bIsZipline = UKismetSystemLibrary::DoesImplementInterface(CandidateActor, UGMS_ZiplineInterface::StaticClass()); + + if (bIsZipline) + { + const FVector MoverLoc = UpdatedComponent->GetComponentLocation(); + USceneComponent* ZipPointA = IGMS_ZiplineInterface::Execute_GetStartComponent(CandidateActor); + USceneComponent* ZipPointB = IGMS_ZiplineInterface::Execute_GetEndComponent(CandidateActor); + + if (FVector::DistSquared(ZipPointA->GetComponentLocation(), MoverLoc) < FVector::DistSquared(ZipPointB->GetComponentLocation(), MoverLoc)) + { + OutZipState.bIsMovingAtoB = true; + StartPoint = ZipPointA; + EndPoint = ZipPointB; + } + else + { + OutZipState.bIsMovingAtoB = false; + StartPoint = ZipPointB; + EndPoint = ZipPointA; + } + + ZipDirection = (EndPoint->GetComponentLocation() - StartPoint->GetComponentLocation()).GetSafeNormal(); + + const FVector WarpLocation = StartPoint->GetComponentLocation() - ActorToZiplineOffset; + + FlatFacingDir = FVector::VectorPlaneProject(ZipDirection, MoverComp->GetUpDirection()).GetSafeNormal(); + + OutZipState.ZiplineActor = CandidateActor; + + UpdatedComponent->GetOwner()->TeleportTo(WarpLocation, FlatFacingDir.ToOrientationRotator()); + + break; + } + } + + // If we were unable to find a valid target zipline, refund all the time and let the actor fall + if (!StartPoint || !EndPoint) + { + FName DefaultAirMode = DefaultModeNames::Falling; + if (UCommonLegacyMovementSettings* LegacySettings = MoverComp->FindSharedSettings_Mutable()) + { + DefaultAirMode = LegacySettings->AirMovementModeName; + } + + OutputState.MovementEndState.NextModeName = DefaultModeNames::Falling; + OutputState.MovementEndState.RemainingMs = Params.TimeStep.StepMs; + return; + } + } + else + { + check(StartingZipState->ZiplineActor); + OutZipState = *StartingZipState; + + USceneComponent* ZipPointA = IGMS_ZiplineInterface::Execute_GetStartComponent(StartingZipState->ZiplineActor); + USceneComponent* ZipPointB = IGMS_ZiplineInterface::Execute_GetEndComponent(StartingZipState->ZiplineActor); + + if (StartingZipState->bIsMovingAtoB) + { + StartPoint = ZipPointA; + EndPoint = ZipPointB; + } + else + { + StartPoint = ZipPointB; + EndPoint = ZipPointA; + } + + ZipDirection = (EndPoint->GetComponentLocation() - StartPoint->GetComponentLocation()).GetSafeNormal(); + FlatFacingDir = FVector::VectorPlaneProject(ZipDirection, MoverComp->GetUpDirection()).GetSafeNormal(); + } + + + // Now let's slide along the zipline + const FVector StepStartPos = UpdatedComponent->GetComponentLocation() + ActorToZiplineOffset; + const FVector DesiredEndPos = StepStartPos + (ZipDirection * MaxSpeed * DeltaSeconds); // TODO: Make speed more dynamic + + FVector ActualEndPos = FMath::ClosestPointOnSegment(DesiredEndPos, + StartPoint->GetComponentLocation(), + EndPoint->GetComponentLocation()); + + bool bWillReachEndPosition = (ActualEndPos - EndPoint->GetComponentLocation()).IsNearlyZero(); + + FVector MoveDelta = ActualEndPos - StepStartPos; + + + FMovementRecord MoveRecord; + MoveRecord.SetDeltaSeconds(DeltaSeconds); + + + if (!MoveDelta.IsNearlyZero()) + { + FHitResult Hit(1.f); + + UMovementUtils::TrySafeMoveUpdatedComponent(Params.MovingComps, MoveDelta, FlatFacingDir.ToOrientationQuat(), true, Hit, ETeleportType::None, MoveRecord); + } + + + const FVector FinalLocation = UpdatedComponent->GetComponentLocation(); + const FVector FinalVelocity = MoveRecord.GetRelevantVelocity(); + + OutputSyncState.SetTransforms_WorldSpace(FinalLocation, + UpdatedComponent->GetComponentRotation(), + FinalVelocity, + nullptr); // no movement base + + UpdatedComponent->ComponentVelocity = FinalVelocity; + + + if (bWillReachEndPosition) + { + FName DefaultAirMode = DefaultModeNames::Falling; + if (UCommonLegacyMovementSettings* LegacySettings = MoverComp->FindSharedSettings_Mutable()) + { + DefaultAirMode = LegacySettings->AirMovementModeName; + } + + OutputState.MovementEndState.NextModeName = DefaultAirMode; + // TODO: If we reach the end position early, we should refund the remaining time + } +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_CurvesBlend.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_CurvesBlend.cpp new file mode 100644 index 0000000..21ee0c5 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_CurvesBlend.cpp @@ -0,0 +1,118 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Nodes/GMS_AnimNode_CurvesBlend.h" + +#include "Animation/AnimTrace.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimNode_CurvesBlend) + +void FGMS_AnimNode_CurvesBlend::Initialize_AnyThread(const FAnimationInitializeContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + + Super::Initialize_AnyThread(Context); + + SourcePose.Initialize(Context); + CurvesPose.Initialize(Context); +} + +void FGMS_AnimNode_CurvesBlend::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + + Super::CacheBones_AnyThread(Context); + + SourcePose.CacheBones(Context); + CurvesPose.CacheBones(Context); +} + +void FGMS_AnimNode_CurvesBlend::Update_AnyThread(const FAnimationUpdateContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + + Super::Update_AnyThread(Context); + + GetEvaluateGraphExposedInputs().Execute(Context); + + SourcePose.Update(Context); + + const auto CurrentBlendAmount{GetBlendAmount()}; + if (FAnimWeight::IsRelevant(CurrentBlendAmount)) + { + CurvesPose.Update(Context); + } + + TRACE_ANIM_NODE_VALUE(Context, TEXT("Blend Amount"), CurrentBlendAmount); + + TRACE_ANIM_NODE_VALUE(Context, TEXT("Blend Mode"), *StaticEnum()->GetNameStringByValue(static_cast(GetBlendMode()))); +} + +void FGMS_AnimNode_CurvesBlend::Evaluate_AnyThread(FPoseContext& Output) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() + ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(CurvesBlend, !IsInGameThread()); + + Super::Evaluate_AnyThread(Output); + + SourcePose.Evaluate(Output); + + const auto CurrentBlendAmount{GetBlendAmount()}; + if (!FAnimWeight::IsRelevant(CurrentBlendAmount)) + { + return; + } + + auto CurvesPoseContext{Output}; + CurvesPose.Evaluate(CurvesPoseContext); + + switch (GetBlendMode()) + { + case EGMS_CurvesBlendMode::BlendByAmount: + Output.Curve.Accumulate(CurvesPoseContext.Curve, CurrentBlendAmount); + break; + + case EGMS_CurvesBlendMode::Combine: + Output.Curve.Combine(CurvesPoseContext.Curve); + break; + + case EGMS_CurvesBlendMode::CombinePreserved: + Output.Curve.CombinePreserved(CurvesPoseContext.Curve); + break; + + case EGMS_CurvesBlendMode::UseMaxValue: + Output.Curve.UseMaxValue(CurvesPoseContext.Curve); + break; + + case EGMS_CurvesBlendMode::UseMinValue: + Output.Curve.UseMinValue(CurvesPoseContext.Curve); + break; + + case EGMS_CurvesBlendMode::Override: + Output.Curve.Override(CurvesPoseContext.Curve); + break; + } +} + +void FGMS_AnimNode_CurvesBlend::GatherDebugData(FNodeDebugData& DebugData) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData) + + TStringBuilder<256> DebugItemBuilder{InPlace, DebugData.GetNodeName(this), TEXTVIEW(": Blend Amount: ")}; + + DebugItemBuilder.Appendf(TEXT("%.2f"), GetBlendAmount()); + + DebugData.AddDebugItem(FString{DebugItemBuilder}); + SourcePose.GatherDebugData(DebugData.BranchFlow(1.0f)); + CurvesPose.GatherDebugData(DebugData.BranchFlow(GetBlendAmount())); +} + +float FGMS_AnimNode_CurvesBlend::GetBlendAmount() const +{ + return GET_ANIM_NODE_DATA(float, BlendAmount); +} + +EGMS_CurvesBlendMode FGMS_AnimNode_CurvesBlend::GetBlendMode() const +{ + return GET_ANIM_NODE_DATA(EGMS_CurvesBlendMode, BlendMode); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_GameplayTagsBlend.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_GameplayTagsBlend.cpp new file mode 100644 index 0000000..64e1d97 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_GameplayTagsBlend.cpp @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Nodes/GMS_AnimNode_GameplayTagsBlend.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimNode_GameplayTagsBlend) + +int32 FGMS_AnimNode_GameplayTagsBlend::GetActiveChildIndex() +{ + const auto& CurrentActiveTag{GetActiveTag()}; + + return CurrentActiveTag.IsValid() + ? GetTags().Find(CurrentActiveTag) + 1 + : 0; +} + +const FGameplayTag& FGMS_AnimNode_GameplayTagsBlend::GetActiveTag() const +{ + return GET_ANIM_NODE_DATA(FGameplayTag, ActiveTag); +} + +const TArray& FGMS_AnimNode_GameplayTagsBlend::GetTags() const +{ + return GET_ANIM_NODE_DATA(TArray, Tags); +} + +#if WITH_EDITOR +void FGMS_AnimNode_GameplayTagsBlend::RefreshPoses() +{ + const auto Difference{BlendPose.Num() - GetTags().Num() - 1}; + if (Difference == 0) + { + return; + } + + if (Difference > 0) + { + for (auto i{Difference}; i > 0; i--) + { + RemovePose(BlendPose.Num() - 1); + } + } + else + { + for (auto i{Difference}; i < 0; i++) + { + AddPose(); + } + } +} +#endif diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_LayeredBoneBlend.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_LayeredBoneBlend.cpp new file mode 100644 index 0000000..3c56bea --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_LayeredBoneBlend.cpp @@ -0,0 +1,296 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Nodes/GMS_AnimNode_LayeredBoneBlend.h" +#include "AnimationRuntime.h" +#include "Animation/AnimInstanceProxy.h" +#include "Animation/AnimTrace.h" +#include "Animation/AnimCurveTypes.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimNode_LayeredBoneBlend) + +///////////////////////////////////////////////////// +// FGMS_AnimNode_LayeredBoneBlend + +void FGMS_AnimNode_LayeredBoneBlend::Initialize_AnyThread(const FAnimationInitializeContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread) + FAnimNode_Base::Initialize_AnyThread(Context); + + const int NumPoses = BlendPoses.Num(); + checkSlow(BlendWeights.Num() == NumPoses); + + // initialize children + BasePose.Initialize(Context); + + if (NumPoses > 0) + { + for (int32 ChildIndex = 0; ChildIndex < NumPoses; ++ChildIndex) + { + BlendPoses[ChildIndex].Initialize(Context); + } + } +} + +void FGMS_AnimNode_LayeredBoneBlend::RebuildPerBoneBlendWeights(const USkeleton* InSkeleton) +{ + if (InSkeleton) + { + if (ExternalLayerSetup.BranchFilters.IsEmpty()) + { + FAnimationRuntime::CreateMaskWeights(PerBoneBlendWeights, LayerSetup, InSkeleton); + } + else + { + for (const FBranchFilter& BranchFilter : ExternalLayerSetup.BranchFilters) + { + LayerSetup[0].BranchFilters.Add(BranchFilter); + } + FAnimationRuntime::CreateMaskWeights(PerBoneBlendWeights, LayerSetup, InSkeleton); + } + + SkeletonGuid = InSkeleton->GetGuid(); + VirtualBoneGuid = InSkeleton->GetVirtualBoneGuid(); + } +} + +bool FGMS_AnimNode_LayeredBoneBlend::ArePerBoneBlendWeightsValid(const USkeleton* InSkeleton) const +{ + return (InSkeleton != nullptr && InSkeleton->GetGuid() == SkeletonGuid && InSkeleton->GetVirtualBoneGuid() == VirtualBoneGuid); +} + +void FGMS_AnimNode_LayeredBoneBlend::UpdateCachedBoneData(const FBoneContainer& RequiredBones, const USkeleton* Skeleton) +{ + if (LayerSetup.IsValidIndex(0) && LayerSetup[0].BranchFilters.IsEmpty()) + { + RebuildPerBoneBlendWeights(Skeleton); + } + + // if(RequiredBones.GetSerialNumber() == RequiredBonesSerialNumber) + // { + // return; + // } + + if (!ArePerBoneBlendWeightsValid(Skeleton)) + { + RebuildPerBoneBlendWeights(Skeleton); + } + + // build desired bone weights + const TArray& RequiredBoneIndices = RequiredBones.GetBoneIndicesArray(); + const int32 NumRequiredBones = RequiredBoneIndices.Num(); + DesiredBoneBlendWeights.SetNumZeroed(NumRequiredBones); + for (int32 RequiredBoneIndex=0; RequiredBoneIndexForEachCurveMetaData([this, &RequiredBones](const FName& InCurveName, const FCurveMetaData& InMetaData) + { + for (const FBoneReference& LinkedBone : InMetaData.LinkedBones) + { + FCompactPoseBoneIndex CompactPoseIndex = LinkedBone.GetCompactPoseIndex(RequiredBones); + if (CompactPoseIndex != INDEX_NONE) + { + if (DesiredBoneBlendWeights[CompactPoseIndex.GetInt()].BlendWeight > 0.f) + { + CurvePoseSourceIndices.Add(InCurveName, DesiredBoneBlendWeights[CompactPoseIndex.GetInt()].SourceIndex); + break; + } + } + } + }); + + // Shrink afterwards to exactly what was used if the Reserve increased, to save memory. Eventually the reserve will + // stabilize at the maximum number of nodes actually used in practice for this specific anim node. + if (CurvePoseSourceIndices.Num() > OriginalReserve) + { + CurvePoseSourceIndices.Shrink(); + } + } + + RequiredBonesSerialNumber = RequiredBones.GetSerialNumber(); + LayerSetup[0].BranchFilters.Reset(); +} + +void FGMS_AnimNode_LayeredBoneBlend::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(CacheBones_AnyThread) + BasePose.CacheBones(Context); + int32 NumPoses = BlendPoses.Num(); + for(int32 ChildIndex=0; ChildIndexGetRequiredBones(), Context.AnimInstanceProxy->GetSkeleton()); +} + +void FGMS_AnimNode_LayeredBoneBlend::Update_AnyThread(const FAnimationUpdateContext& Context) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread) + bHasRelevantPoses = false; + int32 RootMotionBlendPose = -1; + float RootMotionWeight = 0.f; + const float RootMotionClearWeight = bBlendRootMotionBasedOnRootBone ? 0.f : 1.f; + + if (IsLODEnabled(Context.AnimInstanceProxy)) + { + GetEvaluateGraphExposedInputs().Execute(Context); + + for (int32 ChildIndex = 0; ChildIndex < BlendPoses.Num(); ++ChildIndex) + { + const float ChildWeight = BlendWeights[ChildIndex]; + if (FAnimWeight::IsRelevant(ChildWeight)) + { + if (bHasRelevantPoses == false) + { + // Update cached data now we know we might be valid + UpdateCachedBoneData(Context.AnimInstanceProxy->GetRequiredBones(), Context.AnimInstanceProxy->GetSkeleton()); + + // Update weights + FAnimationRuntime::UpdateDesiredBoneWeight(DesiredBoneBlendWeights, CurrentBoneBlendWeights, BlendWeights); + bHasRelevantPoses = true; + + if(bBlendRootMotionBasedOnRootBone && !CurrentBoneBlendWeights.IsEmpty()) + { + const float NewRootMotionWeight = CurrentBoneBlendWeights[0].BlendWeight; + if(NewRootMotionWeight > ZERO_ANIMWEIGHT_THRESH) + { + RootMotionWeight = NewRootMotionWeight; + RootMotionBlendPose = CurrentBoneBlendWeights[0].SourceIndex; + } + } + } + + const float ThisPoseRootMotionWeight = (ChildIndex == RootMotionBlendPose) ? RootMotionWeight : RootMotionClearWeight; + BlendPoses[ChildIndex].Update(Context.FractionalWeightAndRootMotion(ChildWeight, ThisPoseRootMotionWeight)); + } + } + } + + // initialize children + const float BaseRootMotionWeight = 1.f - RootMotionWeight; + + if (BaseRootMotionWeight < ZERO_ANIMWEIGHT_THRESH) + { + BasePose.Update(Context.FractionalWeightAndRootMotion(1.f, BaseRootMotionWeight)); + } + else + { + BasePose.Update(Context); + } + + TRACE_ANIM_NODE_VALUE(Context, TEXT("Num Poses"), BlendPoses.Num()); +} + +void FGMS_AnimNode_LayeredBoneBlend::Evaluate_AnyThread(FPoseContext& Output) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread) + ANIM_MT_SCOPE_CYCLE_COUNTER(BlendPosesInGraph, !IsInGameThread()); + + const int NumPoses = BlendPoses.Num(); + if ((NumPoses == 0) || !bHasRelevantPoses) + { + BasePose.Evaluate(Output); + } + else + { + FPoseContext BasePoseContext(Output); + + // evaluate children + BasePose.Evaluate(BasePoseContext); + + TArray TargetBlendPoses; + TargetBlendPoses.SetNum(NumPoses); + + TArray TargetBlendCurves; + TargetBlendCurves.SetNum(NumPoses); + + TArray TargetBlendAttributes; + TargetBlendAttributes.SetNum(NumPoses); + + for (int32 ChildIndex = 0; ChildIndex < NumPoses; ++ChildIndex) + { + if (FAnimWeight::IsRelevant(BlendWeights[ChildIndex])) + { + FPoseContext CurrentPoseContext(Output); + BlendPoses[ChildIndex].Evaluate(CurrentPoseContext); + + TargetBlendPoses[ChildIndex].MoveBonesFrom(CurrentPoseContext.Pose); + TargetBlendCurves[ChildIndex].MoveFrom(CurrentPoseContext.Curve); + TargetBlendAttributes[ChildIndex].MoveFrom(CurrentPoseContext.CustomAttributes); + } + else + { + TargetBlendPoses[ChildIndex].ResetToRefPose(BasePoseContext.Pose.GetBoneContainer()); + TargetBlendCurves[ChildIndex].InitFrom(Output.Curve); + } + } + + // filter to make sure it only includes curves that are linked to the correct bone filter + UE::Anim::FNamedValueArrayUtils::RemoveByPredicate(BasePoseContext.Curve, CurvePoseSourceIndices, + [](const UE::Anim::FCurveElement& InOutBasePoseElement, const UE::Anim::FCurveElementIndexed& InSourceIndexElement) + { + // if source index is set, remove base pose curve value + return (InSourceIndexElement.Index != INDEX_NONE); + }); + + // Filter child pose curves + for (int32 ChildIndex = 0; ChildIndex < NumPoses; ++ChildIndex) + { + UE::Anim::FNamedValueArrayUtils::RemoveByPredicate(TargetBlendCurves[ChildIndex], CurvePoseSourceIndices, + [ChildIndex](const UE::Anim::FCurveElement& InOutBasePoseElement, const UE::Anim::FCurveElementIndexed& InSourceIndexElement) + { + // if not source, remove it + return (InSourceIndexElement.Index != INDEX_NONE) && (InSourceIndexElement.Index != ChildIndex); + }); + } + + FAnimationRuntime::EBlendPosesPerBoneFilterFlags BlendFlags = FAnimationRuntime::EBlendPosesPerBoneFilterFlags::None; + if (bMeshSpaceRotationBlend) + { + BlendFlags |= FAnimationRuntime::EBlendPosesPerBoneFilterFlags::MeshSpaceRotation; + } + if (bMeshSpaceScaleBlend) + { + BlendFlags |= FAnimationRuntime::EBlendPosesPerBoneFilterFlags::MeshSpaceScale; + } + + FAnimationPoseData AnimationPoseData(Output); + FAnimationRuntime::BlendPosesPerBoneFilter(BasePoseContext.Pose, TargetBlendPoses, BasePoseContext.Curve, TargetBlendCurves, BasePoseContext.CustomAttributes, TargetBlendAttributes, AnimationPoseData, CurrentBoneBlendWeights, BlendFlags, CurveBlendOption); + } +} + + +void FGMS_AnimNode_LayeredBoneBlend::GatherDebugData(FNodeDebugData& DebugData) +{ + DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData) + const int NumPoses = BlendPoses.Num(); + + FString DebugLine = DebugData.GetNodeName(this); + DebugLine += FString::Printf(TEXT("(Num Poses: %i)"), NumPoses); + DebugData.AddDebugItem(DebugLine); + + BasePose.GatherDebugData(DebugData.BranchFlow(1.f)); + + for (int32 ChildIndex = 0; ChildIndex < NumPoses; ++ChildIndex) + { + BlendPoses[ChildIndex].GatherDebugData(DebugData.BranchFlow(BlendWeights[ChildIndex])); + } +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_OrientationWarping.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_OrientationWarping.cpp new file mode 100644 index 0000000..3eb0d15 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Nodes/GMS_AnimNode_OrientationWarping.cpp @@ -0,0 +1,680 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Nodes/GMS_AnimNode_OrientationWarping.h" +#include "Animation/AnimInstanceProxy.h" +#include "Animation/AnimNodeFunctionRef.h" +#include "Animation/AnimRootMotionProvider.h" +#include "BoneControllers/AnimNode_OffsetRootBone.h" +#include "HAL/IConsoleManager.h" +#include "Animation/AnimTrace.h" +#include "Logging/LogVerbosity.h" +#include "VisualLogger/VisualLogger.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimNode_OrientationWarping) + +DECLARE_CYCLE_STAT(TEXT("OrientationWarping Eval"), STAT_OrientationWarping_Eval, STATGROUP_Anim); + +#if ENABLE_ANIM_DEBUG +static TAutoConsoleVariable CVarAnimNodeOrientationWarpingDebug(TEXT("a.AnimNode.GenericOrientationWarping.Debug"), 0, TEXT("Turn on visualization debugging for Orientation Warping.")); +static TAutoConsoleVariable CVarAnimNodeOrientationWarpingVerbose(TEXT("a.AnimNode.GenericOrientationWarping.Verbose"), 0, TEXT("Turn on verbose graph debugging for Orientation Warping")); +static TAutoConsoleVariable CVarAnimNodeOrientationWarpingEnable(TEXT("a.AnimNode.GenericOrientationWarping.Enable"), 1, TEXT("Toggle Orientation Warping")); +#endif + +namespace UE::Anim +{ + static inline FVector GetAxisVector(const EAxis::Type& InAxis) + { + switch (InAxis) + { + case EAxis::X: + return FVector::ForwardVector; + case EAxis::Y: + return FVector::RightVector; + default: + return FVector::UpVector; + }; + } + + static inline bool IsInvalidWarpingAngleDegrees(float Angle, float Tolerance) + { + Angle = FRotator::NormalizeAxis(Angle); + return FMath::IsNearlyZero(Angle, Tolerance) || FMath::IsNearlyEqual(FMath::Abs(Angle), 180.f, Tolerance); + } + + static float SignedAngleRadBetweenNormals(const FVector& From, const FVector& To, const FVector& Axis) + { + const float FromDotTo = FVector::DotProduct(From, To); + const float Angle = FMath::Acos(FromDotTo); + const FVector Cross = FVector::CrossProduct(From, To); + const float Dot = FVector::DotProduct(Cross, Axis); + return Dot >= 0 ? Angle : -Angle; + } +} + +void FGMS_AnimNode_OrientationWarping::GatherDebugData(FNodeDebugData& DebugData) +{ + FString DebugLine = DebugData.GetNodeName(this); +#if ENABLE_ANIM_DEBUG + if (CVarAnimNodeOrientationWarpingVerbose.GetValueOnAnyThread() == 1) + { + if (Mode == EWarpingEvaluationMode::Manual) + { + DebugLine += TEXT("\n - Evaluation Mode: (Manual)"); + DebugLine += FString::Printf(TEXT("\n - Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); + } + else + { + DebugLine += TEXT("\n - Evaluation Mode: (Graph)"); + DebugLine += FString::Printf(TEXT("\n - Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); + // Locomotion angle is already in degrees. + DebugLine += FString::Printf(TEXT("\n - Locomotion Angle: (%.3fd)"), LocomotionAngle); + DebugLine += FString::Printf(TEXT("\n - Locomotion Delta Angle Threshold: (%.3fd)"), LocomotionAngleDeltaThreshold); +#if WITH_EDITORONLY_DATA + DebugLine += FString::Printf(TEXT("\n - Root Motion Delta Attribute Found: %s)"), (bFoundRootMotionAttribute) ? TEXT("true") : TEXT("false")); +#endif + } + if (const UEnum* TypeEnum = FindObject(nullptr, TEXT("/Script/CoreUObject.EAxis"))) + { + DebugLine += FString::Printf(TEXT("\n - Rotation Axis: (%s)"), *(TypeEnum->GetNameStringByIndex(static_cast(RotationAxis)))); + } + DebugLine += FString::Printf(TEXT("\n - Rotation Interpolation Speed: (%.3fd)"), RotationInterpSpeed); + } + else +#endif + { + const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); + DebugLine += FString::Printf(TEXT("(Orientation Angle: %.3fd)"), ActualOrientationAngleDegrees); + } + DebugData.AddDebugItem(DebugLine); + ComponentPose.GatherDebugData(DebugData); +} + +void FGMS_AnimNode_OrientationWarping::Initialize_AnyThread(const FAnimationInitializeContext& Context) +{ + FAnimNode_SkeletalControlBase::Initialize_AnyThread(Context); + + Reset(Context); + + //早一点拿到ExternalBoneReference. + if (IsLODEnabled(Context.AnimInstanceProxy)) + { + GetEvaluateGraphExposedInputs().Execute(Context); + } +} + +void FGMS_AnimNode_OrientationWarping::UpdateInternal(const FAnimationUpdateContext& Context) +{ + FAnimNode_SkeletalControlBase::UpdateInternal(Context); + + // If we just became relevant and haven't been initialized yet, then reset. + if (!bIsFirstUpdate && UpdateCounter.HasEverBeenUpdated() && !UpdateCounter.WasSynchronizedCounter(Context.AnimInstanceProxy->GetUpdateCounter())) + { + Reset(Context); + } + UpdateCounter.SynchronizeWith(Context.AnimInstanceProxy->GetUpdateCounter()); + BlendWeight = Context.GetFinalBlendWeight(); + + // if (WarpingSpace == EOrientationWarpingSpace::RootBoneTransform) + // { + // if (UE::AnimationWarping::FRootOffsetProvider* RootOffsetProvider = Context.GetMessage()) + // { + // WarpingSpaceTransform = RootOffsetProvider->GetRootTransform(); + // } + // else + // { + // WarpingSpaceTransform = Context.AnimInstanceProxy->GetComponentTransform(); + // } + // } + if (WarpingSpace == EOrientationWarpingSpace::ComponentTransform) + { + WarpingSpaceTransform = Context.AnimInstanceProxy->GetComponentTransform(); + } +} + +void FGMS_AnimNode_OrientationWarping::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray& OutBoneTransforms) +{ + SCOPE_CYCLE_COUNTER(STAT_OrientationWarping_Eval); + check(OutBoneTransforms.Num() == 0); + + float TargetOrientationAngleRad; + + const float DeltaSeconds = Output.AnimInstanceProxy->GetDeltaSeconds(); + const float MaxAngleCorrectionRad = FMath::DegreesToRadians(MaxCorrectionDegrees); + const FVector RotationAxisVector = UE::Anim::GetAxisVector(RotationAxis); + FVector LocomotionForward = FVector::ZeroVector; + + bool bGraphDrivenWarping = false; + const UE::Anim::IAnimRootMotionProvider* RootMotionProvider = UE::Anim::IAnimRootMotionProvider::Get(); + + if (Mode == EWarpingEvaluationMode::Graph) + { + bGraphDrivenWarping = !!RootMotionProvider; + ensureMsgf(bGraphDrivenWarping, TEXT("Graph driven Orientation Warping expected a valid root motion delta provider interface.")); + } + +#if WITH_EDITORONLY_DATA + bFoundRootMotionAttribute = false; +#endif + +#if ENABLE_ANIM_DEBUG + FTransform RootMotionTransformDelta = FTransform::Identity; + float RootMotionDeltaAngleRad = 0.0; + const float PreviousOrientationAngleRad = ActualOrientationAngleRad; +#endif + + // We will likely need to revisit LocomotionAngle participating as an input to orientation warping. + // Without velocity information from the motion model (such as the capsule), LocomotionAngle isn't enough + // information in isolation for all cases when deciding to warp. + // + // For example imagine that the motion model has stopped moving with zero velocity due to a + // transition into a strafing stop. During that transition we may play an animation with non-zero + // velocity for an arbitrary number of frames. In this scenario the concept of direction is meaningless + // since we cannot orient the animation to match a zero velocity and consequently a zero direction, + // since that would break the pose. For those frames, we would incorrectly over-orient the strafe. + // + // The solution may be instead to pass velocity with the actor base rotation, allowing us to retain + // speed information about the motion. It may also allow us to do more complex orienting behavior + // when multiple degrees of freedom can be considered. + + if (WarpingSpace == EOrientationWarpingSpace::ComponentTransform) + { + WarpingSpaceTransform = Output.AnimInstanceProxy->GetComponentTransform(); + } + + if (bGraphDrivenWarping) + { +#if !ENABLE_ANIM_DEBUG + FTransform RootMotionTransformDelta = FTransform::Identity; +#endif + + bGraphDrivenWarping = RootMotionProvider->ExtractRootMotion(Output.CustomAttributes, RootMotionTransformDelta); + + // Graph driven orientation warping will modify the incoming root motion to orient towards the intended locomotion angle + if (bGraphDrivenWarping) + { +#if WITH_EDITORONLY_DATA + // Graph driven Orientation Warping expects a root motion delta to be present in the attribute stream. + bFoundRootMotionAttribute = true; +#endif + + // In UE, forward is defined as +x; consequently this is also true when sampling an actor's velocity. Historically the skeletal + // mesh component forward will not match the actor, requiring us to correct the rotation before sampling the LocomotionForward. + // In order to make orientation warping 'pure' in the future we will need to provide more context about the intent of + // the actor vs the intent of the animation in their respective spaces. Specifically, we will need some form the following information: + // + // 1. Actor Forward + // 2. Actor Velocity + // 3. Skeletal Mesh Relative Rotation + + if (LocomotionDirection.SquaredLength() > UE_SMALL_NUMBER) + { + // if we have a LocomotionDirection vector, transform into root bone local space + LocomotionForward = WarpingSpaceTransform.InverseTransformVector(LocomotionDirection); + LocomotionForward.Normalize(); + } + else + { + LocomotionAngle = FRotator::NormalizeAxis(LocomotionAngle); + // UE-184297 Avoid storing LocomotionAngle in radians in case haven't updated the pinned input, to avoid a DegToRad(RadianValue) + const float LocomotionAngleRadians = FMath::DegreesToRadians(LocomotionAngle); + const FQuat LocomotionRotation = FQuat(RotationAxisVector, LocomotionAngleRadians); + const FTransform SkeletalMeshRelativeTransform = Output.AnimInstanceProxy->GetComponentRelativeTransform(); + const FQuat SkeletalMeshRelativeRotation = SkeletalMeshRelativeTransform.GetRotation(); + LocomotionForward = SkeletalMeshRelativeRotation.UnrotateVector(LocomotionRotation.GetForwardVector()).GetSafeNormal(); + } + + // Flatten locomotion direction, along the rotation axis. + LocomotionForward = (LocomotionForward - RotationAxisVector.Dot(LocomotionForward) * RotationAxisVector).GetSafeNormal(); + + // @todo: Graph mode using a "manual value" makes no sense. Restructure logic to address this in the future. + if (bUseManualRootMotionVelocity) + { + RootMotionTransformDelta.SetTranslation(ManualRootMotionVelocity * DeltaSeconds); + } + + FVector RootMotionDeltaTranslation = RootMotionTransformDelta.GetTranslation(); + + // Flatten root motion translation, along the rotation axis. + RootMotionDeltaTranslation = RootMotionDeltaTranslation - RotationAxisVector.Dot(RootMotionDeltaTranslation) * RotationAxisVector; + + const float RootMotionDeltaSpeed = RootMotionDeltaTranslation.Size() / DeltaSeconds; + if (RootMotionDeltaSpeed < MinRootMotionSpeedThreshold) + { + // If we're under the threshold, snap orientation angle to 0, and let interpolation handle the delta + TargetOrientationAngleRad = 0.0f; + } + else + { + + const FVector PreviousRootMotionDeltaDirection = RootMotionDeltaDirection; + // Hold previous direction if we can't calculate it from current move delta, because the root is no longer moving + RootMotionDeltaDirection = RootMotionDeltaTranslation.GetSafeNormal(UE_SMALL_NUMBER, PreviousRootMotionDeltaDirection); + TargetOrientationAngleRad = UE::Anim::SignedAngleRadBetweenNormals(RootMotionDeltaDirection, LocomotionForward, RotationAxisVector); + + // Motion Matching may return an animation that deviates a lot from the movement direction (e.g movement direction going bwd and motion matching could return the fwd animation for a few frames) + // When that happens, since we use the delta between root motion and movement direction, we would be over-rotating the lower body and breaking the pose during those frames + // So, when that happens we use the inverse of the root motion direction to calculate our target rotation. + // This feels a bit 'hacky' but its the only option I've found so far to mitigate the problem + if (LocomotionAngleDeltaThreshold > 0.f) + { + if (FMath::Abs(FMath::RadiansToDegrees(TargetOrientationAngleRad)) > LocomotionAngleDeltaThreshold) + { + TargetOrientationAngleRad = FMath::UnwindRadians(TargetOrientationAngleRad + FMath::DegreesToRadians(180.0f)); + RootMotionDeltaDirection = -RootMotionDeltaDirection; + } + } + + // Don't compensate interpolation by the root motion angle delta if the previous angle isn't valid. + if (bCounterCompenstateInterpolationByRootMotion && !PreviousRootMotionDeltaDirection.IsNearlyZero(UE_SMALL_NUMBER)) + { +#if !ENABLE_ANIM_DEBUG + float RootMotionDeltaAngleRad; +#endif + // Counter the interpolated orientation angle by the root motion direction angle delta. + // This prevents our interpolation from fighting the natural root motion that's flowing through the graph. + RootMotionDeltaAngleRad = UE::Anim::SignedAngleRadBetweenNormals(RootMotionDeltaDirection, PreviousRootMotionDeltaDirection, RotationAxisVector); + // Root motion may have large deltas i.e. bad blends or sudden direction changes like pivots. + // If there's an instantaneous pop in root motion direction, this is likely a pivot. + const float MaxRootMotionDeltaToCompensateRad = FMath::DegreesToRadians(MaxRootMotionDeltaToCompensateDegrees); + if (FMath::Abs(RootMotionDeltaAngleRad) < MaxRootMotionDeltaToCompensateRad) + { + ActualOrientationAngleRad = FMath::UnwindRadians(ActualOrientationAngleRad + RootMotionDeltaAngleRad); + } + } + + // Rotate the root motion delta fully by the warped angle + const FVector WarpedRootMotionTranslationDelta = FQuat(RotationAxisVector, TargetOrientationAngleRad).RotateVector(RootMotionDeltaTranslation); + RootMotionTransformDelta.SetTranslation(WarpedRootMotionTranslationDelta); + } + + // Forward the side effects of orientation warping on the root motion contribution for this sub-graph + const bool bRootMotionOverridden = RootMotionProvider->OverrideRootMotion(RootMotionTransformDelta, Output.CustomAttributes); + ensureMsgf(bRootMotionOverridden, TEXT("Graph driven Orientation Warping expected a root motion delta to be present in the attribute stream prior to warping/overriding it.")); + } + else + { + // Early exit on missing root motion delta attribute + return; + } + } + else + { + // Manual orientation warping will take the angle directly + TargetOrientationAngleRad = FRotator::NormalizeAxis(OrientationAngle); + TargetOrientationAngleRad = FMath::DegreesToRadians(TargetOrientationAngleRad); + } + + // Optionally interpolate the effective orientation towards the target orientation angle + // When the orientation warping node becomes relevant, the input pose orientation may not be aligned with the desired orientation. + // Instead of interpolating this difference, snap to the desired orientation if it's our first update to minimize corrections over-time. + if ((RotationInterpSpeed > 0.f) && !bIsFirstUpdate) + { + const float SmoothOrientationAngleRad = FMath::FInterpTo(ActualOrientationAngleRad, TargetOrientationAngleRad, DeltaSeconds, RotationInterpSpeed); + // Limit our interpolation rate to prevent pops. + // @TODO: Use better, more physically accurate interpolation here. + ActualOrientationAngleRad = FMath::Clamp(SmoothOrientationAngleRad, ActualOrientationAngleRad - MaxAngleCorrectionRad, ActualOrientationAngleRad + MaxAngleCorrectionRad); + } + else + { + ActualOrientationAngleRad = TargetOrientationAngleRad; + } + + ActualOrientationAngleRad = FMath::Clamp(ActualOrientationAngleRad, -MaxAngleCorrectionRad, MaxAngleCorrectionRad); + // Allow the alpha value of the node to affect the final rotation + ActualOrientationAngleRad *= ActualAlpha; + + if (bScaleByGlobalBlendWeight) + { + ActualOrientationAngleRad *= BlendWeight; + } + +#if ENABLE_ANIM_DEBUG + bool bDebugging = false; +#if WITH_EDITORONLY_DATA + bDebugging = bDebugging || bEnableDebugDraw; +#else + constexpr float DebugDrawScale = 1.f; +#endif + const int32 DebugIndex = CVarAnimNodeOrientationWarpingDebug.GetValueOnAnyThread(); + bDebugging = bDebugging || (DebugIndex > 0); + + if (bDebugging) + { + const FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform(); + const FVector ActorForwardDirection = Output.AnimInstanceProxy->GetActorTransform().GetRotation().GetForwardVector(); + FVector DebugArrowOffset = FVector::ZAxisVector * DebugDrawScale; + + // Draw debug shapes + { + const FVector ForwardDirection = bGraphDrivenWarping + ? ComponentTransform.GetRotation().RotateVector(LocomotionForward) + : ActorForwardDirection; + + Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + ForwardDirection * 100.f * DebugDrawScale, + 40.f * DebugDrawScale, FColor::Red, false, 0.f, 2.f * DebugDrawScale); + + const FVector RotationDirection = bGraphDrivenWarping + ? ComponentTransform.GetRotation().RotateVector(RootMotionDeltaDirection) + : ActorForwardDirection.RotateAngleAxis(OrientationAngle, RotationAxisVector); + + DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; + Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + RotationDirection * 100.f * DebugDrawScale, + 40.f * DebugDrawScale, FColor::Blue, false, 0.f, 2.f * DebugDrawScale); + + const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); + const FVector WarpedRotationDirection = bGraphDrivenWarping + ? RotationDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector) + : ActorForwardDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector); + + DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; + Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + WarpedRotationDirection * 100.f * DebugDrawScale, + 40.f * DebugDrawScale, FColor::Green, false, 0.f, 2.f * DebugDrawScale); + } + + // Draw text on mesh in world space + { + TStringBuilder<1024> DebugLine; + + const float PreviousOrientationAngleDegrees = FMath::RadiansToDegrees(PreviousOrientationAngleRad); + const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); + const float TargetOrientationAngleDegrees = FMath::RadiansToDegrees(TargetOrientationAngleRad); + if (Mode == EWarpingEvaluationMode::Manual) + { + DebugLine.Appendf(TEXT("\n - Previous Orientation Angle: (%.3fd)"), PreviousOrientationAngleDegrees); + DebugLine.Appendf(TEXT("\n - Orientation Angle: (%.3fd)"), ActualOrientationAngleDegrees); + DebugLine.Appendf(TEXT("\n - Target Orientation Angle: (%.3fd)"), TargetOrientationAngleRad); + } + else + { + if (RotationInterpSpeed > 0.0f) + { + DebugLine.Appendf(TEXT("\n - Previous Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(PreviousOrientationAngleRad)); + DebugLine.Appendf(TEXT("\n - Root Motion Frame Delta Angle: (%.3fd)"), FMath::RadiansToDegrees(RootMotionDeltaAngleRad)); + } + DebugLine.Appendf(TEXT("\n - Actual Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); + DebugLine.Appendf(TEXT("\n - Target Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(TargetOrientationAngleRad)); + // Locomotion angle is already in degrees. + DebugLine.Appendf(TEXT("\n - Locomotion Angle: (%.3fd)"), LocomotionAngle); + DebugLine.Appendf(TEXT("\n - Root Motion Delta: %s)"), *RootMotionTransformDelta.GetTranslation().ToString()); + DebugLine.Appendf(TEXT("\n - Root Motion Speed: %.3fd)"), RootMotionTransformDelta.GetTranslation().Size() / DeltaSeconds); + } + Output.AnimInstanceProxy->AnimDrawDebugInWorldMessage(DebugLine.ToString(), FVector::UpVector * 50.0f, FColor::Yellow, 1.f /*TextScale*/); + } + } +#endif + +#if ANIM_TRACE_ENABLED + { + const float PreviousOrientationAngleDegrees = FMath::RadiansToDegrees(PreviousOrientationAngleRad); + const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); + const float TargetOrientationAngleDegrees = FMath::RadiansToDegrees(TargetOrientationAngleRad); + + TRACE_ANIM_NODE_VALUE(Output, TEXT("Previous OrientationAngle Degrees"), PreviousOrientationAngleDegrees); + TRACE_ANIM_NODE_VALUE(Output, TEXT("Actual Orientation Angle Degrees"), ActualOrientationAngleDegrees); + TRACE_ANIM_NODE_VALUE(Output, TEXT("Target Orientation Angle Degrees"), TargetOrientationAngleDegrees); + + if (Mode == EWarpingEvaluationMode::Graph) + { + const float RootMotionDeltaAngleDegrees = FMath::RadiansToDegrees(RootMotionDeltaAngleRad); + TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Delta Angle Degrees"), RootMotionDeltaAngleDegrees); + TRACE_ANIM_NODE_VALUE(Output, TEXT("Locomotion Angle"), LocomotionAngle); + TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Translation Delta"), RootMotionTransformDelta.GetTranslation()); + + const float RootMotionSpeed = RootMotionTransformDelta.GetTranslation().Size() / DeltaSeconds; + TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Speed"), RootMotionSpeed); + } + } +#endif + + +#if ENABLE_VISUAL_LOG + if (FVisualLogger::IsRecording()) + { + const FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform(); + const FVector ActorForwardDirection = Output.AnimInstanceProxy->GetActorTransform().GetRotation().GetForwardVector(); + FVector DebugArrowOffset = FVector::ZAxisVector * DebugDrawScale; + + // Draw debug shapes + { + const FVector ForwardDirection = bGraphDrivenWarping + ? ComponentTransform.GetRotation().RotateVector(LocomotionForward) + : ActorForwardDirection; + + UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + ForwardDirection * 100.f * DebugDrawScale, + FColor::Red, TEXT("")); + + const FVector RotationDirection = bGraphDrivenWarping + ? ComponentTransform.GetRotation().RotateVector(RootMotionDeltaDirection) + : ActorForwardDirection.RotateAngleAxis(OrientationAngle, RotationAxisVector); + + DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; + UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + RotationDirection * 100.f * DebugDrawScale, + FColor::Blue, TEXT("")); + + const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); + const FVector WarpedRotationDirection = bGraphDrivenWarping + ? RotationDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector) + : ActorForwardDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector); + + DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; + + UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, + ComponentTransform.GetLocation() + DebugArrowOffset, + ComponentTransform.GetLocation() + DebugArrowOffset + WarpedRotationDirection * 100.f * DebugDrawScale, + FColor::Green, TEXT("")); + } + } +#endif + + const float RootOffset = FMath::UnwindRadians(ActualOrientationAngleRad * DistributedBoneOrientationAlpha); + + // Rotate Root Bone first, as that cheaply rotates the whole pose with one transformation. + if (!FMath::IsNearlyZero(RootOffset, KINDA_SMALL_NUMBER)) + { + const FQuat RootRotation = FQuat(RotationAxisVector, RootOffset); + const FCompactPoseBoneIndex RootBoneIndex(0); + + FTransform RootBoneTransform(Output.Pose.GetComponentSpaceTransform(RootBoneIndex)); + RootBoneTransform.SetRotation(RootRotation * RootBoneTransform.GetRotation()); + RootBoneTransform.NormalizeRotation(); + Output.Pose.SetComponentSpaceTransform(RootBoneIndex, RootBoneTransform); + } + + const int32 NumSpineBones = SpineBoneDataArray.Num(); + const bool bSpineOrientationAlpha = !FMath::IsNearlyZero(DistributedBoneOrientationAlpha, KINDA_SMALL_NUMBER); + const bool bUpdateSpineBones = (NumSpineBones > 0) && bSpineOrientationAlpha; + + if (bUpdateSpineBones) + { + // Spine bones counter rotate body orientation evenly across all bones. + for (int32 ArrayIndex = 0; ArrayIndex < NumSpineBones; ArrayIndex++) + { + const FOrientationWarpingSpineBoneData& BoneData = SpineBoneDataArray[ArrayIndex]; + const FQuat SpineBoneCounterRotation = FQuat(RotationAxisVector, -ActualOrientationAngleRad * DistributedBoneOrientationAlpha * BoneData.Weight); + check(BoneData.Weight > 0.f); + + FTransform SpineBoneTransform(Output.Pose.GetComponentSpaceTransform(BoneData.BoneIndex)); + SpineBoneTransform.SetRotation((SpineBoneCounterRotation * SpineBoneTransform.GetRotation())); + SpineBoneTransform.NormalizeRotation(); + Output.Pose.SetComponentSpaceTransform(BoneData.BoneIndex, SpineBoneTransform); + } + } + + const float IKFootRootOrientationAlpha = 1.f - DistributedBoneOrientationAlpha; + const bool bUpdateIKFootRoot = (IKFootData.IKFootRootBoneIndex != FCompactPoseBoneIndex(INDEX_NONE)) && !FMath::IsNearlyZero(IKFootRootOrientationAlpha, KINDA_SMALL_NUMBER); + + // Rotate IK Foot Root + if (bUpdateIKFootRoot) + { + const FQuat BoneRotation = FQuat(RotationAxisVector, ActualOrientationAngleRad * IKFootRootOrientationAlpha); + + FTransform IKFootRootTransform(Output.Pose.GetComponentSpaceTransform(IKFootData.IKFootRootBoneIndex)); + IKFootRootTransform.SetRotation(BoneRotation * IKFootRootTransform.GetRotation()); + IKFootRootTransform.NormalizeRotation(); + Output.Pose.SetComponentSpaceTransform(IKFootData.IKFootRootBoneIndex, IKFootRootTransform); + + // IK Feet + // These match the root orientation, so don't rotate them. Just preserve root rotation. + // We need to update their translation though, since we rotated their parent (the IK Foot Root bone). + const int32 NumIKFootBones = IKFootData.IKFootBoneIndexArray.Num(); + const bool bUpdateIKFootBones = bUpdateIKFootRoot && (NumIKFootBones > 0); + + if (bUpdateIKFootBones) + { + const FQuat IKFootRotation = FQuat(RotationAxisVector, -ActualOrientationAngleRad * IKFootRootOrientationAlpha); + + for (int32 ArrayIndex = 0; ArrayIndex < NumIKFootBones; ArrayIndex++) + { + const FCompactPoseBoneIndex& IKFootBoneIndex = IKFootData.IKFootBoneIndexArray[ArrayIndex]; + + FTransform IKFootBoneTransform(Output.Pose.GetComponentSpaceTransform(IKFootBoneIndex)); + IKFootBoneTransform.SetRotation(IKFootRotation * IKFootBoneTransform.GetRotation()); + IKFootBoneTransform.NormalizeRotation(); + Output.Pose.SetComponentSpaceTransform(IKFootBoneIndex, IKFootBoneTransform); + } + } + } + + OutBoneTransforms.Sort(FCompareBoneTransformIndex()); + bIsFirstUpdate = false; +} + +bool FGMS_AnimNode_OrientationWarping::IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) +{ +#if ENABLE_ANIM_DEBUG + if (CVarAnimNodeOrientationWarpingEnable.GetValueOnAnyThread() == 0) + { + return false; + } +#endif + if (RotationAxis == EAxis::None) + { + return false; + } + + if (Mode == EWarpingEvaluationMode::Manual && UE::Anim::IsInvalidWarpingAngleDegrees(OrientationAngle, KINDA_SMALL_NUMBER)) + { + return false; + } + + if (SpineBoneDataArray.IsEmpty()) + { + return false; + } + else + { + for (const auto& Spine : SpineBoneDataArray) + { + if (Spine.BoneIndex == INDEX_NONE) + { + return false; + } + } + } + + if (IKFootData.IKFootRootBoneIndex == INDEX_NONE) + { + return false; + } + + if (IKFootData.IKFootBoneIndexArray.IsEmpty()) + { + return false; + } + else + { + for (const auto& IKFootBoneIndex : IKFootData.IKFootBoneIndexArray) + { + if (IKFootBoneIndex == INDEX_NONE) + { + return false; + } + } + } + return true; +} + +void FGMS_AnimNode_OrientationWarping::InitializeBoneReferences(const FBoneContainer& RequiredBones) +{ + ExternalBoneReference.IKFootRootBone.Initialize(RequiredBones); + IKFootData.IKFootRootBoneIndex = ExternalBoneReference.IKFootRootBone.GetCompactPoseIndex(RequiredBones); + + IKFootData.IKFootBoneIndexArray.Reset(); + for (auto& Bone : ExternalBoneReference.IKFootBones) + { + Bone.Initialize(RequiredBones); + IKFootData.IKFootBoneIndexArray.Add(Bone.GetCompactPoseIndex(RequiredBones)); + } + + SpineBoneDataArray.Reset(); + for (auto& Bone : ExternalBoneReference.SpineBones) + { + Bone.Initialize(RequiredBones); + SpineBoneDataArray.Add(FOrientationWarpingSpineBoneData(Bone.GetCompactPoseIndex(RequiredBones))); + } + + if (SpineBoneDataArray.Num() > 0) + { + // Sort bones indices so we can transform parent before child + SpineBoneDataArray.Sort(FOrientationWarpingSpineBoneData::FCompareBoneIndex()); + + // Assign Weights. + TArray> IndicesToUpdate; + + for (int32 Index = SpineBoneDataArray.Num() - 1; Index >= 0; Index--) + { + // If this bone's weight hasn't been updated, scan its parents. + // If parents have weight, we add it to 'ExistingWeight'. + // split (1.f - 'ExistingWeight') between all members of the chain that have no weight yet. + if (SpineBoneDataArray[Index].Weight == 0.f) + { + IndicesToUpdate.Reset(SpineBoneDataArray.Num()); + float ExistingWeight = 0.f; + IndicesToUpdate.Add(Index); + + const FCompactPoseBoneIndex CompactBoneIndex = SpineBoneDataArray[Index].BoneIndex; + for (int32 ParentIndex = Index - 1; ParentIndex >= 0; ParentIndex--) + { + if (RequiredBones.BoneIsChildOf(CompactBoneIndex, SpineBoneDataArray[ParentIndex].BoneIndex)) + { + if (SpineBoneDataArray[ParentIndex].Weight > 0.f) + { + ExistingWeight += SpineBoneDataArray[ParentIndex].Weight; + } + else + { + IndicesToUpdate.Add(ParentIndex); + } + } + } + + check(IndicesToUpdate.Num() > 0); + const float WeightToShare = 1.f - ExistingWeight; + const float IndividualWeight = WeightToShare / float(IndicesToUpdate.Num()); + + for (int32 UpdateListIndex = 0; UpdateListIndex < IndicesToUpdate.Num(); UpdateListIndex++) + { + SpineBoneDataArray[IndicesToUpdate[UpdateListIndex]].Weight = IndividualWeight; + } + } + } + } +} + +void FGMS_AnimNode_OrientationWarping::Reset(const FAnimationBaseContext& Context) +{ + bIsFirstUpdate = true; + RootMotionDeltaDirection = FVector::ZeroVector; + ManualRootMotionVelocity = FVector::ZeroVector; + ActualOrientationAngleRad = 0.f; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingEnumLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingEnumLibrary.cpp new file mode 100644 index 0000000..abaae84 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingEnumLibrary.cpp @@ -0,0 +1,6 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Settings/GMS_SettingEnumLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_SettingEnumLibrary) \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingObjectLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingObjectLibrary.cpp new file mode 100644 index 0000000..2a83840 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingObjectLibrary.cpp @@ -0,0 +1,277 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Settings/GMS_SettingObjectLibrary.h" +#include "Locomotions/GMS_AnimLayer.h" +#include "Animation/BlendSpace.h" +#include "Locomotions/GMS_AnimLayer_Additive.h" +#include "Locomotions/GMS_AnimLayer_Overlay.h" +#include "Locomotions/GMS_AnimLayer_SkeletalControls.h" +#include "Locomotions/GMS_AnimLayer_States.h" +#include "Locomotions/GMS_AnimLayer_View_Default.h" +#include "Misc/DataValidation.h" +#include "Utility/GMS_Utility.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_SettingObjectLibrary) + +#pragma region CommonSettings + +#if WITH_EDITOR +#include "UObject/ObjectSaveContext.h" + +void UGMS_MovementDefinition::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); +} + +EDataValidationResult UGMS_MovementDefinition::IsDataValid(class FDataValidationContext& Context) const +{ + for (const TTuple& Pair : MovementSets) + { + if (Pair.Value.ControlSetting == nullptr) + { + Context.AddError(FText::FromString(FString::Format(TEXT("ControlSetting is required on {0}!!!"), {Pair.Key.GetTagName().ToString()}))); + } + + if (Pair.Value.bUseInstancedStatesSetting && Pair.Value.AnimLayerSetting_States && Pair.Value.AnimLayerSetting_States->IsDataValid(Context) == EDataValidationResult::Invalid) + { + return EDataValidationResult::Invalid; + } + + if (Pair.Value.bUseInstancedOverlaySetting && Pair.Value.AnimLayerSetting_Overlay && Pair.Value.AnimLayerSetting_Overlay->IsDataValid(Context) == EDataValidationResult::Invalid) + { + return EDataValidationResult::Invalid; + } + + if (Pair.Value.AnimLayerSetting_Additive && Pair.Value.AnimLayerSetting_Additive->IsDataValid(Context) == EDataValidationResult::Invalid) + { + return EDataValidationResult::Invalid; + } + + if (Pair.Value.AnimLayerSetting_View && Pair.Value.AnimLayerSetting_View->IsDataValid(Context) == EDataValidationResult::Invalid) + { + return EDataValidationResult::Invalid; + } + + if (Pair.Value.AnimLayerSetting_SkeletalControls && Pair.Value.AnimLayerSetting_SkeletalControls->IsDataValid(Context) == EDataValidationResult::Invalid) + { + return EDataValidationResult::Invalid; + } + } + return Super::IsDataValid(Context); +} + +#endif + + +FGameplayTag UGMS_MovementControlSetting_Default::MatchStateTagBySpeed(float Speed, float Threshold) const +{ + for (const FGMS_MovementStateSetting& MovementState : MovementStates) + { + if (MovementState.Speed > 0.0f && MovementState.Speed < Speed + Threshold) + { + return MovementState.Tag; + } + } + return FGameplayTag::EmptyTag; +} + +bool UGMS_MovementControlSetting_Default::GetStateByIndex(const int32& Index, FGMS_MovementStateSetting& OutSetting) const +{ + if (MovementStates.IsValidIndex(Index)) + { + OutSetting = MovementStates[Index]; + return true; + } + return false; +} + +bool UGMS_MovementControlSetting_Default::GetStateBySpeedLevel(const int32& Level, FGMS_MovementStateSetting& OutSetting) const +{ + if (SpeedLevelToArrayIndex.Contains(Level)) + { + OutSetting = MovementStates[SpeedLevelToArrayIndex[Level]]; + return true; + } + return false; +} + +bool UGMS_MovementControlSetting_Default::GetStateByTag(const FGameplayTag& Tag, FGMS_MovementStateSetting& OutSetting) const +{ + if (auto Setting = GetMovementStateSetting(Tag)) + { + OutSetting = *Setting; + return true; + } + return false; +} + +const FGMS_MovementStateSetting* UGMS_MovementControlSetting_Default::GetMovementStateSetting(const FGameplayTag& Tag) const +{ + if (!Tag.IsValid()) + { + return nullptr; + } + return MovementStates.FindByPredicate([Tag](const FGMS_MovementStateSetting& Setting) + { + return Setting.Tag.IsValid() && Setting.Tag == Tag; + }); +} + +const FGMS_MovementStateSetting* UGMS_MovementControlSetting_Default::GetMovementStateSetting(const FGameplayTag& Tag, bool bHasFallback) const +{ + if (auto Setting = GetMovementStateSetting(Tag)) + { + return Setting; + } + + if (bHasFallback) + { + checkf(!MovementStates.IsEmpty(), TEXT("%s: MovementStates can't be empty!"), *GetNameSafe(this)) + return &MovementStates.Last(); + } + return nullptr; +} + +#pragma endregion + +#pragma region ControlSettings + +#if WITH_EDITOR + +float UGMS_MovementControlSetting_Default::MigrateRotationInterpolationSpeed(float Old) +{ + if (Old <= 0.0f) + { + return 0.0f; + } + // larger old value, smaller new value. + return FMath::GetMappedRangeValueClamped(FVector2f{0.0f, 20.0f}, FVector2f{0.3f, 0.1f}, Old); +} + +void UGMS_MovementControlSetting_Default::PreSave(FObjectPreSaveContext SaveContext) +{ + Super::PreSave(SaveContext); + + SpeedLevelToArrayIndex.Empty(); + + MovementStates.Sort([](const FGMS_MovementStateSetting& A, const FGMS_MovementStateSetting& B) + { + return A.SpeedLevel < B.SpeedLevel; + }); + + for (int i = 0; i < MovementStates.Num(); ++i) + { + FGMS_MovementStateSetting& Setting = MovementStates[i]; + Setting.EditorFriendlyName = FString::Format(TEXT("State({0}) SpeedLevel({1}) Speed({2})"), {UGMS_Utility::GetSimpleTagName(Setting.Tag).ToString(), Setting.SpeedLevel, Setting.Speed}); + SpeedLevelToArrayIndex.Emplace(Setting.SpeedLevel, i); + } + + // Migration code for GMS1.5, TODO remove in 1.6 + PRAGMA_DISABLE_DEPRECATION_WARNINGS + if (MovementStates.Num() > 0) + { + const FGMS_MovementStateSetting& LastMovementState = MovementStates[MovementStates.Num() - 1]; + + // migrate velocity direction setting. + if (!VelocityDirectionSetting.IsValid()) + { + if (LastMovementState.VelocityDirectionSetting.DirectionMode != EGMS_VelocityDirectionMode_DEPRECATED::TurningCircle) + { + FGMS_VelocityDirectionSetting_Default Temp; + Temp.bEnableRotationWhenNotMoving = LastMovementState.VelocityDirectionSetting.bEnableRotationWhenNotMoving; + Temp.TargetYawAngleRotationSpeed = LastMovementState.TargetYawAngleRotationSpeed; + Temp.RotationInterpolationSpeed = MigrateRotationInterpolationSpeed(LastMovementState.RotationInterpolationSpeed); + Temp.bOrientateToMoveInputIntent = LastMovementState.VelocityDirectionSetting.DirectionMode == EGMS_VelocityDirectionMode_DEPRECATED::OrientToInputDirection; + + VelocityDirectionSetting.InitializeAs(Temp); + } + else if (LastMovementState.VelocityDirectionSetting.DirectionMode == EGMS_VelocityDirectionMode_DEPRECATED::TurningCircle) + { + FGMS_VelocityDirectionSetting_RateBased Temp; + Temp.bEnableRotationWhenNotMoving = LastMovementState.VelocityDirectionSetting.bEnableRotationWhenNotMoving; + Temp.TurnRate = LastMovementState.VelocityDirectionSetting.TurningRate; + VelocityDirectionSetting.InitializeAs(Temp); + } + } + + // migrate view direction setting. + if (!ViewDirectionSetting.IsValid()) + { + if (LastMovementState.ViewDirectionSetting.DirectionMode == EGMS_ViewDirectionMode_DEPRECATED::Aiming) + { + FGMS_ViewDirectionSetting_Aiming Temp; + Temp.bEnableRotationWhenNotMoving = LastMovementState.ViewDirectionSetting.bRotateToViewDirectionWhileNotMoving; + Temp.TargetYawAngleRotationSpeed = LastMovementState.TargetYawAngleRotationSpeed; + Temp.RotationInterpolationSpeed = MigrateRotationInterpolationSpeed(LastMovementState.RotationInterpolationSpeed); + Temp.MinAimingYawAngleLimit = LastMovementState.ViewDirectionSetting.MinAimingYawAngleLimit; + + ViewDirectionSetting.InitializeAs(Temp); + } + else if (LastMovementState.ViewDirectionSetting.DirectionMode == EGMS_ViewDirectionMode_DEPRECATED::Default) + { + FGMS_ViewDirectionSetting_Default Temp; + Temp.bEnableRotationWhenNotMoving = LastMovementState.ViewDirectionSetting.bRotateToViewDirectionWhileNotMoving; + Temp.TargetYawAngleRotationSpeed = LastMovementState.TargetYawAngleRotationSpeed; + Temp.RotationInterpolationSpeed = MigrateRotationInterpolationSpeed(LastMovementState.RotationInterpolationSpeed); + ViewDirectionSetting.InitializeAs(Temp); + } + } + } + PRAGMA_ENABLE_DEPRECATION_WARNINGS + + // Safety guard if still invalid. + if (!VelocityDirectionSetting.IsValid()) + { + VelocityDirectionSetting.InitializeAs(FGMS_VelocityDirectionSetting_Default()); + } + if (!ViewDirectionSetting.IsValid()) + { + ViewDirectionSetting.InitializeAs(FGMS_ViewDirectionSetting_Default()); + } +} + +EDataValidationResult UGMS_MovementControlSetting_Default::IsDataValid(class FDataValidationContext& Context) const +{ + for (int32 i = 0; i < MovementStates.Num(); i++) + { + const FGMS_MovementStateSetting& MRSetting = MovementStates[i]; + if (!MRSetting.Tag.IsValid()) + { + Context.AddError(FText::FromString(FString::Format(TEXT("Invalid tag at index({0}) of MovementStates"), {i}))); + return EDataValidationResult::Invalid; + } + if (MRSetting.AllowedRotationModes.IsEmpty()) + { + Context.AddError(FText::FromString( + FString::Format(TEXT("AllowedRotationModes at index({0}) of MovementStates can't be empty!"), {i}))); + return EDataValidationResult::Invalid; + } + } + if (!ViewDirectionSetting.IsValid()) + { + Context.AddError(FText::FromString(TEXT("Invalid view direction setting"))); + return EDataValidationResult::Invalid; + } + if (ViewDirectionSetting.IsValid() && ViewDirectionSetting.GetScriptStruct() == FGMS_ViewDirectionSetting::StaticStruct()) + { + Context.AddError(FText::FromString(FString::Format(TEXT("View direction setting({0}) was deprecated!"), {FGMS_ViewDirectionSetting::StaticStruct()->GetName()}))); + return EDataValidationResult::Invalid; + } + + if (!VelocityDirectionSetting.IsValid()) + { + Context.AddError(FText::FromString(TEXT("Invalid velocity direction setting"))); + return EDataValidationResult::Invalid; + } + if (VelocityDirectionSetting.IsValid() && VelocityDirectionSetting.GetScriptStruct() == FGMS_VelocityDirectionSetting::StaticStruct()) + { + Context.AddError(FText::FromString(FString::Format(TEXT("Velocity direction setting({0}) was deprecated!"), {FGMS_VelocityDirectionSetting::StaticStruct()->GetName()}))); + return EDataValidationResult::Invalid; + } + + return Super::IsDataValid(Context); +} +#endif + +#pragma endregion diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingStructLibrary.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingStructLibrary.cpp new file mode 100644 index 0000000..53c9b76 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Settings/GMS_SettingStructLibrary.cpp @@ -0,0 +1,27 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Settings/GMS_SettingStructLibrary.h" + +#include "Settings/GMS_SettingObjectLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_SettingStructLibrary) + + +#if WITH_EDITOR +void FGMS_AnimDataSetting_General::PostEditChangeProperty(const FPropertyChangedEvent& PropertyChangedEvent) +{ + if (PropertyChangedEvent.GetPropertyName() != GET_MEMBER_NAME_CHECKED(FGMS_AnimDataSetting_General, GroundPredictionResponseChannels)) + { + return; + } + + GroundPredictionSweepResponses.SetAllChannels(ECR_Ignore); + + for (const auto CollisionChannel : GroundPredictionResponseChannels) + { + GroundPredictionSweepResponses.SetResponse(CollisionChannel, ECR_Block); + } +} + +#endif diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Log.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Log.cpp new file mode 100644 index 0000000..8fafb45 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Log.cpp @@ -0,0 +1,48 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Utility/GMS_Log.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" +#include "Animation/AnimInstance.h" + + +const FName GMSLog::MessageLogName{TEXTVIEW("GMS")}; + +DEFINE_LOG_CATEGORY(LogGMS) +DEFINE_LOG_CATEGORY(LogGMS_Animation) + +FString GetGMSLogContextString(const UObject* ContextObject) +{ + ENetRole Role = ROLE_None; + FString RoleName = TEXT("None"); + FString Name = "None"; + + if (const AActor* Actor = Cast(ContextObject)) + { + Role = Actor->GetLocalRole(); + Name = Actor->GetName(); + } + else if (const UActorComponent* Component = Cast(ContextObject)) + { + Role = Component->GetOwnerRole(); + Name = Component->GetOwner()->GetName(); + } + else if (const UAnimInstance* AnimInstance = Cast(ContextObject)) + { + if (AnimInstance->GetOwningActor()) + { + Role = AnimInstance->GetOwningActor()->GetLocalRole(); + Name = AnimInstance->GetOwningActor()->GetName(); + } + } + else if (IsValid(ContextObject)) + { + Name = ContextObject->GetName(); + } + + if (Role != ROLE_None) + { + RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client"); + } + return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Math.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Math.cpp new file mode 100644 index 0000000..9efecd0 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Math.cpp @@ -0,0 +1,8 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utility/GMS_Math.h" + +#include "Locomotions/GMS_LocomotionEnumLibrary.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_Math) diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Tags.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Tags.cpp new file mode 100644 index 0000000..7a2b7fe --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Tags.cpp @@ -0,0 +1,47 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#include "Utility/GMS_Tags.h" + +namespace GMS_MovementModeTags +{ + UE_DEFINE_GAMEPLAY_TAG(None, FName{TEXTVIEW("GMS.LocomotionMode.None")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded, FName{TEXTVIEW("GMS.LocomotionMode.Grounded")}) + UE_DEFINE_GAMEPLAY_TAG(InAir, FName{TEXTVIEW("GMS.LocomotionMode.InAir")}) + UE_DEFINE_GAMEPLAY_TAG(Flying, FName{TEXTVIEW("GMS.LocomotionMode.Flying")}) + UE_DEFINE_GAMEPLAY_TAG(Swimming, FName{TEXTVIEW("GMS.LocomotionMode.Swimming")}) + UE_DEFINE_GAMEPLAY_TAG(Zipline, FName{TEXTVIEW("GMS.LocomotionMode.Zipline")}) +} + +namespace GMS_RotationModeTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(VelocityDirection, FName{TEXTVIEW("GMS.RotationMode.VelocityDirection")}, "Rotate(orientate) to given velocity direction.(转向指定的的速率方向)") + UE_DEFINE_GAMEPLAY_TAG_COMMENT(ViewDirection, FName{TEXTVIEW("GMS.RotationMode.ViewDirection")}, "Rotate(orientate) to view direction(转向看的方向)") +} + +namespace GMS_MovementStateTags +{ + UE_DEFINE_GAMEPLAY_TAG(Walk, FName{TEXTVIEW("GMS.MovementState.Walk")}) + UE_DEFINE_GAMEPLAY_TAG(Jog, FName{TEXTVIEW("GMS.MovementState.Jog")}) + UE_DEFINE_GAMEPLAY_TAG(Sprint, FName{TEXTVIEW("GMS.MovementState.Sprint")}) +} + +namespace GMS_OverlayModeTags +{ + UE_DEFINE_GAMEPLAY_TAG(None, FName{TEXTVIEW("GMS.OverlayMode.None")}) + UE_DEFINE_GAMEPLAY_TAG(Default, FName{TEXTVIEW("GMS.OverlayMode.Default")}) +} + +namespace GMS_SMTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Root, FName{TEXTVIEW("GMS.SM")}, "State Machine Root Tag"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(InAir, FName{TEXTVIEW("GMS.SM.InAir")}, "InAir States") + UE_DEFINE_GAMEPLAY_TAG(InAir_Jump, FName{TEXTVIEW("GMS.SM.InAir.Jump")}) + UE_DEFINE_GAMEPLAY_TAG(InAir_Fall, FName{TEXTVIEW("GMS.SM.InAir.Fall")}) + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Grounded, FName{TEXTVIEW("GMS.SM.Grounded")}, "Grounded States") + UE_DEFINE_GAMEPLAY_TAG(Grounded_Idle, FName{TEXTVIEW("GMS.SM.Grounded.Idle")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded_Start, FName{TEXTVIEW("GMS.SM.Grounded.Start")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded_Cycle, FName{TEXTVIEW("GMS.SM.Grounded.Cycle")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded_Stop, FName{TEXTVIEW("GMS.SM.Grounded.Stop")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded_Pivot, FName{TEXTVIEW("GMS.SM.Grounded.Pivot")}) + UE_DEFINE_GAMEPLAY_TAG(Grounded_Land, FName{TEXTVIEW("GMS.SM.Grounded.Land")}) +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Utility.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Utility.cpp new file mode 100644 index 0000000..13fc725 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Utility.cpp @@ -0,0 +1,237 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utility/GMS_Utility.h" + +#include "Chooser.h" +#include "ChooserPropertyAccess.h" +#include "PoseSearch/PoseSearchDatabase.h" +#include "GameplayTagsManager.h" +#include "IObjectChooser.h" +#include "Animation/AnimInstance.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/World.h" +#include "GameFramework/Character.h" +#include "GameFramework/HUD.h" +#include "Animation/AnimSequenceBase.h" +#include "Locomotions/GMS_AnimLayer.h" +#include "Locomotions/GMS_MainAnimInstance.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "Utility/GMS_Log.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_Utility) + +FString UGMS_Utility::NameToDisplayString(const FName& Name, const bool bNameIsBool) +{ + return FName::NameToDisplayString(Name.ToString(), bNameIsBool); +} + +float UGMS_Utility::GetAnimationCurveValueFromCharacter(const ACharacter* Character, const FName& CurveName) +{ + const auto* Mesh{IsValid(Character) ? Character->GetMesh() : nullptr}; + const auto* AnimationInstance{IsValid(Mesh) ? Mesh->GetAnimInstance() : nullptr}; + + return IsValid(AnimationInstance) ? AnimationInstance->GetCurveValue(CurveName) : 0.0f; +} + +FGameplayTagContainer UGMS_Utility::GetChildTags(const FGameplayTag& Tag) +{ + return UGameplayTagsManager::Get().RequestGameplayTagChildren(Tag); +} + +FName UGMS_Utility::GetSimpleTagName(const FGameplayTag& Tag) +{ + const auto TagNode{UGameplayTagsManager::Get().FindTagNode(Tag)}; + + return TagNode.IsValid() ? TagNode->GetSimpleTagName() : NAME_None; +} + +bool UGMS_Utility::ShouldDisplayDebugForActor(const AActor* Actor, const FName& DisplayName) +{ + const auto* World{IsValid(Actor) ? Actor->GetWorld() : nullptr}; + const auto* PlayerController{IsValid(World) ? World->GetFirstPlayerController() : nullptr}; + auto* Hud{IsValid(PlayerController) ? PlayerController->GetHUD() : nullptr}; + + return IsValid(Hud) && Hud->ShouldDisplayDebug(DisplayName) && Hud->GetCurrentDebugTargetActor() == Actor; +} + + +float UGMS_Utility::CalculateAnimatedSpeed(const UAnimSequenceBase* AnimSequence) +{ + if (AnimSequence == nullptr) + { + UE_LOG(LogGMS, Warning, TEXT("Passed invalid anim sequence")); + return 0.0f; + } + + const float AnimLength = AnimSequence->GetPlayLength(); + // Calculate the speed as: (distance traveled by the animation) / (length of the animation) + const FVector RootMotionTranslation = AnimSequence->ExtractRootMotionFromRange(0.0f, AnimLength).GetTranslation(); + const float RootMotionDistance = RootMotionTranslation.Size2D(); + if (!FMath::IsNearlyZero(RootMotionDistance)) + { + const float AnimationSpeed = RootMotionDistance / AnimLength; + return AnimationSpeed; + } + UE_LOG(LogGMS, Warning, TEXT("Unable to Calculate animation speed for animation with no root motion delta (%s)."), *GetNameSafe(AnimSequence)); + return 0.0f; +} + + +UAnimSequence* UGMS_Utility::SelectAnimationWithFloat(const TArray& Animations, const float& ReferenceValue) +{ + if (Animations.IsEmpty()) + { + return nullptr; + } + + float Delta = FLT_MAX; // 使用FLT_MAX来初始化Delta + int32 Found = INDEX_NONE; + + for (int32 i = 0; i < Animations.Num(); i++) + { + // 计算与传入Float的绝对差值 + float TempDelta = FMath::Abs(ReferenceValue - Animations[i].Distance); + + // 如果当前的差值更小,则更新Delta和Found + if (TempDelta < Delta) + { + Delta = TempDelta; + Found = i; + } + } + + // 返回找到的动画或最后一个动画作为备选 + return (Found != INDEX_NONE) ? Animations[Found].Animation : Animations.Last().Animation; +} + +bool UGMS_Utility::ValidatePoseSearchDatabasesChooser(const UChooserTable* ChooserTable, FText& OutMessage) +{ + if (!IsValid(ChooserTable)) + { + OutMessage = FText::FromName("Invalid ChooserTable"); + return false; + } + if (ChooserTable->GetContextData().Num() != 2) + { + OutMessage = FText::FromString(FString::Format(TEXT("ChooserTable({0}):Context is empty, and only allow 2 element!"), {*ChooserTable->GetName()})); + return false; + } + + if (ChooserTable->ResultType != EObjectChooserResultType::ObjectResult || ChooserTable->OutputObjectType != UPoseSearchDatabase::StaticClass()) + { + OutMessage = FText::FromString(FString::Format(TEXT("ChooserTable({0}):Result type must be ObjectResult and the OutputObjectType must be PoseSearchDatabase."), {*ChooserTable->GetName()})); + return false; + } + + const FContextObjectTypeClass* Ctx1 = ChooserTable->GetContextData()[0].GetPtr(); + + bool bValidCtx1 = Ctx1 != nullptr && Ctx1->Class != nullptr && Ctx1->Class->IsChildOf(UGMS_MainAnimInstance::StaticClass()); + + if (!bValidCtx1) + { + OutMessage = FText::FromString(FString::Format( + TEXT("ChooserTable({0}): First context must be ContextObjectTypeClass and the class must be Subclass of UGMS_MainAnimInstance."), {*ChooserTable->GetName()})); + return false; + } + + const FContextObjectTypeClass* Ctx2 = ChooserTable->GetContextData()[1].GetPtr(); + + bool bValidCtx2 = Ctx2 != nullptr && Ctx2->Class != nullptr && Ctx2->Class->IsChildOf(UGMS_AnimLayer::StaticClass()); + + if (!bValidCtx2) + { + OutMessage = FText::FromString( + FString::Format(TEXT("ChooserTable({0}): Secondary context must be ContextObjectTypeClass and the class must be Subclass of UGMS_AnimLayer."), {*ChooserTable->GetName()})); + return false; + } + return true; +} + +bool UGMS_Utility::IsValidPoseSearchDatabasesChooser(const UChooserTable* ChooserTable) +{ + if (!IsValid(ChooserTable)) + { + return false; + } + if (ChooserTable->GetContextData().Num() != 2) + { + UE_LOG(LogGMS, Warning, TEXT("ChooserTable(%s):Context is empty, and only allow 2 element!"), *ChooserTable->GetName()); + return false; + } + + if (ChooserTable->ResultType != EObjectChooserResultType::ObjectResult || ChooserTable->OutputObjectType != UPoseSearchDatabase::StaticClass()) + { + UE_LOG(LogGMS, Warning, TEXT("ChooserTable(%s):Result type must be ObjectResult and the OutputObjectType must be PoseSearchDatabase."), *ChooserTable->GetName()); + return false; + } + + const FContextObjectTypeClass* Ctx1 = ChooserTable->GetContextData()[0].GetPtr(); + + bool bValidCtx1 = Ctx1 != nullptr && Ctx1->Class != nullptr && Ctx1->Class->IsChildOf(UGMS_MainAnimInstance::StaticClass()); + + if (!bValidCtx1) + { + UE_LOG(LogGMS, Warning, TEXT("ChooserTable(%s): First context must be ContextObjectTypeClass and the class must be Subclass of UGMS_MainAnimInstance."), + *ChooserTable->GetName()); + return false; + } + + const FContextObjectTypeClass* Ctx2 = ChooserTable->GetContextData()[1].GetPtr(); + + bool bValidCtx2 = Ctx2 != nullptr && Ctx2->Class != nullptr && Ctx2->Class->IsChildOf(UGMS_AnimLayer::StaticClass()); + + if (!bValidCtx2) + { + GMS_LOG(Warning, "ChooserTable(%s): Secondary context must be ContextObjectTypeClass and the class must be Subclass of UGMS_AnimLayer.", + *ChooserTable->GetName()) + return false; + } + + return true; +} + +TArray UGMS_Utility::EvaluatePoseSearchDatabasesChooser(const UGMS_MainAnimInstance* MainAnimInstance, const UGMS_AnimLayer* AnimLayerInstance, + UChooserTable* ChooserTable) +{ + TArray Ret; + + if (!IsValid(ChooserTable)) + { + return Ret; + } + + // Fallback single context object version + FChooserEvaluationContext Context; + Context.AddObjectParam(const_cast(MainAnimInstance)); + Context.AddObjectParam(const_cast(AnimLayerInstance)); + + auto Callback = FObjectChooserBase::FObjectChooserIteratorCallback::CreateLambda([&Ret](UObject* InResult) + { + if (InResult && InResult->IsA(UPoseSearchDatabase::StaticClass())) + { + Ret.Add(Cast(InResult)); + } + return FObjectChooserBase::EIteratorStatus::Continue; + }); + + UChooserTable::EvaluateChooser(Context, ChooserTable, Callback); + return Ret; +} + +const UGMS_MovementSetUserSetting* UGMS_Utility::GetMovementSetUserSetting(const FGMS_MovementSetSetting& MovementSetSetting, TSubclassOf DesiredClass) +{ + if (!IsValid(DesiredClass)) + { + return nullptr; + } + + for (TObjectPtr UserSetting : MovementSetSetting.UserSettings) + { + if (UserSetting->GetClass() == DesiredClass) + { + return UserSetting; + } + } + return nullptr; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Vector.cpp b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Vector.cpp new file mode 100644 index 0000000..8370ac8 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Private/Utility/GMS_Vector.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#include "Utility/GMS_Vector.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_Vector) + +FVector UGMS_Vector::SlerpSkipNormalization(const FVector& From, const FVector& To, const float Ratio) +{ + // http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/ + + auto Dot{From | To}; + + if (Dot > 0.9995f) + { + return FMath::Lerp(From, To, Ratio).GetSafeNormal(); + } + + Dot = FMath::Max(-1.0f, Dot); + + const auto Theta{UE_REAL_TO_FLOAT(FMath::Acos(Dot)) * Ratio}; + + float Sin, Cos; + FMath::SinCos(&Sin, &Cos, Theta); + + auto FromPerpendicular{To - From * Dot}; + FromPerpendicular.Normalize(); + + return From * Cos + FromPerpendicular * Sin; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_CharacterMovementSystemComponent.h b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_CharacterMovementSystemComponent.h new file mode 100644 index 0000000..9eb0591 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_CharacterMovementSystemComponent.h @@ -0,0 +1,616 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GMS_MovementSystemComponent.h" +#include "GameFramework/Character.h" +#include "GMS_CharacterMovementSystemComponent.generated.h" + +class UGMS_CharacterMovementSetting_Default; +class UGMS_CharacterRotationSetting_Default; + +/** + * Movement system component for characters. + * 角色的运动系统组件。 + */ +UCLASS(ClassGroup=GMS, BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent), DisplayName="GMS Movement System Component(Character)") +class GENERICMOVEMENTSYSTEM_API UGMS_CharacterMovementSystemComponent : public UGMS_MovementSystemComponent +{ + GENERATED_BODY() + +public: + /** + * Constructor with object initializer. + * 使用对象初始化器构造函数。 + */ + explicit UGMS_CharacterMovementSystemComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** + * Gets lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the character movement component. + * 获取角色运动组件。 + * @return The character movement component. 角色运动组件。 + */ + UCharacterMovementComponent* GetCharacterMovement() const { return CharacterMovement; }; + +protected: + /** + * The character movement component. + * 角色运动组件。 + */ + UPROPERTY() + TObjectPtr CharacterMovement; + + /** + * The owning character. + * 拥有该组件的角色。 + */ + UPROPERTY() + TObjectPtr OwnerCharacter{nullptr}; + + /** + * Maps movement modes to gameplay tags. + * 将运动模式映射到游戏标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|GameplayTags", meta=(Categories="GMS.LocomotionMode")) + TMap, FGameplayTag> MovementModeToTagMapping; + + /** + * Maps custom movement modes to gameplay tags. + * 将自定义运动模式映射到游戏标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|GameplayTags", meta=(Categories="GMS.LocomotionMode")) + TMap CustomMovementModeToTagMapping; + + /** + * Whether to apply movement settings to the character movement component. + * 是否将运动设置应用于角色运动组件。 + * @note Can be disabled for external control (e.g., AI). 可禁用以进行外部控制(例如AI)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|DynamicMovementState") + bool bAllowRefreshCharacterMovementSettings{true}; + + /** + * Curve mapping ground speed to movement state index (e.g., walk, jog, sprint). + * 将地面速度映射到运动状态索引的曲线(例如走、慢跑、冲刺)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|DynamicMovementState", meta=(EditCondition="!bAllowRefreshCharacterMovementSettings")) + TObjectPtr SpeedToMovementStateCurve; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +public: + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + +protected: + /** + * Called when the character's movement mode changes. + * 角色运动模式更改时调用。 + * @param InCharacter The character. 角色。 + * @param PrevMovementMode The previous movement mode. 之前的运动模式。 + * @param PreviousCustomMode The previous custom mode. 之前的自定义模式。 + */ + UFUNCTION() + virtual void OnCharacterMovementModeChanged(ACharacter* InCharacter, EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0); + + /** + * Called when the locomotion mode is replicated. + * 运动模式复制时调用。 + * @param PreviousLocomotionMode The previous locomotion mode. 之前的运动模式。 + */ + virtual void OnReplicated_LocomotionMode(const FGameplayTag& PreviousLocomotionMode) override; + + /** + * Calculates the actual movement state. + * 计算实际的运动状态。 + * @return The actual movement state. 实际的运动状态。 + */ + virtual FGameplayTag CalculateActualMovementState(); + + /** + * Applies the desired movement state settings to the character movement component. + * 将期望的运动状态设置应用于角色运动组件。 + */ + virtual void ApplyMovementSetting() override; + + /** + * Called when the rotation mode changes. + * 旋转模式更改时调用。 + * @param PreviousRotationMode The previous rotation mode. 之前的旋转模式。 + */ + virtual void OnRotationModeChanged_Implementation(const FGameplayTag& PreviousRotationMode) override; + + virtual void RefreshMovementBase() override; + +#pragma region MovementState + +public: + /** + * Gets the desired movement state. + * 获取期望的运动状态。 + * @return The desired movement state. 期望的运动状态。 + */ + virtual const FGameplayTag& GetDesiredMovementState() const override; + + /** + * Sets the desired movement state. + * 设置期望的运动状态。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + */ + virtual void SetDesiredMovement(const FGameplayTag& NewDesiredMovement) override; + + /** + * Gets the current movement state. + * 获取当前的运动状态。 + * @return The current movement state. 当前的运动状态。 + */ + virtual const FGameplayTag& GetMovementState() const override; + +protected: + /** + * Sets the movement state. + * 设置运动状态。 + * @param NewMovementState The new movement state. 新运动状态。 + */ + virtual void SetMovementState(const FGameplayTag& NewMovementState); + + /** + * Refreshes the movement state. + * 刷新运动状态。 + */ + virtual void RefreshMovementState(); + +private: + /** + * Sets the desired movement state with optional RPC. + * 设置期望的运动状态,可选择是否发送RPC。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetDesiredMovement(const FGameplayTag& NewDesiredMovement, bool bSendRpc); + + /** + * Client RPC to set the desired movement state. + * 客户端RPC设置期望的运动状态。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + */ + UFUNCTION(Client, Reliable) + void ClientSetDesiredMovement(const FGameplayTag& NewDesiredMovement); + virtual void ClientSetDesiredMovement_Implementation(const FGameplayTag& NewDesiredMovement); + + /** + * Server RPC to set the desired movement state. + * 服务器RPC设置期望的运动状态。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + */ + UFUNCTION(Server, Reliable) + void ServerSetDesiredMovement(const FGameplayTag& NewDesiredMovement); + virtual void ServerSetDesiredMovement_Implementation(const FGameplayTag& NewDesiredMovement); +#pragma endregion + +#pragma region RotationMode + +public: + /** + * Gets the desired rotation mode. + * 获取期望的旋转模式。 + * @return The desired rotation mode. 期望的旋转模式。 + */ + virtual const FGameplayTag& GetDesiredRotationMode() const override; + + /** + * Sets the desired rotation mode. + * 设置期望的旋转模式。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + */ + virtual void SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode) override; + + /** + * Gets the current rotation mode. + * 获取当前的旋转模式。 + * @return The current rotation mode. 当前的旋转模式。 + */ + virtual const FGameplayTag& GetRotationMode() const override; + +protected: + /** + * Sets the rotation mode. + * 设置旋转模式。 + * @param NewRotationMode The new rotation mode. 新旋转模式。 + */ + virtual void SetRotationMode(const FGameplayTag& NewRotationMode); + + /** + * Refreshes the rotation mode. + * 刷新旋转模式。 + */ + virtual void RefreshRotationMode(); + +private: + /** + * Sets the desired rotation mode with optional RPC. + * 设置期望的旋转模式,可选择是否发送RPC。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode, bool bSendRpc); + + /** + * Client RPC to set the desired rotation mode. + * 客户端RPC设置期望的旋转模式。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + */ + UFUNCTION(Client, Reliable) + void ClientSetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode); + virtual void ClientSetDesiredRotationMode_Implementation(const FGameplayTag& NewDesiredRotationMode); + + /** + * Server RPC to set the desired rotation mode. + * 服务器RPC设置期望的旋转模式。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + */ + UFUNCTION(Server, Reliable) + void ServerSetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode); + virtual void ServerSetDesiredRotationMode_Implementation(const FGameplayTag& NewDesiredRotationMode); +#pragma endregion + +#pragma region Input + +public: + /** + * Gets the movement intent. + * 获取移动意图。 + * @return The movement intent vector. 移动意图向量。 + */ + virtual FVector GetMovementIntent() const override; + + /** + * Sets the movement intent. + * 设置移动意图。 + * @param NewMovementIntent The new movement intent vector. 新移动意图向量。 + */ + void SetMovementIntent(FVector NewMovementIntent); + +protected: + /** + * Refreshes input handling. + * 刷新输入处理。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshInput(float DeltaTime) override; + + /** + * Replicated movement intent. + * 复制的移动意图。 + */ + UPROPERTY(VisibleAnywhere, Category="State|Input", Transient, Replicated) + FVector_NetQuantizeNormal MovementIntent; + + /** + * Desired movement state. + * 期望的运动状态。 + */ + UPROPERTY(EditAnywhere, Category="Settings", Replicated, meta=(Categories="GMS.MovementState")) + FGameplayTag DesiredMovementState{GMS_MovementStateTags::Jog}; + + /** + * Desired rotation mode. + * 期望的旋转模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings", Replicated, meta=(Categories="GMS.RotationMode")) + FGameplayTag DesiredRotationMode{GMS_RotationModeTags::ViewDirection}; + + /** + * Current movement state. + * 当前的运动状态。 + */ + UPROPERTY(VisibleAnywhere, Category="State", Transient, meta=(Categories="GMS.MovementState")) + FGameplayTag MovementState{GMS_MovementStateTags::Jog}; + + /** + * Current rotation mode. + * 当前的旋转模式。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", Transient, meta=(Categories="GMS.RotationMode")) + FGameplayTag RotationMode{GMS_RotationModeTags::ViewDirection}; +#pragma endregion + +#pragma region ViewSystem + /** + * Refreshes the view system. + * 刷新视图系统。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshView(float DeltaTime); +#pragma endregion + +#pragma region Abstraction + +public: + /** + * Checks if the character is crouching. + * 检查角色是否在蹲伏。 + * @return True if crouching. 如果在蹲伏返回true。 + */ + virtual bool IsCrouching() const override; + + /** + * Gets the maximum speed. + * 获取最大速度。 + * @return The maximum speed. 最大速度。 + */ + virtual float GetMaxSpeed() const override; + + /** + * Gets the scaled capsule radius. + * 获取缩放的胶囊体半径。 + * @return The capsule radius. 胶囊体半径。 + */ + virtual float GetScaledCapsuleRadius() const override; + + /** + * Gets the scaled capsule half height. + * 获取缩放的胶囊体半高。 + * @return The capsule half height. 胶囊体半高。 + */ + virtual float GetScaledCapsuleHalfHeight() const override; + + /** + * Gets the maximum acceleration. + * 获取最大加速度。 + * @return The maximum acceleration. 最大加速度。 + */ + virtual float GetMaxAcceleration() const override; + + /** + * Gets the maximum braking deceleration. + * 获取最大制动减速度。 + * @return The maximum braking deceleration. 最大制动减速度。 + */ + virtual float GetMaxBrakingDeceleration() const override; + + /** + * Gets the walkable floor Z value. + * 获取可行走地面Z值。 + * @return The walkable floor Z. 可行走地面Z值。 + */ + virtual float GetWalkableFloorZ() const override; + + /** + * Gets the gravity Z value. + * 获取重力Z值。 + * @return The gravity Z. 重力Z值。 + */ + virtual float GetGravityZ() const override; + + /** + * Gets the skeletal mesh component. + * 获取骨骼网格组件。 + * @return The skeletal mesh component. 骨骼网格组件。 + */ + virtual USkeletalMeshComponent* GetMesh() const override; + + virtual bool IsMovingOnGround() const override; + +#pragma endregion + +#pragma region Locomotion + +protected: + /** + * Early refresh for locomotion. + * 运动的早期刷新。 + */ + virtual void RefreshLocomotionEarly(); + + /** + * Refreshes locomotion state. + * 刷新运动状态。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshLocomotion(float DeltaTime); + + /** + * Late refresh for locomotion. + * 运动的后期刷新。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshLocomotionLate(float DeltaTime); + + /** + * Refreshes dynamic movement state. + * 刷新动态运动状态。 + */ + virtual void RefreshDynamicMovementState(); +#pragma endregion + +#pragma region Rotation System + +public: + /** + * Sets rotation instantly. + * 立即设置旋转。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + * @param Teleport The teleport type. 传送类型。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|MovementSystem") + void SetRotationInstant(const float TargetYawAngle, const ETeleportType Teleport); + + /** + * Sets rotation smoothly. + * 平滑设置旋转。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param InterpolationHalfLife The rotation interpolation speed. 旋转插值速度。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|MovementSystem") + void SetRotationSmooth(float TargetYawAngle, float DeltaTime, float InterpolationHalfLife); + + /** + * Sets rotation with extra smoothness. + * 额外平滑设置旋转。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param InterpolationHalfLife The rotation interpolation speed. 旋转插值速度。 + * @param TargetYawAngleRotationSpeed The target yaw rotation speed. 目标偏航旋转速度。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|MovementSystem") + void SetRotationExtraSmooth(float TargetYawAngle, float DeltaTime, float InterpolationHalfLife, float TargetYawAngleRotationSpeed); + +protected: + /** + * Refreshes the character's rotation. + * 刷新角色的旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + void RefreshRotation(float DeltaTime); + virtual void RefreshRotation_Implementation(float DeltaTime); + + /** + * Refreshes grounded rotation. + * 刷新地面旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshGroundedRotation(float DeltaTime); + + /** + * Refreshes rotation when grounded and not moving. + * 地面上且不移动时刷新旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshGroundedNotMovingRotation(float DeltaTime); + + + virtual float CalculateGroundedMovingRotationInterpolationSpeed(TObjectPtr SpeedCurve, float Default = 12.0f) const; + + /** + * Refreshes rotation when grounded and moving. + * 地面上且移动时刷新旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshGroundedMovingRotation(float DeltaTime); + + /** + * Constrains aiming rotation. + * 约束瞄准旋转。 + * @param ActorRotation The actor's rotation. Actor的旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param bApplySecondaryConstraint Whether to apply secondary constraints. 是否应用次级约束。 + * @return True if rotation was constrained. 如果旋转被约束返回true。 + */ + virtual bool ConstrainAimingRotation(FRotator& ActorRotation, float DeltaTime, bool bApplySecondaryConstraint = false); + + /** + * Applies rotation yaw speed animation curve. + * 应用旋转偏航速度动画曲线。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @return True if the curve was applied. 如果应用了曲线返回true。 + */ + bool ApplyRotationYawSpeedAnimationCurve(float DeltaTime); + + /** + * Custom rotation logic for grounded moving state. + * 地面移动状态的自定义旋转逻辑。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @return True if default rotation logic should be skipped. 如果应跳过默认旋转逻辑返回true。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + bool RefreshCustomGroundedMovingRotation(float DeltaTime); + + /** + * Custom rotation logic for grounded not moving state. + * 地面不移动状态的自定义旋转逻辑。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @return True if default rotation logic should be skipped. 如果应跳过默认旋转逻辑返回true。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + bool RefreshCustomGroundedNotMovingRotation(float DeltaTime); + + /** + * Refreshes in-air rotation. + * 刷新空中旋转。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshInAirRotation(float DeltaTime); + + /** + * Refreshes the target yaw angle using actor's rotation. + * 使用Actor的旋转刷新目标偏航角。 + */ + void RefreshTargetYawAngleUsingActorRotation(); + + /** + * Sets the target yaw angle. + * 设置目标偏航角。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + */ + void SetTargetYawAngle(float TargetYawAngle); + + /** + * Sets the target yaw angle smoothly. + * 平滑设置目标偏航角。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param RotationSpeed The rotation speed. 旋转速度。 + */ + void SetTargetYawAngleSmooth(float TargetYawAngle, float DeltaTime, float RotationSpeed); + + /** + * Refreshes the view-relative target yaw angle. + * 刷新相对于视图的目标偏航角。 + */ + void RefreshViewRelativeTargetYawAngle(); + + virtual void SetActorRotation(FRotator DesiredRotation); + +#pragma endregion + +#pragma region DistanceMatching + +public: + /** + * Gets parameters for predicting ground movement pivot location. + * 获取预测地面运动枢轴位置的参数。 + * @return The pivot location parameters. 枢轴位置参数。 + */ + virtual FGMS_PredictGroundMovementPivotLocationParams GetPredictGroundMovementPivotLocationParams() const override; + + /** + * Gets parameters for predicting ground movement stop location. + * 获取预测地面运动停止位置的参数。 + * @return The stop location parameters. 停止位置参数。 + */ + virtual FGMS_PredictGroundMovementStopLocationParams GetPredictGroundMovementStopLocationParams() const override; +#pragma endregion +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MovementSystemComponent.h b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MovementSystemComponent.h new file mode 100644 index 0000000..9a79cab --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MovementSystemComponent.h @@ -0,0 +1,1047 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/NetSerialization.h" +#include "Components/ActorComponent.h" +#include "Locomotions/GMS_LocomotionStructLibrary.h" +#include "Settings/GMS_SettingStructLibrary.h" +#include "Utility/GMS_Tags.h" +#include "GMS_MovementSystemComponent.generated.h" + +class IPoseSearchTrajectoryPredictorInterface; +class UGMS_MainAnimInstance; +class UGMS_MovementControlSetting_Default; +class UGMS_AnimGraphSetting; +class UGMS_MovementSystemComponentSettings; +class APawn; +class UAnimInstance; +class UGMS_MovementDefinition; + +/** + * Delegate for movement set change events. + * 运动集更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMovementSetChangedSignature, const FGameplayTag&, PreviousMovementSet); + +/** + * Delegate for movement state change events. + * 运动状态更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMovementChangedSignature, const FGameplayTag&, PreviousMovement); + +/** + * Delegate for overlay mode change events. + * 覆盖模式更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOverlayModeChangedSignature, const FGameplayTag&, PreviousMovement); + +/** + * Delegate for rotation mode change events. + * 旋转模式更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FRotationModeChangedSignature, const FGameplayTag&, PreviousRotationMode); + +/** + * Delegate for locomotion mode change events. + * 运动模式更改事件的委托。 + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLocomotionModeChangedSignature, const FGameplayTag&, PreviousLocomotionMode); + +/** + * Base movement system component. + * 基础运动系统组件。 + * @details This is the bridge between the movement logic part and animation part. 这是运动逻辑和动画之间的桥梁。 + */ +UCLASS(BlueprintType, Abstract, NotBlueprintable, HideCategories = (Navigation,Cooking,ComponentReplication,Sockets)) +class GENERICMOVEMENTSYSTEM_API UGMS_MovementSystemComponent : public UActorComponent +{ + GENERATED_BODY() + + friend UGMS_MainAnimInstance; + +public: + /** + * Constructor with object initializer. + * 使用对象初始化器构造函数。 + */ + explicit UGMS_MovementSystemComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + virtual void PostLoad() override; + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Gets lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Gets the movement system component from an actor. + * 从Actor获取运动系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @return The movement system component. 运动系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem", meta=(DefaultToSelf="Actor", DisplayName="Get Movement System Component")) + static UGMS_MovementSystemComponent* GetMovementSystemComponent(const AActor* Actor); + + /** + * Finds the movement system component on an actor. + * 在Actor上查找运动系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param Instance The found instance (output). 找到的实例(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", meta=(DisplayName="Find Movement System Component", DefaultToSelf="Actor", ExpandBoolAsExecs = "ReturnValue")) + static bool K2_FindMovementComponent(const AActor* Actor, UGMS_MovementSystemComponent*& Instance); + + /** + * Finds the movement system component with a specific class. + * 查找特定类的运动系统组件。 + * @param Actor The actor to query. 要查询的Actor。 + * @param DesiredClass The desired component class. 期望的组件类。 + * @param Instance The found instance (output). 找到的实例(输出)。 + * @return True if found. 如果找到返回true。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", + meta=(DisplayName="Find Movement System Component(Ext)", DefaultToSelf="Actor", ExpandBoolAsExecs = "ReturnValue", DeterminesOutputType=DesiredClass, DynamicOutputParam="Instance")) + static bool K2_FindMovementComponentExt(const AActor* Actor, TSubclassOf DesiredClass, UGMS_MovementSystemComponent*& Instance); + + /** + * Gets combined gameplay tags from owned tags and tag provider. + * 获取拥有的标签和标签提供者的合并游戏标签。 + * @return The combined gameplay tags. 合并的游戏标签。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem", meta=(BlueprintThreadSafe)) + FGameplayTagContainer GetGameplayTags() const; + + /** + * Sets the gameplay tags provider. + * 设置游戏标签提供者。 + * @param Provider The object providing gameplay tags. 提供游戏标签的对象。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void SetGameplayTagsProvider(UObject* Provider); + +#pragma region GameplayTags + + /** + * Adds a single gameplay tag. + * 添加单个游戏标签。 + * @param TagToAdd The tag to add. 要添加的标签。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void AddGameplayTag(FGameplayTag TagToAdd); + + /** + * Removes a single gameplay tag. + * 移除单个游戏标签。 + * @param TagToRemove The tag to remove. 要移除的标签。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void RemoveGameplay(FGameplayTag TagToRemove); + + /** + * Sets gameplay tags, overriding existing ones. + * 设置游戏标签,覆盖现有标签。 + * @param TagsToSet The new tags to set. 要设置的新标签。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void SetGameplayTags(FGameplayTagContainer TagsToSet); + +private: + /** + * Adds a gameplay tag with optional RPC. + * 添加游戏标签,可选择是否发送RPC。 + * @param TagToAdd The tag to add. 要添加的标签。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void AddGameplayTag(const FGameplayTag& TagToAdd, bool bSendRpc); + + /** + * Removes a gameplay tag with optional RPC. + * 移除游戏标签,可选择是否发送RPC。 + * @param TagToRemove The tag to remove. 要移除的标签。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void RemoveGameplayTag(const FGameplayTag& TagToRemove, bool bSendRpc); + + /** + * Sets gameplay tags with optional RPC. + * 设置游戏标签,可选择是否发送RPC。 + * @param TagsToSet The new tags to set. 要设置的新标签。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetGameplayTags(const FGameplayTagContainer& TagsToSet, bool bSendRpc); + + /** + * Client RPC to add a gameplay tag. + * 客户端RPC添加游戏标签。 + * @param TagToAdd The tag to add. 要添加的标签。 + */ + UFUNCTION(Client, Reliable) + void ClientAddGameplayTag(const FGameplayTag& TagToAdd); + + /** + * Server RPC to add a gameplay tag. + * 服务器RPC添加游戏标签。 + * @param TagToAdd The tag to add. 要添加的标签。 + */ + UFUNCTION(Server, Reliable) + void ServerAddGameplayTag(const FGameplayTag& TagToAdd); + + /** + * Client RPC to remove a gameplay tag. + * 客户端RPC移除游戏标签。 + * @param TagToRemove The tag to remove. 要移除的标签。 + */ + UFUNCTION(Client, Reliable) + void ClientRemoveGameplayTag(const FGameplayTag& TagToRemove); + + /** + * Server RPC to remove a gameplay tag. + * 服务器RPC移除游戏标签。 + * @param TagToRemove The tag to remove. 要移除的标签。 + */ + UFUNCTION(Server, Reliable) + void ServerRemoveGameplayTag(const FGameplayTag& TagToRemove); + + /** + * Client RPC to set gameplay tags. + * 客户端RPC设置游戏标签。 + * @param TagsToSet The new tags to set. 要设置的新标签。 + */ + UFUNCTION(Client, Reliable) + void ClientSetGameplayTags(const FGameplayTagContainer& TagsToSet); + + /** + * Server RPC to set gameplay tags. + * 服务器RPC设置游戏标签。 + * @param TagsToSet The new tags to set. 要设置的新标签。 + */ + UFUNCTION(Server, Reliable) + void ServerSetGameplayTags(const FGameplayTagContainer& TagsToSet); + +#pragma endregion + +#pragma region Abstraction + +public: + virtual TScriptInterface GetTrajectoryPredictor() const; + + /** + * Checks if the character is crouching. + * 检查角色是否在蹲伏。 + * @return True if crouching. 如果在蹲伏返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem", meta=(BlueprintThreadSafe)) + virtual bool IsCrouching() const; + + /** + * Gets the maximum speed. + * 获取最大速度。 + * @return The maximum speed. 最大速度。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual float GetMaxSpeed() const; + + /** + * Gets the scaled capsule radius. + * 获取缩放的胶囊体半径。 + * @return The capsule radius. 胶囊体半径。 + */ + virtual float GetScaledCapsuleRadius() const { return 0.0f; }; + + /** + * Gets the scaled capsule half height. + * 获取缩放的胶囊体半高。 + * @return The capsule half height. 胶囊体半高。 + */ + virtual float GetScaledCapsuleHalfHeight() const { return 0.0f; }; + + /** + * Gets the maximum acceleration. + * 获取最大加速度。 + * @return The maximum acceleration. 最大加速度。 + */ + virtual float GetMaxAcceleration() const; + + /** + * Gets the maximum braking deceleration. + * 获取最大制动减速度。 + * @return The maximum braking deceleration. 最大制动减速度。 + */ + virtual float GetMaxBrakingDeceleration() const { return 0; }; + + /** + * Gets the walkable floor Z value. + * 获取可行走地面Z值。 + * @return The walkable floor Z. 可行走地面Z值。 + */ + virtual float GetWalkableFloorZ() const { return 0; } + + /** + * Gets the gravity Z value. + * 获取重力Z值。 + * @return The gravity Z. 重力Z值。 + */ + virtual float GetGravityZ() const { return 0; } + + /** + * Gets the skeletal mesh component. + * 获取骨骼网格组件。 + * @return The skeletal mesh component. 骨骼网格组件。 + */ + virtual USkeletalMeshComponent* GetMesh() const { return nullptr; }; + + /** Returns true if currently moving on the ground (e.g. walking or driving) */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", meta=(BlueprintThreadSafe)) + virtual bool IsMovingOnGround() const; + +#pragma endregion + +#pragma region Locomotion + + /** + * Gets the locomotion state. + * 获取运动状态。 + * @return The locomotion state. 运动状态。 + */ + const FGMS_LocomotionState& GetLocomotionState() const; + + /** + * Gets the locomotion mode. + * 获取运动模式。 + * @return The locomotion mode. 运动模式。 + */ + const FGameplayTag& GetLocomotionMode() const; + + const FGMS_MovementBaseState& GetMovementBase() const; + + /** + * Sets the locomotion mode. + * 设置运动模式。 + * @param NewLocomotionMode The new locomotion mode. 新运动模式。 + */ + void SetLocomotionMode(const FGameplayTag& NewLocomotionMode); + +protected: + /** + * Sets the desired velocity yaw angle. + * 设置期望的速度偏航角。 + * @param NewDesiredVelocityYawAngle The new velocity yaw angle. 新速度偏航角。 + */ + void SetDesiredVelocityYawAngle(float NewDesiredVelocityYawAngle); + + /** + * Server RPC to set the desired velocity yaw angle. + * 服务器RPC设置期望的速度偏航角。 + * @param NewDesiredVelocityYawAngle The new velocity yaw angle. 新速度偏航角。 + */ + UFUNCTION(Server, Unreliable) + void ServerSetDesiredVelocityYawAngle(float NewDesiredVelocityYawAngle); + virtual void ServerSetDesiredVelocityYawAngle_Implementation(float NewDesiredVelocityYawAngle); + + /** + * Called when the locomotion mode is replicated. + * 运动模式复制时调用。 + * @param PreviousLocomotionMode The previous locomotion mode. 之前的运动模式。 + */ + UFUNCTION() + virtual void OnReplicated_LocomotionMode(const FGameplayTag& PreviousLocomotionMode); + + /** + * Called when the locomotion mode changes. + * 运动模式更改时调用。 + * @param PreviousLocomotionMode The previous locomotion mode. 之前的运动模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + void OnLocomotionModeChanged(const FGameplayTag& PreviousLocomotionMode); + virtual void OnLocomotionModeChanged_Implementation(const FGameplayTag& PreviousLocomotionMode); + +#pragma endregion + +#pragma region MovementBase + + virtual void RefreshMovementBase(); + +#pragma endregion + +#pragma region MovementSet + +public: + /** + * Gets the current movement set. + * 获取当前运动集。 + * @return The current movement set. 当前运动集。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + const FGameplayTag& GetMovementSet() const; + + /** + * Gets the current movement definition. + * 获取当前运动定义。 + * @return The movement definition. 运动定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + TSoftObjectPtr GetMovementDefinition() const; + + /** + * Gets the previous movement definition. + * 获取之前的运动定义。 + * @return The previous movement definition. 之前的运动定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + TSoftObjectPtr GetPrevMovementDefinition() const; + + /** + * Gets the current loaded movement definition. + * 获取当前已经加载的运动定义。 + * @return The movement definition. 运动定义。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + const UGMS_MovementDefinition* GetLoadedMovementDefinition() const; + + /** + * Gets the current movement set setting. + * 获取当前运动集设置。 + * @return The movement set setting. 运动集设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual const FGMS_MovementSetSetting& GetMovementSetSetting() const; + + /** + * Gets the current movement state setting. + * 获取当前运动状态设置。 + * @return The movement state setting. 运动状态设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + const FGMS_MovementStateSetting& GetMovementStateSetting() const; + + /** + * Gets the current control setting. + * 获取当前控制设置。 + * @return The control setting. 控制设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + const UGMS_MovementControlSetting_Default* GetControlSetting() const; + + /** + * Gets the number of movement state settings. + * 获取运动状态设置的数量。 + * @return The number of movement state settings. 运动状态设置数量。 + */ + int32 GetNumOfMovementStateSettings() const; + + /** + * Sets the movement set. + * 设置运动集。 + * @param NewMovementSet The new movement set. 新运动集。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", Meta = (AutoCreateRefTerm = "NewMovementSet")) + void SetMovementSet(UPARAM(meta=(Categories="GMS.MovementSet")) + const FGameplayTag& NewMovementSet); + + /** + * Set a new movement definition, refreshing the movement set. + * 设置新的运动定义,刷新运动集。 + * @param NewDefinition The new movement definition. 新运动定义。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void SetMovementDefinition(TSoftObjectPtr NewDefinition); + + /** + * Set a new movement definition, refreshing the movement set. + * 设置新的运动定义,刷新运动集。 + * @details This allowed locally quick switch movement set without networking involved. 此函数允许快速在本地切换运动集,而无需涉及网络。 + * @param NewDefinition The new movement definition. 新运动定义。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void LocalSetMovementDefinition(TSoftObjectPtr NewDefinition); + + /** + * Pushes a new movement definition, refreshing the movement set. + * 推送新的运动定义,刷新运动集。 + * @param NewDefinition The new movement definition. 新运动定义。 + * @param bPopCurrent Whether to pop the current definition. 是否弹出当前定义。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", meta=(DeprecatedFunction, DeprecationMessage="Use SetMovementDefinition instead.")) + void PushAvailableMovementDefinition(TSoftObjectPtr NewDefinition, bool bPopCurrent = true); + + /** + * Removes the last movement definition, refreshing the movement set. + * 移除最后一个运动定义,刷新运动集。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", meta=(DeprecatedFunction, DeprecationMessage="No longer needed!")) + void PopAvailableMovementDefinition(); + +private: + void InternalSetMovementDefinition(const TSoftObjectPtr NewDefinition, bool bSendRpc = true); + + /** + * Client RPC to set a movement definition. + * 客户端RPC设置运动定义。 + * @param NewDefinition The new movement definition. 新运动定义。 + */ + UFUNCTION(Client, Reliable) + void ClientSetMovementDefinition(const TSoftObjectPtr& NewDefinition); + void ClientSetMovementDefinition_Implementation(const TSoftObjectPtr& NewDefinition); + + /** + * Server RPC to set a movement definition. + * 服务器RPC设置运动定义。 + * @param NewDefinition The new movement definition. 新运动定义。 + */ + UFUNCTION(Server, Reliable) + void ServerSetMovementDefinition(const TSoftObjectPtr& NewDefinition); + virtual void ServerSetMovementDefinition_Implementation(const TSoftObjectPtr& NewDefinition); + + /** + * Sets the movement set with optional RPC. + * 设置运动集,可选择是否发送RPC。 + * @param NewMovementSet The new movement set. 新运动集。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetMovementSet(const FGameplayTag& NewMovementSet, bool bSendRpc); + + /** + * Client RPC to set the movement set. + * 客户端RPC设置运动集。 + * @param NewMovementSet The new movement set. 新运动集。 + */ + UFUNCTION(Client, Reliable) + void ClientSetMovementSet(const FGameplayTag& NewMovementSet); + + /** + * Server RPC to set the movement set. + * 服务器RPC设置运动集。 + * @param NewMovementSet The new movement set. 新运动集。 + */ + UFUNCTION(Server, Reliable) + void ServerSetMovementSet(const FGameplayTag& NewMovementSet); + virtual void ServerSetMovementSet_Implementation(const FGameplayTag& NewMovementSet); + +protected: + /** + * Called when movement definition are replicated. + * 运动定义复制时调用。 + */ + UFUNCTION() + virtual void OnReplicated_MovementDefinition(); + + /** + * Called when the movement set is replicated. + * 运动集复制时调用。 + * @param PreviousMovementSet The previous movement set. 之前的运动集。 + */ + UFUNCTION() + void OnReplicated_MovementSet(const FGameplayTag& PreviousMovementSet); + + /** + * Updates the current movement set setting. + * 更新当前运动集设置。 + */ + virtual void RefreshMovementSetSetting(); + + /** + * Updates the current control setting. + * 更新当前控制设置。 + */ + virtual void RefreshControlSetting(); + + /** + * Updates the current movement state setting. + * 更新当前运动状态设置。 + */ + virtual void RefreshMovementStateSetting(); + + /** + * Applies the current movement settings. + * 应用当前运动设置。 + */ + virtual void ApplyMovementSetting(); + + /** + * Called when the movement set changes. + * 运动集更改时调用。 + * @param PreviousMovementSet The previous movement set. 之前的运动集。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + void OnMovementSetChanged(const FGameplayTag& PreviousMovementSet); + virtual void OnMovementSetChanged_Implementation(const FGameplayTag& PreviousMovementSet); + +#pragma endregion + +#pragma region MovementState + +public: + /** + * Gets the desired movement state. + * 获取期望的运动状态。 + * @return The desired movement state. 期望的运动状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual const FGameplayTag& GetDesiredMovementState() const; + + /** + * Sets the desired movement state. + * 设置期望的运动状态。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", Meta = (DisplayName="Set Desired Movement State", AutoCreateRefTerm = "NewDesiredMovement")) + virtual void SetDesiredMovement(UPARAM(meta=(Categories="GMS.MovementState")) + const FGameplayTag& NewDesiredMovement); + + /** + * Cycles through desired movement states. + * 循环切换期望的运动状态。 + * @param bForward Whether to cycle forward. 是否向前循环。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + virtual void CycleDesiredMovementState(bool bForward = true); + + /** + * Gets the current movement state. + * 获取当前的运动状态。 + * @return The current movement state. 当前的运动状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual const FGameplayTag& GetMovementState() const; + + /** + * Gets the speed level. + * 获取速度级别。 + * @return The speed level. 速度级别。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + int32 GetSpeedLevel() const; + + virtual float GetMappedMovementSpeedLevel(float Speed) const; + +protected: + /** + * Called when the movement state changes. + * 运动状态更改时调用。 + * @param PreviousMovementState The previous movement state. 之前的运动状态。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + void OnMovementStateChanged(const FGameplayTag& PreviousMovementState); + virtual void OnMovementStateChanged_Implementation(const FGameplayTag& PreviousMovementState); + +#pragma endregion + +#pragma region Input + +public: + /** + * Gets the character movement intent. + * 获取角色移动意图。 + * @return The movement intent vector. 移动意图向量。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual FVector GetMovementIntent() const; + + /** + * Applies turn input at a specified rate. + * 以指定速率应用转向输入。 + * @param Direction The turn direction. 转向方向。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + void TurnAtRate(float Direction); + +protected: + /** + * Refreshes input handling. + * 刷新输入处理。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshInput(float DeltaTime); +#pragma endregion + +#pragma region ViewSystem + +public: + /** + * Gets the view state. + * 获取视图状态。 + * @return The view state. 视图状态。 + */ + const FGMS_ViewState& GetViewState() const; + +protected: + /** + * Sets the replicated view rotation. + * 设置复制的视图旋转。 + * @param NewViewRotation The new view rotation. 新视图旋转。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + virtual void SetReplicatedViewRotation(const FRotator& NewViewRotation, bool bSendRpc); + + /** + * Server RPC to set the replicated view rotation. + * 服务器RPC设置复制的视图旋转。 + * @param NewViewRotation The new view rotation. 新视图旋转。 + */ + UFUNCTION(Server, Unreliable) + virtual void ServerSetReplicatedViewRotation(const FRotator& NewViewRotation); + virtual void ServerSetReplicatedViewRotation_Implementation(const FRotator& NewViewRotation); + + /** + * Called when the view rotation is replicated. + * 视图旋转复制时调用。 + */ + UFUNCTION() + virtual void OnReplicated_ReplicatedViewRotation(); + +#pragma endregion + +#pragma region Rotation Mode + + /** + * Gets the desired rotation mode. + * 获取期望的旋转模式。 + * @return The desired rotation mode. 期望的旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual const FGameplayTag& GetDesiredRotationMode() const; + + /** + * Sets the desired rotation mode. + * 设置期望的旋转模式。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem", Meta = (AutoCreateRefTerm = "NewDesiredRotationMode")) + virtual void SetDesiredRotationMode(UPARAM(meta=(Categories="GMS.RotationMode")) + const FGameplayTag& NewDesiredRotationMode); + + /** + * Gets the current rotation mode. + * 获取当前的旋转模式。 + * @return The current rotation mode. 当前的旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + virtual const FGameplayTag& GetRotationMode() const; + + /** + * Called when the rotation mode changes. + * 旋转模式更改时调用。 + * @param PreviousRotationMode The previous rotation mode. 之前的旋转模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + void OnRotationModeChanged(const FGameplayTag& PreviousRotationMode); + virtual void OnRotationModeChanged_Implementation(const FGameplayTag& PreviousRotationMode); + +#pragma endregion + +#pragma region OverlayMode + +public: + /** + * Gets the current overlay mode. + * 获取当前的覆盖模式。 + * @return The current overlay mode. 当前的覆盖模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|MovementSystem") + const FGameplayTag& GetOverlayMode() const; + + /** + * Sets the overlay mode. + * 设置覆盖模式。 + * @param NewOverlayMode The new overlay mode. 新覆盖模式。 + */ + UFUNCTION(BlueprintCallable, Category = "GMS|MovementSystem", Meta = (AutoCreateRefTerm = "NewOverlayMode", Categories="GMS.OverlayMode")) + void SetOverlayMode(const FGameplayTag& NewOverlayMode); + +private: + /** + * Sets the overlay mode with optional RPC. + * 设置覆盖模式,可选择是否发送RPC。 + * @param NewOverlayMode The new overlay mode. 新覆盖模式。 + * @param bSendRpc Whether to send RPC. 是否发送RPC。 + */ + void SetOverlayMode(const FGameplayTag& NewOverlayMode, bool bSendRpc); + + /** + * Client RPC to set the overlay mode. + * 客户端RPC设置覆盖模式。 + * @param NewOverlayMode The new overlay mode. 新覆盖模式。 + */ + UFUNCTION(Client, Reliable) + void ClientSetOverlayMode(const FGameplayTag& NewOverlayMode); + + /** + * Server RPC to set the overlay mode. + * 服务器RPC设置覆盖模式。 + * @param NewOverlayMode The new overlay mode. 新覆盖模式。 + */ + UFUNCTION(Server, Reliable) + void ServerSetOverlayMode(const FGameplayTag& NewOverlayMode); + + /** + * Called when the overlay mode is replicated. + * 覆盖模式复制时调用。 + * @param PreviousOverlayMode The previous overlay mode. 之前的覆盖模式。 + */ + UFUNCTION() + void OnReplicated_OverlayMode(const FGameplayTag& PreviousOverlayMode); + +protected: + /** + * Called when the overlay mode changes. + * 覆盖模式更改时调用。 + * @param PreviousOverlayMode The previous overlay mode. 之前的覆盖模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "GMS|MovementSystem") + void OnOverlayModeChanged(const FGameplayTag& PreviousOverlayMode); +#pragma endregion + +#pragma region DistanceMatching + +public: + /** + * Gets parameters for predicting ground movement stop location. + * 获取预测地面运动停止位置的参数。 + * @return The stop location parameters. 停止位置参数。 + */ + virtual FGMS_PredictGroundMovementStopLocationParams GetPredictGroundMovementStopLocationParams() const + { + return FGMS_PredictGroundMovementStopLocationParams(); + }; + + /** + * Gets parameters for predicting ground movement pivot location. + * 获取预测地面运动枢轴位置的参数。 + * @return The pivot location parameters. 枢轴位置参数。 + */ + virtual FGMS_PredictGroundMovementPivotLocationParams GetPredictGroundMovementPivotLocationParams() const + { + return FGMS_PredictGroundMovementPivotLocationParams(); + }; +#pragma endregion + +#pragma region Properties + + /** + * Event for locomotion mode changes. + * 运动模式更改事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FLocomotionModeChangedSignature OnLocomotionModeChangedEvent; + + /** + * Event for movement set changes. + * 运动集更改事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FMovementSetChangedSignature OnMovementSetChangedEvent; + + /** + * Event for rotation mode changes. + * 旋转模式更改事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FRotationModeChangedSignature OnRotationModeChangedEvent; + + /** + * Event for movement state changes. + * 运动状态更改事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FMovementChangedSignature OnMovementStateChangedEvent; + + /** + * Event for overlay mode changes. + * 覆盖模式更改事件。 + */ + UPROPERTY(BlueprintAssignable, Category="Event") + FOverlayModeChangedSignature OnOverlayModeChangedEvent; + +protected: +#if WITH_EDITORONLY_DATA + /** + * Available movement definitions for the component. + * 组件可用的运动定义。 + * @note Lower definitions have higher priority. 较低的定义具有较高优先级。 + * @details Queries matching MovementSetSetting from bottom to top when MovementSet changes. 当运动集更改时,从下到上查询匹配的运动集设置。 + * @attention Used to dynamically add/remove movement sets at runtime (e.g., equipping a Greatsword). 用于在运行时动态添加/移除运动集(例如装备大剑)。 + */ + UE_DEPRECATED(1.5, "deprecated and will be removed in 1.6") + UPROPERTY(EditAnywhere, Category="Advanced", meta=(EditCondition=false, DeprecatedProperty, DeprecationMessage="Deprecated in faver of SetMovementDefinition.")) + TArray> MovementDefinitions; +#endif + /** + * Current selected movement definition. + * 当前选择的运动定义。 + */ + UPROPERTY(EditAnywhere, Category="Settings|Definitions", ReplicatedUsing=OnReplicated_MovementDefinition) + TSoftObjectPtr MovementDefinition{nullptr}; + + UPROPERTY(VisibleInstanceOnly, Category="Settings|Definitions") + TSoftObjectPtr PrevMovementDefinition{nullptr}; + + /** + * Current selected movement set setting. + * 当前选择的运动集设置。 + */ + UPROPERTY(VisibleInstanceOnly, Category="Settings|Definitions") + FGMS_MovementSetSetting MovementSetSetting; + + /** + * Current selected control setting. + * 当前选择的控制设置。 + */ + UPROPERTY(VisibleInstanceOnly, Category="Settings|Definitions") + TObjectPtr ControlSetting; + + /** + * Current selected movement state setting. + * 当前选择的运动状态设置。 + */ + UPROPERTY(VisibleInstanceOnly, Category="Settings|Definitions") + FGMS_MovementStateSetting MovementStateSetting; + + /** + * Current movement set. + * 当前的运动集。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings", ReplicatedUsing = "OnReplicated_MovementSet", meta=(Categories="GMS.MovementSet")) + FGameplayTag MovementSet{FGameplayTag::EmptyTag}; + + /** + * Will check and adjust the desired rotation mode based on whether the current movement state settings allows it. + * 若勾选,当运动状态/旋转模式变更时,会先检查当前运动状态设置是否允许“DesiredRotationMode”,并进行调整。 + * @details Uncheck if you want decouple movement state from rotation mode(for example:sprinting in view direction) + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + bool bRespectAllowedRotationModesSettings{true}; + + /** + * Current gameplay tags owned by the component. + * 组件拥有的当前游戏标签。 + * @note GameplayTagsProvider is preferred. 更推荐使用GameplayTagsProvider。 + */ + UPROPERTY(EditAnywhere, Replicated, Category="Settings|GameplayTags") + FGameplayTagContainer OwnedTags; + + /** + * Optional object providing gameplay tags. + * 提供游戏标签的可选对象。 + * @note Can integrate with Gameplay Ability System (GAS) by setting an Ability System Component as the provider. 可通过将能力系统组件设置为提供者与游戏能力系统(GAS)集成。 + */ + UPROPERTY(VisibleAnywhere, Category="Settings|GameplayTags") + TObjectPtr GameplayTagsProvider{nullptr}; + + /** + * Tags that block grounded rotation. + * 阻止地面旋转的标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|GameplayTags") + FGameplayTagContainer GroundedRotationBlockingTags; + + /** + * Tags that block in-air rotation. + * 阻止空中旋转的标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|GameplayTags") + FGameplayTagContainer InAirRotationBlockingTags; + + /** + * Current locomotion mode. + * 当前的运动模式。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", ReplicatedUsing = "OnReplicated_LocomotionMode", meta=(Categories="GMS.LocomotionMode")) + FGameplayTag LocomotionMode{GMS_MovementModeTags::Grounded}; + + /** + * Current overlay mode. + * 当前的覆盖模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "State", ReplicatedUsing = "OnReplicated_OverlayMode") + FGameplayTag OverlayMode{GMS_OverlayModeTags::Default}; + +public: + /** + * Animation graph settings for different skeletons or layers. + * 用于不同骨架或层的动画图设置。 + * @note Useful for projects with multiple skeletons or custom animation layers without runtime retargeting. 适用于具有多个骨架或不需要运行时重定向的自定义动画层的项目。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings|Animation") + TObjectPtr AnimGraphSetting{nullptr}; + +protected: + /** + * Desired velocity yaw angle, clamped between -180 and 180 degrees. + * 期望的速度偏航角,限制在-180到180度之间。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", Transient, Replicated, Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float DesiredVelocityYawAngle{0.0f}; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "State", Transient) + FGMS_MovementBaseState MovementBase; + /** + * Replicated raw view rotation, in world or movement base space. + * 复制的原始视图旋转,在世界或运动基础空间中。 + * @note Use FGMS_ViewState::Rotation for network smoothing. 使用FGMS_ViewState::Rotation进行网络平滑。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", Transient, ReplicatedUsing = "OnReplicated_ReplicatedViewRotation") + FRotator ReplicatedViewRotation{ForceInit}; + + /** + * Current locomotion state. + * 当前的运动状态。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State", Transient) + FGMS_LocomotionState LocomotionState; + + /** + * Current view state. + * 当前的视图状态。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", Transient) + FGMS_ViewState ViewState; + + /** + * Main animation instance (weak reference). + * 主动画实例(弱引用)。 + */ + UPROPERTY(VisibleInstanceOnly, Category="State", Transient, meta=(ShowInnerProperties)) + TWeakObjectPtr MainAnimInstance{nullptr}; + + /** + * Animation instance. + * 动画实例。 + */ + UPROPERTY() + TObjectPtr AnimationInstance{nullptr}; + + /** + * Owning pawn. + * 拥有该组件的Pawn。 + */ + UPROPERTY(Transient) + TObjectPtr OwnerPawn{nullptr}; + +#pragma endregion + +#if WITH_EDITOR + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MoverMovementSystemComponent.h b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MoverMovementSystemComponent.h new file mode 100644 index 0000000..52f2fe4 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/GMS_MoverMovementSystemComponent.h @@ -0,0 +1,546 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_MovementSystemComponent.h" +#include "MoverSimulationTypes.h" +#include "GMS_MoverMovementSystemComponent.generated.h" + +class UMoverTrajectoryPredictor; +class UNavMoverComponent; +class UGMS_InputProducer; + +/** + * Movement system component for mover-based movement (work in progress). + * 基于Mover的运动系统组件(开发中)。 + * @note Not recommended for use until production ready. 在标记为生产就绪之前不建议使用。 + */ +UCLASS(ClassGroup=GMS, BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent), DisplayName="GMS Movement System Component(Mover)") +class GENERICMOVEMENTSYSTEM_API UGMS_MoverMovementSystemComponent : public UGMS_MovementSystemComponent, public IMoverInputProducerInterface +{ + GENERATED_BODY() + +public: + /** + * Constructor with object initializer. + * 使用对象初始化器构造函数。 + */ + UGMS_MoverMovementSystemComponent(const FObjectInitializer& ObjectInitializer); + + /** + * Gets lifetime replicated properties. + * 获取生命周期复制属性。 + * @param OutLifetimeProps The lifetime properties. 生命周期属性。 + */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Produces input for the mover simulation. + * 为Mover模拟生成输入。 + * @param SimTimeMs Simulation time in milliseconds. 模拟时间(毫秒)。 + * @param InputCmdResult The input command context (output). 输入命令上下文(输出)。 + */ + virtual void ProduceInput_Implementation(int32 SimTimeMs, FMoverInputCmdContext& InputCmdResult) override; + + /** + * Called every frame. + * 每帧调用。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + * @param TickType The type of tick. tick类型。 + * @param ThisTickFunction The tick function. tick函数。 + */ + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + /** + * Called before the mover simulation tick. + * Mover模拟tick前调用。 + * @param TimeStep The mover time step. Mover时间步。 + * @param InputCmd The input command context. 输入命令上下文。 + */ + UFUNCTION() + virtual void OnMoverPreSimulationTick(const FMoverTimeStep& TimeStep, const FMoverInputCmdContext& InputCmd); + +protected: + /** + * The mover component. + * Mover组件。 + */ + UPROPERTY() + TObjectPtr MoverComponent{nullptr}; + + + UPROPERTY() + TObjectPtr TrajectoryPredictor{nullptr}; + + /** + * The skeletal mesh component. + * 骨骼网格组件。 + */ + UPROPERTY() + TObjectPtr MeshComponent{nullptr}; + + /** + * Handles navigation movement data and functions. + * 处理导航运动数据和函数。 + */ + UPROPERTY() + TObjectPtr NavMoverComponent{nullptr}; + + /** + * Maps movement modes to gameplay tags. + * 将运动模式映射到游戏标签。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings|GameplayTags", meta=(Categories="GMS.LocomotionMode")) + TMap MovementModeToTagMapping; + + /** + * Initializes the component. + * 初始化组件。 + */ + virtual void InitializeComponent() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void BeginPlay() override; + + /** + * Called when the game ends. + * 游戏结束时调用。 + * @param EndPlayReason The reason for ending. 结束原因。 + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * Handle for the movement state modifier (e.g., crouching). + * 运动状态修饰符的句柄(例如蹲伏)。 + */ + FMovementModifierHandle MovementStateModiferHandle; + + /** + * Called when the mover movement mode changes. + * Mover运动模式更改时调用。 + * @param PreviousMovementModeName The previous movement mode name. 之前的运动模式名称。 + * @param NewMovementModeName The new movement mode name. 新运动模式名称。 + */ + UFUNCTION() + virtual void OnMoverMovementModeChanged(const FName& PreviousMovementModeName, const FName& NewMovementModeName); + + /** + * Applies movement settings. + * 应用运动设置。 + */ + virtual void ApplyMovementSetting() override; + +#pragma region ViewSystem + /** + * Refreshes the view system. + * 刷新视图系统。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshView(float DeltaTime); + + /** + * Server RPC to set replicated view rotation. + * 服务器RPC设置复制的视图旋转。 + * @param NewViewRotation The new view rotation. 新视图旋转。 + */ + virtual void ServerSetReplicatedViewRotation_Implementation(const FRotator& NewViewRotation) override; +#pragma endregion + +#pragma region Locomotion + +public: + virtual TScriptInterface GetTrajectoryPredictor() const override; + /** + * Checks if the character is crouching. + * 检查角色是否在蹲伏。 + * @return True if crouching. 如果在蹲伏返回true。 + */ + virtual bool IsCrouching() const override; + + /** + * Gets the maximum speed. + * 获取最大速度。 + * @return The maximum speed. 最大速度。 + */ + virtual float GetMaxSpeed() const override; + + /** + * Gets the scaled capsule radius. + * 获取缩放的胶囊体半径。 + * @return The capsule radius. 胶囊体半径。 + */ + virtual float GetScaledCapsuleRadius() const override; + + /** + * Gets the scaled capsule half height. + * 获取缩放的胶囊体半高。 + * @return The capsule half height. 胶囊体半高。 + */ + virtual float GetScaledCapsuleHalfHeight() const override; + + /** + * Gets the maximum acceleration. + * 获取最大加速度。 + * @return The maximum acceleration. 最大加速度。 + */ + virtual float GetMaxAcceleration() const override; + + /** + * Gets the maximum braking deceleration. + * 获取最大制动减速度。 + * @return The maximum braking deceleration. 最大制动减速度。 + */ + virtual float GetMaxBrakingDeceleration() const override; + + /** + * Gets the walkable floor Z value. + * 获取可行走地面Z值。 + * @return The walkable floor Z. 可行走地面Z值。 + */ + virtual float GetWalkableFloorZ() const override; + + /** + * Gets the gravity Z value. + * 获取重力Z值。 + * @return The gravity Z. 重力Z值。 + */ + virtual float GetGravityZ() const override; + + /** + * Gets the skeletal mesh component. + * 获取骨骼网格组件。 + * @return The skeletal mesh component. 骨骼网格组件。 + */ + virtual USkeletalMeshComponent* GetMesh() const override; + + virtual bool IsMovingOnGround() const override; + +protected: + /** + * Early refresh for locomotion. + * 运动的早期刷新。 + */ + virtual void RefreshLocomotionEarly(); + + /** + * Refreshes locomotion state. + * 刷新运动状态。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshLocomotion(float DeltaTime); + + /** + * Refreshes dynamic movement state. + * 刷新动态运动状态。 + */ + virtual void RefreshDynamicMovementState(); + + /** + * Late refresh for locomotion. + * 运动的后期刷新。 + * @param DeltaTime Time since last frame. 上一帧以来的时间。 + */ + virtual void RefreshLocomotionLate(float DeltaTime); +#pragma endregion + +#pragma region Rotation System + +public: + /** + * Refreshes the target yaw angle using locomotion rotation. + * 使用运动旋转刷新目标偏航角。 + */ + void RefreshTargetYawAngleUsingActorRotation(); + + /** + * Sets the target yaw angle. + * 设置目标偏航角。 + * @param TargetYawAngle The target yaw angle. 目标偏航角。 + */ + void SetTargetYawAngle(float TargetYawAngle); + + /** + * Refreshes the view-relative target yaw angle. + * 刷新相对于视图的目标偏航角。 + */ + void RefreshViewRelativeTargetYawAngle(); +#pragma endregion + +#pragma region DistanceMatching + /** + * Gets parameters for predicting ground movement pivot location. + * 获取预测地面运动枢轴位置的参数。 + * @return The pivot location parameters. 枢轴位置参数。 + */ + virtual FGMS_PredictGroundMovementPivotLocationParams GetPredictGroundMovementPivotLocationParams() const override; + + /** + * Gets parameters for predicting ground movement stop location. + * 获取预测地面运动停止位置的参数。 + * @return The stop location parameters. 停止位置参数。 + */ + virtual FGMS_PredictGroundMovementStopLocationParams GetPredictGroundMovementStopLocationParams() const override; +#pragma endregion + +#pragma region MovementState + + virtual void RefreshMovementBase() override; + + /** + * Gets the desired movement state. + * 获取期望的运动状态。 + * @return The desired movement state. 期望的运动状态。 + */ + virtual const FGameplayTag& GetDesiredMovementState() const override; + + /** + * Sets the desired movement state. + * 设置期望的运动状态。 + * @param NewDesiredMovement The new desired movement state. 新的期望运动状态。 + */ + virtual void SetDesiredMovement(const FGameplayTag& NewDesiredMovement) override; + + /** + * Gets the current movement state. + * 获取当前的运动状态。 + * @return The current movement state. 当前的运动状态。 + */ + virtual const FGameplayTag& GetMovementState() const override; + +protected: + /** + * Applies the movement state and related settings. + * 应用运动状态及相关设置。 + * @param NewMovementState The new movement state. 新运动状态。 + */ + virtual void ApplyMovementState(const FGameplayTag& NewMovementState); +#pragma endregion + +#pragma region RotationMode + +public: + /** + * Gets the desired rotation mode. + * 获取期望的旋转模式。 + * @return The desired rotation mode. 期望的旋转模式。 + */ + virtual const FGameplayTag& GetDesiredRotationMode() const override; + + /** + * Sets the desired rotation mode. + * 设置期望的旋转模式。 + * @param NewDesiredRotationMode The new desired rotation mode. 新的期望旋转模式。 + */ + virtual void SetDesiredRotationMode(const FGameplayTag& NewDesiredRotationMode) override; + + /** + * Gets the current rotation mode. + * 获取当前的旋转模式。 + * @return The current rotation mode. 当前的旋转模式。 + */ + virtual const FGameplayTag& GetRotationMode() const override; + +protected: + /** + * Applies the rotation mode and related settings. + * 应用旋转模式及相关设置。 + * @param NewRotationMode The new rotation mode. 新旋转模式。 + */ + virtual void ApplyRotationMode(const FGameplayTag& NewRotationMode); +#pragma endregion + +#pragma region Input + /** + * Requests movement with an intended directional magnitude. + * 请求以指定方向强度移动。 + * @param DesiredIntent The desired movement intent. 期望的移动意图。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + virtual void RequestMoveByIntent(const FVector& DesiredIntent) { CachedMoveInputIntent = DesiredIntent; } + + /** + * Requests movement with a desired velocity. + * 请求以指定速度移动。 + * @param DesiredVelocity The desired velocity. 期望的速度。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + virtual void RequestMoveByVelocity(const FVector& DesiredVelocity) { CachedMoveInputVelocity = DesiredVelocity; } + + /** + * Clears movement requests. + * 清除移动请求。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|MovementSystem") + virtual void ClearMoveRequest() + { + CachedMoveInputIntent = FVector::ZeroVector; + CachedMoveInputVelocity = FVector::ZeroVector; + } + + /** + * Gets the movement intent. + * 获取移动意图。 + * @return The movement intent vector. 移动意图向量。 + */ + virtual FVector GetMovementIntent() const override; + + /** + * Produces input for the simulation frame. + * 为模拟帧生成输入。 + * @param DeltaMs Time delta in milliseconds. 时间增量(毫秒)。 + * @param InputCmd The input command context. 输入命令上下文。 + * @return The produced input command context. 生成的输入命令上下文。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem") + FMoverInputCmdContext OnProduceInput(float DeltaMs, FMoverInputCmdContext InputCmd); + + /** + * Adjusts orientation intent based on context. + * 根据上下文调整方向意图。 + * @param DeltaSeconds Time since last frame. 上一帧以来的时间。 + * @param OrientationIntent The orientation intent. 方向意图。 + * @return The adjusted orientation intent. 调整后的方向意图。 + */ + virtual FVector AdjustOrientationIntent(float DeltaSeconds, const FVector& OrientationIntent) const; + + /** + * Whether to use base-relative movement. + * 是否使用基于基础的移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings|Input") + bool bUseBaseRelativeMovement = true; + + /** + * Whether to maintain the last input orientation after input ceases. + * 输入停止后是否保持最后输入方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings|Input") + bool bMaintainLastInputOrientation = false; + + /** + * Last non-zero movement input. + * 最后非零移动输入。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + FVector LastAffirmativeMoveInput = FVector::ZeroVector; + + /** + * If true, the actor will remain vertical despite any rotation applied to the actor + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=MoverExamples) + bool bShouldRemainVertical = true; + + /** + * Cached movement input intent. + * 缓存的移动输入意图。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="State|Input") + FVector CachedMoveInputIntent = FVector::ZeroVector; + + /** + * Cached movement input velocity. + * 缓存的移动输入速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="State|Input") + FVector CachedMoveInputVelocity = FVector::ZeroVector; + + /** + * Cached turn input. + * 缓存的转向输入。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + FRotator CachedTurnInput = FRotator::ZeroRotator; + + /** + * Cached look input. + * 缓存的观察输入。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + FRotator CachedLookInput = FRotator::ZeroRotator; + + /** + * Current desired movement state. + * 当前期望的运动状态。 + */ + UPROPERTY(EditAnywhere, Category="State|Input", meta=(Categories="GMS.MovementState")) + FGameplayTag DesiredMovementState{GMS_MovementStateTags::Jog}; + + /** + * Current desired rotation mode. + * 当前期望的旋转模式。 + */ + UPROPERTY(EditAnywhere, Category="Settings", meta=(Categories="GMS.RotationMode")) + FGameplayTag DesiredRotationMode{GMS_RotationModeTags::ViewDirection}; + + /** + * Current movement state. + * 当前的运动状态。 + */ + UPROPERTY(VisibleAnywhere, Category="State", ReplicatedUsing=OnMovementStateChanged, meta=(Categories="GMS.MovementState")) + FGameplayTag MovementState{GMS_MovementStateTags::Jog}; + + /** + * Current rotation mode. + * 当前的旋转模式。 + */ + UPROPERTY(VisibleAnywhere, Category="State", ReplicatedUsing=OnRotationModeChanged, meta=(Categories="GMS.RotationMode")) + FGameplayTag RotationMode{GMS_RotationModeTags::ViewDirection}; + +public: + /** + * Whether the jump input was just pressed. + * 跳跃输入是否刚被按下。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + bool bIsJumpJustPressed = false; + + /** + * Whether the jump input is held. + * 跳跃输入是否被持续按住。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + bool bIsJumpPressed = false; + + /** + * Input tags for the component. + * 组件的输入标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Transient, Category="State|Input") + FGameplayTagContainer InputTags; + + /** + * Whether flying is active. + * 飞行是否激活。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + bool bIsFlyingActive = false; + + /** + * Whether to toggle flying. + * 是否切换飞行状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State|Input") + bool bShouldToggleFlying = false; + +private: + /** + * Flag indicating if input is produced in Blueprint. + * 指示是否在蓝图中生成输入的标志。 + */ + uint8 bHasProduceInputInBpFunc : 1; +#pragma endregion + +#if WITH_EDITOR + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/GenericMovementSystem.h b/Plugins/GMS/Source/GenericMovementSystem/Public/GenericMovementSystem.h new file mode 100644 index 0000000..6b12b53 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/GenericMovementSystem.h @@ -0,0 +1,21 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +struct FAutoCompleteCommand; + +class FGenericMovementSystemModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: +#if ALLOW_CONSOLE + void Console_OnRegisterAutoCompleteEntries(TArray& AutoCompleteCommands); +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer.h new file mode 100644 index 0000000..a6ba2f6 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer.h @@ -0,0 +1,160 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_LocomotionStructLibrary.h" +#include "Engine/DataAsset.h" +#include "Animation/AnimInstance.h" +#include "GMS_AnimLayer.generated.h" + +class UGMS_MovementDefinition; +class UGMS_AnimLayer; +class UGMS_MovementSystemComponent; +class UGMS_MainAnimInstance; +class APawn; + +/** + * Base class for animation layer settings. + * 动画层设置的基类。 + */ +UCLASS(Abstract, BlueprintType, EditInlineNew, Const, CollapseCategories) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Gets the override animation layer class. + * 获取覆盖的动画层类。 + * @param OutLayerClass The output animation layer class. 输出的动画层类。 + * @return True if an override class is provided. 如果提供了覆盖类则返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|AnimationLayer") + bool GetOverrideAnimLayerClass(TSubclassOf& OutLayerClass) const; + + /** + * Validates the data for this animation layer setting. + * 验证此动画层设置的数据。 + * @param ErrorText The error message if validation fails. 如果验证失败的错误信息。 + * @return True if data is valid. 如果数据有效则返回true。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|AnimationLayer", meta=(DisplayName="Is Data Valid")) + bool K2_IsDataValid(FText& ErrorText) const; + +#if WITH_EDITOR + +public: + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; + +/** + * Base class for all animation layers. + * 所有动画层的基类。 + * @note Classes inheriting from this must only be linked to GMS_MainAnimInstance (the main animation instance). 从该类继承的类只能链接到GMS_MainAnimInstance(主动画实例)。 + */ +UCLASS(BlueprintType, Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer : public UAnimInstance +{ + GENERATED_BODY() + +protected: + /** + * The owning pawn of this animation layer. + * 此动画层的拥有Pawn。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="State", Transient) + TObjectPtr PawnOwner; + + /** + * Reference to the movement system component. + * 运动系统组件的引用。 + */ + UPROPERTY(Transient) + TObjectPtr MovementSystem; + +private: + /** + * Weak reference to the parent main animation instance. + * 对父主动画实例的弱引用。 + */ + UPROPERTY(VisibleAnywhere, Category="State", Transient) + TWeakObjectPtr Parent; + +public: + /** + * Constructor. + * 构造函数。 + */ + UGMS_AnimLayer(); + + /** + * Gets the parent main animation instance. + * 获取父主动画实例。 + * @return The parent main animation instance. 父主动画实例。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|AnimationLayer", Meta = (BlueprintThreadSafe, ReturnDisplayName = "Parent")) + UGMS_MainAnimInstance* GetParent() const; + + /** + * Called when the animation layer is linked to the main animation instance. + * 当动画层链接到主动画实例时调用。 + * @note Suitable for initialization tasks similar to BeginPlay. 适合执行类似BeginPlay的初始化任务。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|AnimationLayer") + void OnLinked(); + virtual void OnLinked_Implementation(); + + /** + * Called when the animation layer is unlinked from the main animation instance. + * 当动画层从主动画实例取消链接时调用。 + * @note Suitable for cleanup tasks similar to EndPlay. 适合执行类似EndPlay的清理任务。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|AnimationLayer") + void OnUnlinked(); + virtual void OnUnlinked_Implementation(); + + /** + * Applies settings to the animation layer. + * 向动画层应用设置。 + * @param Setting The setting object to apply, cast to the desired type. 要应用的设置对象,可转换为所需类型。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|AnimationLayer") + void ApplySetting(const UGMS_AnimLayerSetting* Setting); + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting); + + /** + * Resets the settings of the animation layer. + * 重置动画层的设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|AnimationLayer") + void ResetSetting(); + virtual void ResetSetting_Implementation(); + + /** + * Initializes the animation. + * 初始化动画。 + */ + virtual void NativeInitializeAnimation() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void NativeBeginPlay() override; + + /** + * Maps animation state names to gameplay tags for checking node relevance. + * 将动画状态名称映射到游戏标签以检查节点相关性。 + * @note Used to determine if an animation state node is active via NodeRelevantTags in the main animation instance. 用于通过主动画实例中的NodeRelevantTags确定动画状态节点是否活跃。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings", meta=(TitleProperty="Tag")) + TArray AnimStateNameToTagMapping; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Additive.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Additive.h new file mode 100644 index 0000000..767582e --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Additive.h @@ -0,0 +1,17 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "GMS_AnimLayer_Additive.generated.h" + +/** + * Base class for additive animation layer settings. + * 附加动画层设置的基类。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Additive : public UGMS_AnimLayerSetting +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay.h new file mode 100644 index 0000000..a041a07 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay.h @@ -0,0 +1,81 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "GMS_AnimLayer_Overlay.generated.h" + + +#pragma region Blend Data Structures + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_PoseBlendSetting +{ + GENERATED_BODY() + virtual ~FGMS_AnimData_PoseBlendSetting() = default; + /** + * The overall adoption of the pose(0~1). + * 姿势的整体采用度(0~1),1是开启,0是关闭。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(ClampMin=0, ClampMax=1, DisplayPriority=0)) + float BlendAmount{1.0f}; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_PoseBlendSetting_TwoParams : public FGMS_AnimData_PoseBlendSetting +{ + GENERATED_BODY() + + /** + * Overall adoption of Montage playing on this slot (0~1) + * 在此槽上播放的Montage的整体采用度(0~1) + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(ClampMin=0, ClampMax=1, DisplayPriority=2)) + float SlotBlendAmount{1.0f}; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_PoseBlendSetting_ThreeParams : public FGMS_AnimData_PoseBlendSetting_TwoParams +{ + GENERATED_BODY() + + /** + * How much to blend the overlay pose with the underlying motion. + * 叠加姿势与底层运动的混合程度。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(ClampMin=0, ClampMax=1, DisplayPriority=1)) + float AdditiveBlendAmount{0.0f}; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_PoseBlendSetting_FourParams : public FGMS_AnimData_PoseBlendSetting_ThreeParams +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(ClampMin=0, ClampMax=1, DisplayPriority=3)) + bool bMeshSpace{true}; +}; + +#pragma endregion + + +/** + * Base class for overlay animation layer settings. + * 叠加动画层设置的基类。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Overlay : public UGMS_AnimLayerSetting +{ + GENERATED_BODY() + +public: + /** + * Checks if the overlay mode is valid. + * 检查叠加模式是否有效。 + * @param NewOverlayMode The overlay mode to check. 要检查的叠加模式。 + * @return True if the overlay mode is valid, false otherwise. 如果叠加模式有效则返回true,否则返回false。 + */ + virtual bool IsValidForOverlayMode(const FGameplayTag& NewOverlayMode) const { return true; }; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.h new file mode 100644 index 0000000..27b3dc4 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelPoseStack.h @@ -0,0 +1,351 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "Animation/AnimExecutionContext.h" +#include "Animation/AnimNodeReference.h" +#include "GameplayTagContainer.h" +#include "GMS_AnimLayer_Overlay.h" +#include "GMS_AnimState.h" +#include "InstancedStruct.h" +#include "StructUtils/InstancedStruct.h" +#include "GMS_AnimLayer_Overlay_ParallelPoseStack.generated.h" + + +#pragma region Settings + +/** + * The body mask type for human body. + * 针对人的身体遮罩。 + * @attention The enum order also controls the override priority.枚举顺序同时控制了姿势覆盖的优先级。 + */ +UENUM(BlueprintType) +enum class EGMS_BodyMask : uint8 +{ + Head, + ArmLeft, + ArmRight, + UpperBody, + LowerBody, + FullBody, + MAX UMETA(Hidden) +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + virtual ~FGMS_AnimData_BodyPose() = default; + + /** + * Gameplay tag query against to main anim instance's relevance tags(the tags representing the relevant/active state of the anim state machine nodes). + * 针对主动画实例的相关性标签的查询,相关性标签指:用于标识动画状态机节点是否激活的标签。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.SM")) + FGameplayTagQuery RelevanceQuery; + + /** + * Gameplay tag query against to the movement system's owned tags. + * 针对运动系统组件所拥有标签的查询。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FGameplayTagQuery TagQuery; + + /** + * The pose sequence. + * 姿势。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr Pose{nullptr}; + + /** + * The pose time. + * 姿势的帧位置。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float PoseExplicitTime{0.0f}; + + UPROPERTY() + int32 Priority = 999; + + virtual EGMS_BodyMask GetBodyPart() const { return EGMS_BodyMask::FullBody; } + + virtual void Reset() + { + Pose = nullptr; + Priority = 999; + }; + + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const + { + }; + + bool IsValid() const { return Pose != nullptr; } + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category="GES", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; + virtual void PreSave(); +#endif +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_Full : public FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_ThreeParams HeadBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_FourParams ArmLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_FourParams ArmRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting HandLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting HandRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_ThreeParams SpineBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_TwoParams PelvisBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_TwoParams LegsBlend; + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::FullBody; } + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_Upper : public FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_ThreeParams HeadBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_FourParams ArmLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_FourParams ArmRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting HandLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting HandRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_ThreeParams SpineBlend; + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::UpperBody; } + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_Head : public FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_ThreeParams Blend; + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::Head; } + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT(meta=(Hidden)) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_Arms : public FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_FourParams Blend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting HandBlend; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_ArmLeft : public FGMS_AnimData_BodyPose_Arms +{ + GENERATED_BODY() + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::ArmLeft; } + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_ArmRight : public FGMS_AnimData_BodyPose_Arms +{ + GENERATED_BODY() + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::ArmRight; } + + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_BodyPose_Lower : public FGMS_AnimData_BodyPose +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_TwoParams PelvisBlend; + + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayAfter="PoseExplicitTime")) + FGMS_AnimData_PoseBlendSetting_TwoParams LegsBlend; + + virtual EGMS_BodyMask GetBodyPart() const override { return EGMS_BodyMask::LowerBody; } + virtual void UpdateLayeringState(FGMS_AnimState_Layering& LayeringState) const override; +}; + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_OverlayModeSetting_ParallelPoseStack +{ + GENERATED_BODY() + virtual ~FGMS_OverlayModeSetting_ParallelPoseStack() = default; + + /** + * Unique tag for this overlay mode. + * 此叠加模式的唯一标签。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.OverlayMode")) + FGameplayTag Tag; + + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr BasePose = nullptr; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> FullBodyPoses; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> UpperBodyPoses; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> LowerBodyPoses; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> ArmLeftPoses; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> ArmRightPoses; + + UPROPERTY(EditAnywhere, Category="GMS") + TArray> HeadPoses; + +#if WITH_EDITORONLY_DATA + virtual void PreSave(); +#endif +}; + +struct GENERICMOVEMENTSYSTEM_API FGMS_BodyPartOverridePolicy +{ + FGMS_BodyPartOverridePolicy(); + + bool CanOverride(EGMS_BodyMask NewPart, EGMS_BodyMask ExistingPart, int32 NewPriority, int32 ExistingPriority) const; + void ApplyCoverage(EGMS_BodyMask BodyPart, TArray>& SelectedPoses, const TInstancedStruct& NewPose, int32 NewPriority) const; + + TMap> FallbackChain; +}; + +/** + * Anim layer setting for ParallelPoseStack overlay system. + * 针对"并行姿势栈"的动画叠加系统设置。 + * @details Similar to PoseStack, but allowing multiple body part has different overlay at the same time.与姿势栈相似,但允许同时让身体的不同部位有不同的姿势。 + */ +UCLASS(NotBlueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Overlay_ParallelPoseStack final : public UGMS_AnimLayerSetting_Overlay +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Category="Settings", meta=(EditCondition=false, EditConditionHides)) + TMap AcceleratedOverlayModes; + +protected: + UPROPERTY(EditAnywhere, Category="Settings", meta=(TitleProperty="Tag")) + TArray OverlayModes; + +#if WITH_EDITORONLY_DATA + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + +#pragma endregion + +/** + * Anim layer implementation for ParallelPoseStack overlay system. + * 针对"并行姿势栈"的动画叠加系统实现。 + */ +UCLASS(Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_Overlay_ParallelPoseStack : public UGMS_AnimLayer +{ + GENERATED_BODY() + +protected: + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) override; + virtual void ResetSetting_Implementation() override; + virtual void NativeInitializeAnimation() override; + virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override; + + const FGMS_OverlayModeSetting_ParallelPoseStack& GetOverlayModeSetting() const; + void UpdateAnim(const FAnimUpdateContext& Context, const FAnimNodeReference& Node, const EGMS_BodyMask& BodyMask, const FGMS_AnimData_BodyPose& BodyPose); + + void SelectPoses(const FGameplayTagContainer& Tags, const FGameplayTagContainer& Nodes); + void UpdateLayeringState(float DeltaSeconds); + void UpdateLayeringSmoothState(float DeltaSeconds); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void BasePose_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void BodyPart_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node, EGMS_BodyMask BodyMask); + + UPROPERTY() + TArray> SelectedBodyPoses; // Indexed by EGMS_BodyPart + + UPROPERTY() + TObjectPtr CurrentSetting = nullptr; + + UPROPERTY(Transient) + FGameplayTag CurrentOverlayMode; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + TObjectPtr BasePose = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + bool bHasValidSetting = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_LayeringSmooth LayeringSmoothState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + bool bArmLeftMeshSpace{false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + bool bArmRightMeshSpace{false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Layering LayeringState; + + FGMS_BodyPartOverridePolicy OverridePolicy; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.h new file mode 100644 index 0000000..aae3048 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_ParallelSequenceStack.h @@ -0,0 +1,381 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "GMS_AnimLayer_Overlay.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "GMS_AnimLayer_Overlay_ParallelSequenceStack.generated.h" + +struct FCachedAnimStateData; + +/** + * Single entry within "GMS_ParallelSequenceStack". + * "GMS_ParallelSequenceStack"中的单个条目 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ParallelSequenceStackEntry +{ + GENERATED_BODY() + + /** + * Gameplay tag query against to the movement system's owned tags. + * 针对运动系统组件所拥有标签的查询。 + * @details This sequence will be considered if the tags matches this query.此序列会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + FGameplayTagQuery TagQuery; + + /** + * Animation sequence for the overlay. + * 叠加的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + TObjectPtr Sequence = nullptr; + + /** + * Blend weight for the overlay. + * 叠加的混合权重。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMax=1, ClampMin=0)) + float BlendWeight = 1.0f; + + /** + * Whether to blend in mesh space. + * 是否在网格空间中混合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + bool MeshSpaceBlend = true; + + /** + * Play mode for the overlay animation. + * 叠加动画的播放模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + EGMS_OverlayPlayMode PlayMode{EGMS_OverlayPlayMode::SequenceEvaluator}; + + /** + * Start position for sequence player mode. + * 序列播放器模式的起始位置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0, EditCondition="PlayMode == EGMS_OverlayPlayMode::SequencePlayer", EditConditionHides)) + float StartPosition = 0.0f; + + /** + * Explicit time for sequence evaluator mode. + * 序列评估器模式的明确时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0, EditCondition="PlayMode == EGMS_OverlayPlayMode::SequenceEvaluator", EditConditionHides)) + float ExplicitTime = 0.0f; + + /** + * Blend mode for the overlay animation. + * 叠加动画的混合模式。 + * @attention Use branch filters for multiple skeletons to improve reusability. 如果项目中使用多个骨架,建议使用分支过滤器以提高复用性。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + EGMS_LayeredBoneBlendMode BlendMode{EGMS_LayeredBoneBlendMode::BlendMask}; + + /** + * Blend mask name for the overlay. + * 叠加的混合遮罩名称。 + * @attention Must be defined in the skeleton first. 必须先在骨架中定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(EditCondition="BlendMode == EGMS_LayeredBoneBlendMode::BlendMask", EditConditionHides)) + FName BlendMaskName; + + /** + * Branch filters for the overlay. + * 叠加的分支过滤器。 + * @attention Refer to documentation for usage details. 请参阅文档了解使用详情。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(EditCondition="BlendMode == EGMS_LayeredBoneBlendMode::BranchFilter", EditConditionHides)) + FGMS_InputBlendPose BranchFilters; + + /** + * Speed to blend to the specified blend weight. + * 混合到指定混合权重的速度。 + * @attention Zero means instant blending. 为零表示立即混合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + float BlendInSpeed = 10.f; + + /** + * Speed to blend back to zero weight. + * 混合回零权重的速度。 + * @attention Zero means instant return to zero. 为零表示立即归零。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + float BlendOutSpeed = 10.f; + + /** + * Indicates if the overlay data is valid. + * 指示叠加数据是否有效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + bool bValid{false}; + + /** + * Equality operator for overlay data. + * 叠加数据的相等比较运算符。 + */ + friend bool operator==(const FGMS_ParallelSequenceStackEntry& Lhs, const FGMS_ParallelSequenceStackEntry& RHS) + { + return Lhs.Sequence == RHS.Sequence + && Lhs.BlendMode == RHS.BlendMode + && Lhs.MeshSpaceBlend == RHS.MeshSpaceBlend + && Lhs.bValid == RHS.bValid; + } + + /** + * Inequality operator for overlay data. + * 叠加数据的不相等比较运算符。 + */ + friend bool operator!=(const FGMS_ParallelSequenceStackEntry& Lhs, const FGMS_ParallelSequenceStackEntry& RHS) + { + return !(Lhs == RHS); + } + +#if WITH_EDITORONLY_DATA + /** + * Validates the overlay animation data. + * 验证叠加动画数据。 + */ + void Validate(); + /** + * Friendly message for displaying in the editor. + * 在编辑器中显示的友好消息。 + */ + UPROPERTY(VisibleAnywhere, Category = "GMS", Meta = (EditCondition = False, EditConditionHides)) + FString EditorMessage; +#endif +}; + +/** + * A single parallel sequence stack + * 堆叠叠加动画数据的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ParallelSequenceStack +{ + GENERATED_BODY() + + /** + * Animation nodes that determine overlay relevance. + * 确定叠加相关性的动画节点。 + * @attention Configure state-to-tag mapping in the main AnimInstance or derived GMS_AnimLayer. 在主AnimInstance或派生的GMS_AnimLayer中配置状态到标签的映射。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(Categories="GMS.SM")) + FGameplayTagContainer TargetAnimNodes; + + /** + * List of potential overlays, with the first matching one selected. + * 潜在叠加列表,第一个匹配的将被选用。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(TitleProperty="EditorMessage")) + TArray Overlays; + +#if WITH_EDITORONLY_DATA + /** + * Friendly name for displaying in the editor. + * 在编辑器中显示的友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category = "GMS", Meta = (EditCondition = False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Parallel Sequence Stacks for certain overlay mode. + * 针对特定叠加模式的并行Sequence栈 + */ +USTRUCT(BlueprintType) +struct FGMS_OverlayModeSetting_ParallelSequenceStack +{ + GENERATED_BODY() + + /** + * Unique tag for this overlay mode. + * 此叠加模式的唯一标签。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.OverlayMode")) + FGameplayTag Tag; + + /** + * List of parallel sequence stack + * 用于并行混合的动画序列栈列表。 + * @attention Later stacks have higher priority; each stack selects one entry from multiple candidates. 越靠后的栈优先级越高;每个栈从多个候选动画中选择一个。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(TitleProperty="EditorFriendlyName")) + TArray Stacks; +}; + +/** + * Struct for the state of a stacked overlay. + * 栈式叠加状态的结构体。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ParallelSequenceStackState +{ + GENERATED_BODY() + + /** + * Indicates if the target animation nodes are relevant. + * 指示目标动画节点是否相关。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bRelevant{false}; + + /** + * Current blend weight of the overlay. + * 叠加的当前混合权重。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float BlendWeight{0.0f}; + + /** + * Speed to blend out the overlay. + * 叠加混合退出的速度。 + */ + float BlendOutSpeed{0.0f}; + + /** + * Overlay animation data. + * 叠加动画数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FGMS_ParallelSequenceStackEntry Overlay; +}; + +/** + * Anim layer setting for ParallelSequenceStack overlay system. + * 针对"并行序列栈"的动画叠加系统设置。 + * @attention Its more recommended to use SequenceStack instead of this one.通常你应该使用序列栈,而不是这个。 + */ +UCLASS(NotBlueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack final : public UGMS_AnimLayerSetting_Overlay +{ + GENERATED_BODY() + +public: + /** + * Map of accelerated overlay modes. + * 加速叠加模式的映射。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(EditCondition=false, EditConditionHides)) + TMap AcceleratedOverlayModes; + +protected: + /** + * List of overlay mode settings. + * 叠加模式设置列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings", meta=(TitleProperty="Tag")) + TArray OverlayModes; + +#if WITH_EDITORONLY_DATA + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + + +/** + * Anim layer implementation for ParallelSequenceStack overlay system. + * 针对"并行序列栈"的动画叠加系统实现。 + */ +UCLASS(Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_Overlay_ParallelSequenceStack : public UGMS_AnimLayer +{ + GENERATED_BODY() + friend UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack; + +public: + /** + * Called when the component begins play. + * 组件开始播放时调用。 + */ + virtual void NativeBeginPlay() override; + + /** + * Updates the animation. + * 更新动画。 + * @param DeltaSeconds Time since the last update. 自上次更新以来的时间。 + */ + virtual void NativeUpdateAnimation(float DeltaSeconds) override; + + /** + * Updates the animation in a thread-safe manner. + * 以线程安全的方式更新动画。 + * @param DeltaSeconds Time since the last update. 自上次更新以来的时间。 + */ + virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override; + + /** + * Applies the specified animation layer setting. + * 应用指定的动画层设置。 + * @param Setting The animation layer setting to apply. 要应用的动画层设置。 + */ + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) override; + + /** + * Resets the animation layer setting. + * 重置动画层设置。 + */ + virtual void ResetSetting_Implementation() override; + +protected: + /** + * Refreshes the relevance of the overlay stacks. + * 刷新叠加堆栈的相关性。 + */ + virtual void RefreshRelevance(); + + /** + * Refreshes the blending of the overlay stacks. + * 刷新叠加堆栈的混合。 + * @param DeltaSeconds Time since the last update. 自上次更新以来的时间。 + */ + virtual void RefreshBlend(float DeltaSeconds); + + /** + * Previous overlay setting. + * 前一个叠加设置。 + */ + UPROPERTY() + TObjectPtr PrevSetting{nullptr}; + + /** + * Previous overlay mode tag. + * 前一个叠加模式标签。 + */ + UPROPERTY() + FGameplayTag PrevOverlayMode; + + /** + * List of overlay stacks. + * 叠加堆栈列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings", meta=(TitleProperty="EditorFriendlyName")) + TArray OverlayStacks; + + /** + * List of overlay stack states. + * 叠加堆栈状态列表。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + TArray OverlayStackStates; + + /** + * Maximum number of overlay layers. + * 最大叠加层数。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State", meta=(ClampMin=4)) + int32 MaxLayers{10}; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_PoseStack.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_PoseStack.h new file mode 100644 index 0000000..6f43df5 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_PoseStack.h @@ -0,0 +1,439 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "GMS_AnimLayer_Overlay.h" +#include "GMS_AnimState.h" +#include "Settings/GMS_SettingObjectLibrary.h" +#include "GMS_AnimLayer_Overlay_PoseStack.generated.h" + +#pragma region Deprecated +/** + * Enum for pose overlay setting types. + * 姿势叠加设置类型的枚举。 + */ +UENUM(BlueprintType) +enum class EGMS_PoseOverlaySettingType: uint8 +{ + Simple, + Layered +}; + +/** + * Struct for simple pose overlay animation data. + * 简单姿势叠加动画数据的结构体。 + */ +USTRUCT() +struct FGMS_AnimData_PoseOverlay_Simple +{ + GENERATED_BODY() + + /** + * Idle pose animation sequence. + * 空闲姿势动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr IdlePose{nullptr}; + + /** + * Explicit time for the idle pose. + * 空闲姿势的明确时间。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + float IdlePoseExplicitTime{0}; + + /** + * Moving pose animation sequence. + * 移动姿势动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr MovingPose{nullptr}; + + /** + * Explicit time for the moving pose. + * 移动姿势的明确时间。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + float MovingPoseExplicitTime{0}; + + /** + * Aiming sweep pose animation sequence. + * 瞄准扫动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr AimingSweepPose{nullptr}; +}; + +/** + * Struct for layered pose overlay anim data. + * 分层姿势叠加动画数据的结构体。 + */ +USTRUCT() +struct FGMS_AnimData_PoseOverlay_Layered +{ + GENERATED_BODY() + + /** + * Gameplay tag query for layered pose. + * 分层姿势的游戏标签查询。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + FGameplayTagQuery TagQuery; + + /** + * Idle pose animation sequence for layered pose. + * 分层姿势的空闲姿势动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr IdlePose{nullptr}; + + /** + * Explicit time for the idle pose in layered pose. + * 分层姿势中空闲姿势的明确时间。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + float IdlePoseExplicitTime{0}; + + /** + * Moving pose animation sequence for layered pose. + * 分层姿势的移动姿势动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr MovingPose{nullptr}; + + /** + * Explicit time for the moving pose in layered pose. + * 分层姿势中移动姿势的明确时间。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + float MovingPoseExplicitTime{0}; + + /** + * Aiming sweep pose animation sequence for layered pose. + * 分层姿势的瞄准扫动画序列。 + */ + UPROPERTY(EditAnywhere, Category = "GMS") + TObjectPtr AimingSweepPose{nullptr}; +}; +#pragma endregion + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_PerBodyPoseBlendSetting +{ + GENERATED_BODY() + virtual ~FGMS_PerBodyPoseBlendSetting() = default; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_ThreeParams HeadBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_FourParams ArmLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_FourParams ArmRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting HandLeftBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting HandRightBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_ThreeParams SpineBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_TwoParams PelvisBlend; + + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_AnimData_PoseBlendSetting_TwoParams LegsBlend; + + //Read layering setting from sequence curves. + void ApplyFromSequence(const UAnimSequence* InSequence, float ExplicitTime); + //Set blend setting to layering state. + virtual void ApplyToLayeringState(FGMS_AnimState_Layering& InLayeringState) const; + //Read setting from layering state. + virtual void ApplyFromLayeringState(const FGMS_AnimState_Layering& InLayeringState); +}; + +/** + * Single entry within GMS_OverlayModeSetting_PoseStack + * GMS_OverlayModeSetting_PoseStack中的单个条目。 + * + */ +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_PoseStackEntry +{ + GENERATED_BODY() + + /** + * Gameplay tag query against to main anim instance's relevance tags(the tags representing the relevant/active state of the anim state machine nodes). + * 针对主动画实例的相关性标签的查询,相关性标签指:用于标识动画状态机节点是否激活的标签。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.SM")) + FGameplayTagQuery RelevanceQuery; + + /** + * Gameplay tag query against to the movement system's owned tags. + * 针对运动系统组件所拥有标签的查询。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FGameplayTagQuery TagQuery; + + /** + * The sequence for this pose. + * 此姿势的动画序列。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr Pose{nullptr}; + + /** + * Controls the blend weight for each body parts. + * 控制每一个身体部位的混合权重。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FGMS_PerBodyPoseBlendSetting PoseBlend; + + /** + * Explicit time for the pose. + * 姿势的明确播放时间。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float ExplicitTime{0}; + + /** + * Aiming sweep pose animation sequence for layered pose. + * 分层姿势的瞄准扫动画序列。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr AimingSweepPose{nullptr}; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = "GMS", meta = (EditCondition = false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Pose overlay setting for specific overlay mode. + * 针对特定动画叠加模式的姿势叠加设置。 + */ +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_OverlayModeSetting_PoseStack +{ + GENERATED_BODY() + + /** + * Unique tag for this overlay mode. + * 此叠加模式的唯一标签。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.OverlayMode")) + FGameplayTag Tag; + + /** + * Base pose animation sequence. + * 基础姿势动画序列。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr BasePose{nullptr}; + + /** + * Potential dynamic poses. + * 潜在的动态poses。 + * @note To ensure smooth pose switching, Avoid using "multi frames sequence with different explicit time setup", favors "single frame sequence with 0 explicit time setup." + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(TitleProperty="EditorFriendlyName")) + TArray Poses; + +#if WITH_EDITORONLY_DATA + /** + * Type of pose overlay setting. + * 姿势叠加设置的类型。 + */ + UE_DEPRECATED (1.5, "deprecated and will be removed in 1.6") + UPROPERTY(EditAnywhere, Category = "Deprecated", meta = (EditCondition = false, EditConditionHides)) + EGMS_PoseOverlaySettingType PoseOverlaySettingType{EGMS_PoseOverlaySettingType::Simple}; + + /** + * Simple pose overlay settings. + * 简单姿势叠加设置。 + */ + UE_DEPRECATED (1.5, "deprecated and will be removed in 1.6") + UPROPERTY(EditAnywhere, Category = "Deprecated", meta = (EditCondition = false, EditConditionHides)) + FGMS_AnimData_PoseOverlay_Simple SimplePoseSetting; + + /** + * Layered pose overlay settings. + * 分层姿势叠加设置。 + */ + UE_DEPRECATED (1.5, "deprecated and will be removed in 1.6") + UPROPERTY(EditAnywhere, Category = "Deprecated", meta = (EditCondition = false, EditConditionHides)) + TArray LayeredPoseSetting; +#endif +}; + + +/** + * Anim layer setting for PoseStack overlay system. + * 针对"姿势栈"的动画叠加系统设置。 + * @details Similar to ALS's layering system, but more dynamic and easy to use. 类似于ALS的叠层系统,但更动态且易于使用。 + */ +UCLASS(NotBlueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Overlay_PoseStack : public UGMS_AnimLayerSetting_Overlay +{ + GENERATED_BODY() + +public: + /** + * Map of accelerated overlay modes. + * 加速叠加模式的映射。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(EditCondition=false, EditConditionHides)) + TMap AcceleratedOverlayModes; + + /** + * Checks if the overlay mode is valid. + * 检查叠加模式是否有效。 + * @param NewOverlayMode The overlay mode to check. 要检查的叠加模式。 + * @return True if the overlay mode is valid, false otherwise. 如果叠加模式有效则返回true,否则返回false。 + */ + virtual bool IsValidForOverlayMode(const FGameplayTag& NewOverlayMode) const override; + +protected: + /** + * List of pose overlay settings. + * 姿势叠加设置列表。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(TitleProperty="Tag")) + TArray OverlayModes; + +#if WITH_EDITOR + +public: + UFUNCTION(BlueprintCallable, CallInEditor, Category = "GMS") + void RunDataMigration(bool bResetDeprecatedSettings = false); + + UFUNCTION(BlueprintCallable, CallInEditor, Category = "GMS") + static void RunDataMigrationFromDefinition(UGMS_MovementDefinition* InDefinition, bool bResetDeprecatedSettings = false); + + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + +/** + * Anim layer implementation for PoseStack overlay system. + * 针对"姿势栈"的动画叠加系统实现。 + */ +UCLASS(Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_Overlay_PoseStack : public UGMS_AnimLayer +{ + GENERATED_BODY() + +public: + /** + * Applies the specified animation layer setting. + * 应用指定的动画层设置。 + * @param Setting The animation layer setting to apply. 要应用的动画层设置。 + */ + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) override; + + /** + * Resets the animation layer setting. + * 重置动画层设置。 + */ + virtual void ResetSetting_Implementation() override; + bool SelectPose(); + + /** + * Updates the animation in a thread-safe manner. + * 以线程安全的方式更新动画。 + * @param DeltaSeconds Time since the last update. 自上次更新以来的时间。 + */ + virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override; + + const FGMS_OverlayModeSetting_PoseStack& GetOverlayModeSetting() const; + + virtual void UpdateLayeringSmoothState(float DeltaSeconds); + +protected: + /** + * Base pose animation sequence. + * 基础姿势动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + TObjectPtr BasePose{nullptr}; + + /** + * Current Selected Pose. + * 当前选择的Pose. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + TObjectPtr Pose{nullptr}; + + UPROPERTY() + TObjectPtr PrevPose{nullptr}; + + /** + * Explicit time for the current pose + * 当前姿势的明确时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + float ExplicitTime{0}; + + /** + * Aiming sweep pose animation sequence. + * 瞄准扫动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + TObjectPtr AimingSweepPose{nullptr}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bValidPose{false}; + + /** + * Indicates if the aiming pose is valid. + * 指示瞄准姿势是否有效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bValidAimingPose{false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bHasValidSetting = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + float LayeringSmoothSpeed{2.0f}; + + /** + * Current layering state. + * 当前分层状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Layering LayeringState; + + /** + * Current smoothed layering state. + * 当前的平滑分层状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_LayeringSmooth LayeringSmoothState; + + /** + * Reference to the pose-based overlay settings. + * 基于姿势的叠加设置的引用。 + */ + UPROPERTY(Transient) + TObjectPtr CurrentSetting; + + UPROPERTY(Transient) + FGameplayTag CurrentOverlayMode; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h new file mode 100644 index 0000000..39036f7 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_Overlay_SequenceStack.h @@ -0,0 +1,276 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer_Overlay.h" +#include "GMS_AnimLayer_Overlay_SequenceStack.generated.h" + +class UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack; +/** + * Single entry within "GMS_OverlayModeSetting_SequenceStack". + * "GMS_OverlayModeSetting_SequenceStack"中的单个条目 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_SequenceStackEntry +{ + GENERATED_BODY() + + /** + * Gameplay tag query against to main anim instance's relevance tags(the tags representing the relevant/active state of the anim state machine nodes). + * 针对主动画实例的相关性标签的查询,相关性标签指:用于标识动画状态机节点是否激活的标签。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(Categories="GMS.SM")) + FGameplayTagQuery RelevanceQuery; + + /** + * Gameplay tag query against to the movement system's owned tags. + * 针对运动系统组件所拥有标签的查询。 + * @details This pose will be considered if the tags matches this query.此姿势会在标签匹配此查询时被考虑。 + * @note Left empty will bypass this filter. 留空则不应用此过滤。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + FGameplayTagQuery TagQuery; + + /** + * Animation sequence for the overlay. + * 叠加的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + TObjectPtr Sequence = nullptr; + + /** + * Play mode for the overlay animation. + * 叠加动画的播放模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + EGMS_OverlayPlayMode PlayMode{EGMS_OverlayPlayMode::SequencePlayer}; + + /** + * Blend weight for the overlay. + * 叠加的混合权重。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMax=1, ClampMin=0)) + float BlendWeight = 1.0f; + + /** + * Speed to blend to the specified blend weight. + * 混合到指定混合权重的速度。 + * @attention Zero means instant blending. 为零表示立即混合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + float BlendInSpeed = 10.f; + + /** + * Speed to blend back to zero weight. + * 混合回零权重的速度。 + * @attention Zero means instant return to zero. 为零表示立即归零。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + float BlendOutSpeed = 10.f; + + /** + * Whether to blend in mesh space. + * 是否在网格空间中混合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + bool MeshSpaceBlend = true; + + /** + * The start time of animation. + * 动画开始播放的事件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + float AnimationTime = 0.0f; + + /** + * Blend mode for the overlay animation. + * 叠加动画的混合模式。 + * @attention Use branch filters for multiple skeletons to improve reusability. 如果项目中使用多个骨架,建议使用分支过滤器以提高复用性。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS") + EGMS_LayeredBoneBlendMode BlendMode{EGMS_LayeredBoneBlendMode::BlendMask}; + + /** + * Blend mask name for the overlay. + * 叠加的混合遮罩名称。 + * @attention Must be defined in the skeleton first. 必须先在骨架中定义。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(EditCondition="BlendMode == EGMS_LayeredBoneBlendMode::BlendMask", EditConditionHides)) + FName BlendMaskName; + + /** + * Branch filters for the overlay. + * 叠加的分支过滤器。 + * @attention Refer to documentation for usage details. 请参阅文档了解使用详情。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(EditCondition="BlendMode == EGMS_LayeredBoneBlendMode::BranchFilter", EditConditionHides)) + FGMS_InputBlendPose BranchFilters; + + /** + * Speed to blend to the specified blend weight. + * 混合到指定混合权重的速度。 + * @attention Zero means instant blending. 为零表示立即混合。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + float BlendTime = 0.2f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(ClampMin=0)) + FName BlendProfile = NAME_None; + + friend bool operator==(const FGMS_SequenceStackEntry& Lhs, const FGMS_SequenceStackEntry& RHS) + { + return Lhs.Sequence == RHS.Sequence + && Lhs.BlendMode == RHS.BlendMode + && Lhs.MeshSpaceBlend == RHS.MeshSpaceBlend; + } + + friend bool operator!=(const FGMS_SequenceStackEntry& Lhs, const FGMS_SequenceStackEntry& RHS) + { + return !(Lhs == RHS); + } + +#if WITH_EDITORONLY_DATA + /** + * Friendly message for displaying in the editor. + * 在编辑器中显示的友好消息。 + */ + UPROPERTY(VisibleAnywhere, Category = "GMS", Meta = (EditCondition = False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + + +/** + * Pose overlay setting for specific overlay mode. + * 针对特定动画叠加模式的姿势叠加设置。 + */ +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_OverlayModeSetting_SequenceStack +{ + GENERATED_BODY() + + /** + * Unique tag for this overlay mode. + * 此叠加模式的唯一标签。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.OverlayMode")) + FGameplayTag Tag; + + /** + * Potential dynamic sequences. + * 潜在的动态sequences。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(TitleProperty="EditorFriendlyName")) + TArray Sequences; +}; + + +/** + * Anim layer setting for SequenceStack overlay system. + * 针对序列栈的动画叠加系统设置。 + * @details Dynamically stacking different anim sequence. 可动态叠加不同的动画序列。 + */ +UCLASS(NotBlueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_Overlay_SequenceStack : public UGMS_AnimLayerSetting_Overlay +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Category="GMS", meta=(EditCondition=false, EditConditionHides)) + TMap AcceleratedOverlayModes; + +protected: + /** + * List of sequence overlay settings. + * 序列叠加设置列表。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(TitleProperty="Tag")) + TArray OverlayModes; + +#if WITH_EDITOR + +public: + UFUNCTION(BlueprintCallable, CallInEditor, Category = "GMS") + void ConvertToSequenceStack(const UGMS_AnimLayerSetting_Overlay_ParallelSequenceStack* Src); + + UFUNCTION(BlueprintCallable, CallInEditor, Category = "GMS") + static void ConvertToSequenceStackFromDefinition(UGMS_MovementDefinition* InDefinition); + + /** + * Called before saving the object. + * 在保存对象之前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; +#endif +}; + + +/** + * + */ +UCLASS() +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_Overlay_SequenceStack : public UGMS_AnimLayer +{ + GENERATED_BODY() + +public: + /** + * Applies the specified animation layer setting. + * 应用指定的动画层设置。 + * @param Setting The animation layer setting to apply. 要应用的动画层设置。 + */ + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) override; + + /** + * Resets the animation layer setting. + * 重置动画层设置。 + */ + virtual void ResetSetting_Implementation() override; + bool SelectSequence(); + + /** + * Updates the animation in a thread-safe manner. + * 以线程安全的方式更新动画。 + * @param DeltaSeconds Time since the last update. 自上次更新以来的时间。 + */ + virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override; + + const FGMS_OverlayModeSetting_SequenceStack& GetOverlayModeSetting() const; + +protected: + /** + * Current Selected Pose. + * 当前选择的Pose. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + FGMS_SequenceStackEntry Definition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bHasValidDefinition = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + float BlendWeight{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + float BlendOutSpeed{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + TObjectPtr BlendProfile{nullptr}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bHasValidSetting = false; + + /** + * Reference to the pose-based overlay settings. + * 基于姿势的叠加设置的引用。 + */ + UPROPERTY(Transient) + TObjectPtr CurrentSetting; + + UPROPERTY(Transient) + FGameplayTag CurrentOverlayMode; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_SkeletalControls.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_SkeletalControls.h new file mode 100644 index 0000000..3edb2d9 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_SkeletalControls.h @@ -0,0 +1,17 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "GMS_AnimLayer_SkeletalControls.generated.h" + +/** + * Base class for skeletal control animation layer settings. + * 骨骼控制动画层设置的基类。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_SkeletalControls : public UGMS_AnimLayerSetting +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States.h new file mode 100644 index 0000000..5cc2ce0 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States.h @@ -0,0 +1,50 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "UObject/Object.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "Settings/GMS_SettingObjectLibrary.h" +#include "GMS_AnimLayer_States.generated.h" + +/** + * Animation data for state-based animation layers. + * 状态动画层的数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData +{ + GENERATED_BODY() + virtual ~FGMS_AnimData() = default; + + /** + * Validates the animation data. + * 验证动画数据。 + */ + virtual void Validate(); + + /** + * Indicates if the animation data is valid. + * 指示动画数据是否有效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GMS", meta=(EditCondition=false, EditConditionHides)) + bool bValid{false}; +}; + +/** + * Base class for state-based animation layer settings. + * 状态动画层设置的基类。 + * @details Inherit this class to create custom state-based animation layer settings. 继承此类以创建自定义状态动画层设置。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_States : public UGMS_AnimLayerSetting +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h new file mode 100644 index 0000000..fc58495 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_States_DefaultLocomotion.h @@ -0,0 +1,1663 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer_States.h" +#include "GMS_AnimState.h" +#include "Animation/AnimExecutionContext.h" +#include "Animation/AnimNodeReference.h" +#include "GMS_AnimLayer_States_DefaultLocomotion.generated.h" + +#pragma region Settings + +UENUM(BlueprintType) +enum class EGMS_JumpStartAnimType : uint8 +{ + Single, + Direction, +}; + +/** + * Animation data for jump-related states. + * 跳跃相关状态的动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Jump : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Animation selection type for the start of a jump. + * 跳跃开始的动画选择类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_JumpStartAnimType JumpStartType{EGMS_JumpStartAnimType::Single}; + + /** + * Animation sequence for the start of a jump. + * 跳跃开始的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="JumpStartType == EGMS_JumpStartAnimType::Single", EditConditionHides)) + TObjectPtr JumpStart = nullptr; + + /** + * Animation sequence for the start of a jump. + * 跳跃开始的动画序列。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="JumpStartType == EGMS_JumpStartAnimType::Direction", EditConditionHides)) + FGMS_Animations_4Direction JumpStarts; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float JumpStartPosition{0}; + + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + // FName JumpStartBlendProfile{TEXT("FastFeet_InstantRoot")}; + + /** + * Animation sequence for looping during jump start. + * 跳跃开始循环的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr JumpStartLoop = nullptr; + + /** + * Animation sequence for the apex of a jump. + * 跳跃顶点的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr JumpApex = nullptr; + + /** + * Animation sequence for looping during fall, required for fall state entry. + * 坠落循环的动画序列,进入坠落状态必需。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr JumpFallLoop = nullptr; + + /** + * Optional animation for transitioning from fall to land, played based on GroundDistance curve. + * 可选的从坠落到落地的动画,根据GroundDistance曲线播放。 + * @example A 2-second animation with a -300 to 0 curve starts 3m from ground, progressing to match InAirState.GroundDistance. + * @示例 2秒动画,曲线值-300到0,距离地面3米时开始播放,随InAirState.GroundDistance推进。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="bEnableGroundPrediction", EditConditionHides)) + TObjectPtr JumpFallLand = nullptr; + + /** + * Enables ground prediction for JumpFallLand animation, disable to save performance if not used. + * 启用JumpFallLand动画的地面预测,禁用以节约性能。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bEnableGroundPrediction{true}; + + /** + * Indicates if JumpStartLoop is valid. + * 指示JumpStartLoop是否有效。 + */ + UPROPERTY(EditAnywhere, Category="GMS", BlueprintReadWrite, meta=(EditCondition=false, EditConditionHides)) + bool bValidJumpStartLoop{false}; + + /** + * Indicates if JumpApex is valid. + * 指示JumpApex是否有效。 + */ + UPROPERTY(EditAnywhere, Category="GMS", BlueprintReadWrite, meta=(EditCondition=false, EditConditionHides)) + bool bValidJumpApex{false}; + + /** + * Indicates if JumpFallLoop is valid. + * 指示JumpFallLoop是否有效。 + */ + UPROPERTY(EditAnywhere, Category="GMS", BlueprintReadWrite, meta=(EditCondition=false, EditConditionHides)) + bool bValidJumpFallLoop{false}; + + /** + * Indicates if JumpFallLand is valid. + * 指示JumpFallLand是否有效。 + */ + UPROPERTY(EditAnywhere, Category="GMS", BlueprintReadWrite, meta=(EditCondition=false, EditConditionHides)) + bool bValidJumpFallLand{false}; + + /** + * Validates the jump animation data. + * 验证跳跃动画数据。 + */ + virtual void Validate() override; +}; + +/** + * Animation data for idle states. + * 空闲状态的动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Idle : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Validates the idle animation data. + * 验证空闲动画数据。 + */ + virtual void Validate() override; + + /** + * Primary idle animation sequence. + * 主空闲动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Idle{nullptr}; + + /** + * Blend time for idle animations. + * 空闲动画的混合时间。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + float BlendTime{0.3f}; + + /** + * Disables idle break animations. + * 禁用空闲打断动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bDisableIdleBreaks{false}; + + /** + * Array of idle break animations. + * 空闲打断动画数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="!bDisableIdleBreaks", EditConditionHides)) + TArray> Idle_Breaks; + + /** + * Fixed delay for idle breaks; if <= 0, determined by number of Idle_Breaks. + * 空闲打断的固定延迟;若<=0,则由Idle_Breaks数量决定。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="!bDisableIdleBreaks", EditConditionHides)) + float IdleBreakDelayTime{0}; + + /** + * Animation for entering crouch state. + * 进入蹲伏状态的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(DisplayName="Entry")) + TObjectPtr CrouchEntry{nullptr}; + + /** + * Animation for exiting crouch state. + * 退出蹲伏状态的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(DisplayName="Exit")) + TObjectPtr CrouchExit{nullptr}; + + /** + * Indicates if crouch animations are valid. + * 指示蹲伏动画是否有效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition=false, EditConditionHides)) + bool bValidCrouchAnim{false}; +}; + +/** + * Enum for start animation types based on view direction. + * 基于视角方向的开始动画类型枚举。 + */ +UENUM(BlueprintType) +enum class EGMS_StartAnimType_ViewDir : uint8 +{ + Direction_4, + Direction_8, +}; + +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Start : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Adjusts animation play rate to match in-game movement speed. + * 调整动画播放速率以匹配游戏内移动速度。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(DisplayAfter="StrideWarping")) + bool bDynamicPlayRate{true}; + + /** + * Clamp range for animation play rate. + * 动画播放速率的限制范围。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="bDynamicPlayRate", EditConditionHides, DisplayAfter="StrideWarping")) + FVector2D PlayRateClamp{0.8, 1.2}; +}; + +/** + * Animation data for start animations based on view direction. + * 基于视角方向的开始动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Start_ViewDirection : public FGMS_AnimData_Start +{ + GENERATED_BODY() + + /** + * Validates the start view direction animation data. + * 验证基于视角方向的开始动画数据。 + */ + virtual void Validate() override; + + /** + * Animation type for view direction, 4 directions preferred. + * 视角方向的动画类型,推荐4方向。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + EGMS_StartAnimType_ViewDir AnimType{EGMS_StartAnimType_ViewDir::Direction_4}; + + /** + * Animations for 4-direction view-based start. + * 基于4方向视角的开始动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="AnimType == EGMS_StartAnimType_ViewDir::Direction_4", EditConditionHides)) + FGMS_Animations_4Direction Animations; + + /** + * Animations for 8-direction view-based start. + * 基于8方向视角的开始动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="AnimType == EGMS_StartAnimType_ViewDir::Direction_8", EditConditionHides)) + FGMS_Animations_8Direction Animations_8Direction; + + /** + * Stride warping settings for start animations. + * 开始动画的步伐扭曲设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="GMS") + FGMS_StrideWarpingSettings StrideWarping; + + /** + * Blend profile for start animations. + * 开始动画的混合配置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FName BlendProfile{TEXT("FastFeet_InstantRoot")}; +}; + +/** + * Enum for start animation types based on velocity direction. + * 基于速度方向的开始动画类型枚举。 + */ +UENUM(BlueprintType) +enum class EGMS_StartAnimType_VelocityDir : uint8 +{ + Reface, + Single, +}; + +/** + * Animation data for start animations based on velocity direction. + * 基于速度方向的开始动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Start_VelocityDirection : public FGMS_AnimData_Start +{ + GENERATED_BODY() + + /** + * Validates the start velocity direction animation data. + * 验证基于速度方向的开始动画数据。 + */ + virtual void Validate() override; + + /** + * Animation type for velocity direction. + * 速度方向的动画类型。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_StartAnimType_VelocityDir AnimType{EGMS_StartAnimType_VelocityDir::Reface}; + + /** + * Animations for forward-facing velocity direction start. + * 前向速度方向的开始动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="AnimType == EGMS_StartAnimType_VelocityDir::Reface", EditConditionHides)) + FGMS_Animations_StartForwardFacing Animations; + + /** + * Single animation for velocity direction start ÓåÓÚÖ²ÓÚÙÉÝÇÓ×ÄϾÙɽºÍÄÚÈÝ + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="AnimType == EGMS_StartAnimType_VelocityDir::Single", EditConditionHides)) + TObjectPtr Animation; + + /** + * Stride warping settings for start animations. + * 开始动画的步伐扭曲设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="GMS") + FGMS_StrideWarpingSettings StrideWarping; + + /** + * Steering settings for start animations. + * 开始动画的转向设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="GMS") + FGMS_SteeringSettings Steering; + + /** + * Blend profile for start animations. + * 开始动画的混合配置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FName BlendProfile{TEXT("FastFeet_InstantRoot")}; +}; + +/** + * Enum for cycle animation types. + * 循环动画类型枚举。 + */ +UENUM(BlueprintType) +enum class EGMS_CycleAnimType : uint8 +{ + Direction_4, + Direction_8, + Single, +}; + +/** + * Animation data for cycle animations. + * 循环动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Cycle : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Validates the cycle animation data. + * 验证循环动画数据。 + */ + virtual void Validate() override; + + /** + * Animation type for cycle, 4 directions preferred. + * 循环动画类型,推荐4方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_CycleAnimType AnimType{EGMS_CycleAnimType::Direction_4}; + + /** + * Animations for 4-direction cycle. + * 4方向循环动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="AnimType == EGMS_CycleAnimType::Direction_4", EditConditionHides)) + FGMS_Animations_4Direction Animations; + + /** + * Animations for 8-direction cycle. + * 8方向循环动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="AnimType == EGMS_CycleAnimType::Direction_8", EditConditionHides)) + FGMS_Animations_8Direction Animations_8Direction; + + /** + * Single animation for cycle. + * 单循环动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="AnimType == EGMS_CycleAnimType::Single", EditConditionHides)) + TObjectPtr Animation; + + /** + * Blend time for cycle animations. + * 循环动画的混合时间。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + float BlendTime{0.25f}; + + /** + * Adjusts animation play rate to match in-game movement speed. + * 调整动画播放速率以匹配游戏内移动速度。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + bool bDynamicPlayRate{true}; + + /** + * Uses root motion delta for play rate if true, otherwise uses animated speed. + * 如果为true,使用根运动增量计算播放速率,否则使用动画速度。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="bDynamicPlayRate", EditConditionHides)) + bool bHasRootMotion{true}; + + /** + * Animated speed for non-root motion animations. + * 非根运动动画的动画速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "cm/s", EditCondition="!bHasRootMotion", EditConditionHides)) + float AnimatedSpeed{350.0f}; + + /** + * Clamp range for dynamic play rate adjustment. + * 动态播放速率调整的限制范围。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(EditCondition="bDynamicPlayRate", EditConditionHides)) + FVector2D PlayRateClamp{0.8f, 1.2f}; + + /** + * Enables stride warping for cycle animations. + * 为循环动画启用步伐扭曲。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + bool bEnableStrideWarping{true}; + + /** + * Blend profile for cycle animations. + * 循环动画的混合配置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FName BlendProfile{TEXT("FastFeet")}; +}; + +/** + * Enum for stop animation types. + * 停止动画类型枚举。 + */ +UENUM(BlueprintType) +enum class EGMS_StopAnimType : uint8 +{ + Direction_4, + Direction_8, + Single, +}; + +/** + * Animation data for stop animations. + * 停止动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Stop : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Validates the stop animation data. + * 验证停止动画数据。 + */ + virtual void Validate() override; + + /** + * Animation type for stop, 4 directions preferred. + * 停止动画类型,推荐4方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_StopAnimType AnimType{EGMS_StopAnimType::Direction_4}; + + /** + * Animations for 4-direction stop. + * 4方向停止动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="AnimType == EGMS_StopAnimType::Direction_4", EditConditionHides)) + FGMS_Animations_4Direction Animations; + + /** + * Single animation for stop. + * 单停止动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="AnimType == EGMS_StopAnimType::Single", EditConditionHides)) + TObjectPtr Animation; + + /** + * Animations for 8-direction stop. + * 8方向停止动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="AnimType == EGMS_StopAnimType::Direction_8", EditConditionHides)) + FGMS_Animations_8Direction Animations_8Direction; +}; + +/** + * Animation data for pivot animations. + * 枢轴动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Pivot : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Validates the pivot animation data. + * 验证枢轴动画数据。 + */ + virtual void Validate() override; + + /** + * Animations for 4-direction pivot. + * 4方向枢轴动画。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + FGMS_Animations_4Direction Animations; + + /** + * Cooldown time for pivot animations. + * 枢轴动画的冷却时间。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + float PivotCooldown{0.2f}; + + /** + * Clamp range for pivot animation play rate. + * 枢轴动画播放速率的限制范围。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + FVector2D PlayRateClamp{0.8, 1.2}; + + /** + * Stride warping settings for pivot animations. + * 枢轴动画的步伐扭曲设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="GMS") + FGMS_StrideWarpingSettings StrideWarping; +}; + +/** + * Animation data for landing animations. + * 着陆动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Land : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Distance-based landing animations, selects closest to vertical speed or falls back to last. + * 基于距离的着陆动画,选择与垂直速度最接近的动画,或回退到最后一个。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(TitleProperty="EditorFriendlyName")) + TArray Lands; + + /** + * Validates the landing animation data. + * 验证着陆动画数据。 + */ + virtual void Validate() override; +}; + +/** + * Animation data for lean animations. + * 倾斜动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_Lean : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Blend space for lean animations. + * 倾斜动画的混合空间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BlendSpace{nullptr}; + + /** + * Maximum left/right lean angle. + * 最大左右倾斜角度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=1, DisplayName="Max left/right lean angle")) + float MaxXLeanAngle{20.0f}; + + /** + * Maximum forward/backward lean angle. + * 最大前后倾斜角度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=1, DisplayName="Max forward/backward lean angle")) + float MaxYLeanAngle{20.f}; + + /** + * Scales lean based on current speed. + * 根据当前速度缩放倾斜。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bScaleBySpeed{true}; + + /** + * Speed range for lean scaling. + * 倾斜缩放的速度范围。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="bScaleBySpeed", EditConditionHides)) + FVector2D SpeedRange{200.0f, 600.0f}; + + /** + * Scale range for lean scaling. + * 倾斜缩放的范围。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="bScaleBySpeed", EditConditionHides)) + FVector2D ScaleRange{0.2f, 1.0f}; + + /** + * Validates the lean animation data. + * 验证倾斜动画数据。 + */ + virtual void Validate() override; +}; + +/** + * Animation data for turn-in-place animations. + * 原地转身动画数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_TurnInPlace : public FGMS_AnimData +{ + GENERATED_BODY() + + /** + * Validates the turn-in-place animation data. + * 验证原地转身动画数据。 + */ + virtual void Validate() override; + + /** + * Animation for left turn. + * 左转动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Left = nullptr; + + /** + * Animation for right turn. + * 右转动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Right = nullptr; + + /** + * Optional animation for 180-degree left turn. + * 可选的180度左转动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Left180{nullptr}; + + /** + * Optional animation for 180-degree right turn. + * 可选的180度右转动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Right180{nullptr}; + + /** + * Root yaw angle threshold for triggering a turn. + * 触发转身的根偏航角阈值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (DisplayName="Root Yaw Angle Threshold", ClampMin = 1, ClampMax = 180, ForceUnits = "deg")) + float RootYawAngleThreshold{10}; + + /** + * View yaw angle threshold for triggering a turn. + * 触发转身的视角偏航角阈值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (DisplayName="View Yaw Angle Threshold", ClampMin = 10, ClampMax = 180, ForceUnits = "deg")) + float ViewYawAngleThreshold{45.0f}; + + /** + * Blend time for turn animations. + * 转身动画的混合时间。 + */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="GMS", Meta = (ClampMin = 0)) + float BlendTime{0.2f}; + + /** + * Angle threshold for selecting 180-degree turn animations. + * 选择180度转身动画的角度阈值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 10, ClampMax = 180, ForceUnits = "deg")) + float Turn180AngleThreshold{130.0f}; + + /** + * Scales turn rate based on view yaw speed. + * 根据视角偏航速度缩放转身速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bScaleTurnRate{true}; + + /** + * Play rate for turn-in-place animations when not aiming. + * 非瞄准时原地转身动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(DisplayName="PlayRate", ForceUnits = "x")) + float PlayRate{1.0f}; + + /** + * Maps turn yaw angle to activation delay. + * 将转身偏航角映射到激活延迟。 + * @attention Zero disables delay. + * @注意 为零禁用延迟。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (DisplayName="Turn Yaw Angle to Activation Delay", ClampMin = 0, ForceUnits="s")) + FVector2D ViewYawAngleToActivationDelay{0.2f, 0.75f}; + + /** + * Reference view yaw speed for scaling turn rate. + * 用于缩放转身速率的参考视角偏航速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(DisplayName="Reference View Yaw Speed", ForceUnits = "deg/s")) + FVector2D ReferenceViewYawSpeed{180.0f, 460.0f}; + + /** + * Play rate range for dynamic turn rate adjustment. + * 动态转身速率调整的播放速率范围。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0.5, ClampMax=10, ForceUnits = "x")) + FVector2D PlayRateRange{1.0f, 1.5f}; + + /** + * Steering settings for turn-in-place animations. + * 原地转身动画的转向设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="GMS") + FGMS_SteeringSettings Steering; + + /** + * Enables turning to face TargetYaw in velocity direction mode. + * 在速度方向模式下启用朝向TargetYaw转身。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bTurnInVelocityDirection{true}; + + /** + * Enables turning when aiming in view direction mode. + * 在视角方向模式下启用瞄准时转身。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bTurnWhenAimingInViewDirection{true}; +}; + +/** + * Animation data for grounded moving states. + * 地面移动状态的动画数据。 + */ +USTRUCT(DisplayName="GMS AnimData Moving States") +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimData_MovingStates +{ + GENERATED_BODY() + + /** + * Gameplay tag for the moving state. + * 移动状态的游戏标签。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(Categories="GMS.MovementState")) + FGameplayTag Tag; + + /** + * Start animation data for view direction. + * 视角方向的开始动画数据。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayName="Start(ViewDirection)", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Start_ViewDirection")) + FInstancedStruct Start_ViewDir_Inst; + + /** + * Start animation data for velocity direction. + * 速度方向的开始动画数据。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayName="Start(VelocityDirection)", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Start_VelocityDirection")) + FInstancedStruct Start_VelocityDir_Inst; + + /** + * Cycle animation data. + * 循环动画数据。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayName="Cycle", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Cycle")) + FInstancedStruct Cycle_Inst; + + /** + * Stop animation data. + * 停止动画数据。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayName="Stop", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Stop")) + FInstancedStruct Stop_Inst; + + /** + * Pivot animation data. + * 枢轴动画数据。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(DisplayName="Pivot", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Pivot")) + FInstancedStruct Pivot_Inst; + + /** + * Equality operator for comparing with a gameplay tag. + * 与游戏标签比较的相等运算符。 + */ + bool operator==(const FGameplayTag& Other) const + { + return Tag == Other; + } + + /** + * Inequality operator for comparing with a gameplay tag. + * 与游戏标签比较的不相等运算符。 + */ + bool operator!=(const FGameplayTag& Other) const + { + return Tag != Other; + } +}; + +/** + * Default (Lyra-like) states animation layer settings.(The basic locomotion) + * 默认(类Lyra)状态动画层设置。(基础运动) + */ +UCLASS(Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_States_Default : public UGMS_AnimLayerSetting_States +{ + GENERATED_BODY() + +public: + /** + * Idle animation data. + * 空闲动画数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim States", meta=(DisplayName="Idle/IdleBreaks", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Idle")) + FInstancedStruct Idle_Inst; + + /** + * Jump and fall animation data. + * 跳跃和坠落动画数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim States", meta=(DisplayName="Jump/Fall", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Jump")) + FInstancedStruct Jump_Inst; + + /** + * Landing animation data. + * 着陆动画数据。 + */ + UPROPERTY(EditAnywhere, Category="Anim States", meta=(DisplayName="Land", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Land")) + FInstancedStruct Land_Inst; + + /** + * Turn-in-place animation data. + * 原地转身动画数据。 + */ + UPROPERTY(EditAnywhere, Category="Anim States", meta=(DisplayName="Turn", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_TurnInPlace")) + FInstancedStruct Turn_Inst; + + /** + * Lean animation data. + * 倾斜动画数据。 + */ + UPROPERTY(EditAnywhere, Category="Anim States", meta=(DisplayName="Lean", BaseStruct = "/Script/GenericMovementSystem.GMS_AnimData_Lean")) + FInstancedStruct Lean_Inst; + + /** + * Array of moving state animation data. + * 移动状态动画数据数组。 + */ + UPROPERTY(EditAnywhere, Category="Anim States", meta = (TitleProperty="Tag")) + TArray MovingStates; + + /** + * Map for accelerated access to moving states. + * 加速访问移动状态的映射。 + */ + UPROPERTY(VisibleAnywhere, Category="Anim States", meta=(EditCondition=false, EditConditionHides)) + TMap AcceleratedMovingStates; + + virtual bool GetOverrideAnimLayerClass_Implementation(TSubclassOf& OutLayerClass) const override; + +#if WITH_EDITOR + /** + * Called before saving the object. + * 在保存对象之前调用。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates the data for editor use. + * 为编辑器使用验证数据。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; +#pragma endregion + +/** + * Native implementation of default (Lyra-like) state animation layer. + * 默认(类Lyra)状态动画层的原生实现。 + * @attention Core logic is implemented in C++ for performance. + * @注意 核心逻辑在C++中实现以提升性能。 + */ +UCLASS(Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_States_DefaultLocomotion : public UGMS_AnimLayer +{ + GENERATED_BODY() + + friend UGMS_AnimLayerSetting_States_Default; + +#pragma region States + +protected: + /** + * Reference to the default state animation settings. + * 默认状态动画设置的引用。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings") + TObjectPtr Setting; + + /** + * Moving states animation data. + * 移动状态动画数据。 + */ + UPROPERTY() + FGMS_AnimData_MovingStates AnimData_MovingStates; + + /** + * Orientation warping settings. + * 方向扭曲设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings") + FGMS_OrientationWarpingSettings OWSettings; + + /** + * Idle animation data. + * 空闲动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Idle AnimData_Idle; + + /** + * Turn-in-place animation data. + * 原地转身动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_TurnInPlace AnimData_TurnInPlace; + + /** + * Start animation data for view direction. + * 视角方向的开始动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Start_ViewDirection AnimData_Start_ViewDirection; + + /** + * Start animation data for velocity direction. + * 速度方向的开始动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Start_VelocityDirection AnimData_Start_VelocityDirection; + + /** + * Cycle animation data. + * 循环动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Cycle AnimData_Cycle; + + /** + * Lean animation data for grounded states. + * 地面状态的倾斜动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Lean AnimData_GroundedLean; + + /** + * Stop animation data. + * 停止动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Stop AnimData_Stop; + + /** + * Pivot animation data. + * 枢轴动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Pivot AnimData_Pivot; + + /** + * Jump animation data. + * 跳跃动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Jump AnimData_Jump; + + /** + * Landing animation data. + * 着陆动画数据。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + FGMS_AnimData_Land AnimData_Land; + + /** + * Idle state data. + * 空闲状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Idle IdleState; + + /** + * Start state data. + * 开始状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Start StartState; + + /** + * Cycle state data. + * 循环状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Cycle CycleState; + + /** + * Stop state data. + * 停止状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Stop StopState; + + /** + * Pivot state data. + * 枢轴状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Pivot PivotState; + + /** + * Turn-in-place state data. + * 原地转身状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_TurnInPlace TurnInPlaceState; + + /** + * Idle break state data. + * 空闲打断状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_IdleBreak IdleBreakState; + + /** + * Enables landing animations. + * 启用着陆动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bEnableLand{false}; + + /** + * Enables start animations. + * 启用开始动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bEnableStart{false}; + + /** + * Enables stop animations. + * 启用停止动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bEnableStop{false}; + + /** + * Enables pivot animations. + * 启用枢轴动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bEnablePivot{false}; + + /** + * Enables jump animations. + * 启用跳跃动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") + bool bEnableJump{false}; + + /** + * Enables fall animations. + * 启用坠落动画。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings") + bool bEnableFall{false}; + +#pragma endregion States + +public: + /** + * Constructor for the animation layer. + * 动画层构造函数。 + */ + UGMS_AnimLayer_States_DefaultLocomotion(); + + /** + * Initializes the animation. + * 初始化动画。 + */ + virtual void NativeInitializeAnimation() override; + + /** + * Updates the animation in a thread-safe manner. + * 以线程安全的方式更新动画。 + * @param DeltaTime Time since the last update. 自上次更新以来的时间。 + */ + virtual void NativeThreadSafeUpdateAnimation(float DeltaTime) override; + + /** + * Called after animation evaluation. + * 动画评估后调用。 + */ + virtual void NativePostEvaluateAnimation() override; + + /** + * Applies the specified animation layer setting. + * 应用指定的动画层设置。 + * @param NewSetting The new animation layer setting.新的动画层设置。 + */ + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* NewSetting) override; + + /** + * Resets the animation layer setting. + * 重置动画层设置。 + */ + virtual void ResetSetting_Implementation() override; + + /** + * Checks if movement is perpendicular to initial pivot. + * 检查移动是否垂直于初始枢轴。 + * @return True if perpendicular, false otherwise. 如果垂直则返回true,否则返回false。 + */ + UFUNCTION(BlueprintPure, BlueprintCallable, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool IsMovingPerpendicularToInitialPivot() const; + + /** + * Checks if velocity is opposite to acceleration. + * 检查速度是否与加速度相反。 + * @return True if pivoting, false otherwise. 如果在枢轴转动则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool IsPivoting() const; + + /** + * Checks if in start state. + * 检查是否处于开始状态。 + * @return True if starting, false otherwise. 如果在开始则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool IsStarting() const; + +#pragma region Events; + virtual void OnLinked_Implementation() override; + virtual void OnUnlinked_Implementation() override; + UFUNCTION() + virtual void OnRotationModeChanged(const FGameplayTag& PrevMode); +#pragma endregion + +#pragma region Utility + + /** + * Selects cardinal direction from angle. + * 从角度选择主要方向。 + * @param Angle The input angle. 输入角度。 + * @param DeadZone The dead zone for direction selection. 方向选择的死区。 + * @param CurrentDirection The current direction. 当前方向。 + * @param bUseCurrentDirection Whether to use current direction. 是否使用当前方向。 + * @return Selected cardinal direction. 选择的主要方向。 + */ + EGMS_MovementDirection SelectCardinalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection CurrentDirection, bool bUseCurrentDirection) const; + + /** + * Selects octagonal direction from angle. + * 从角度选择八边形方向。 + * @param Angle The input angle. 输入角度。 + * @param DeadZone The dead zone for direction selection. 方向选择的死区。 + * @param CurrentDirection The current direction. 当前方向。 + * @param bUseCurrentDirection Whether to use current direction. 是否使用当前方向。 + * @return Selected octagonal direction. 选择的八边形方向。 + */ + EGMS_MovementDirection_8Way SelectOctagonalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection_8Way CurrentDirection, bool bUseCurrentDirection) const; + + /** + * Gets the opposite cardinal direction. + * 获取相反的主要方向。 + * @param CurrentDirection The current direction. 当前方向。 + * @return Opposite cardinal direction. 相反的主要方向。 + */ + EGMS_MovementDirection GetOppositeCardinalDirection(EGMS_MovementDirection CurrentDirection) const; + + /** + * Checks if in view direction mode. + * 检查是否处于视角方向模式。 + * @return True if in view direction, false otherwise. 如果在视角方向则返回true,否则返回false。 + */ + bool IsViewDirection() const; + + /** + * Checks if in velocity direction mode. + * 检查是否处于速度方向模式。 + * @return True if in velocity direction, false otherwise. 如果在速度方向则返回true,否则返回false。 + */ + bool IsVelocityDirection() const; + + /** + * Gets view direction settings. + * 获取视角方向设置。 + * @return View direction settings. 视角方向设置。 + */ + const TInstancedStruct& GetViewDirectionSetting() const; + + /** + * Gets velocity direction settings. + * 获取速度方向设置。 + * @return Velocity direction settings. 速度方向设置。 + */ + const TInstancedStruct& GetVelocityDirectionSetting() const; + +#pragma endregion Utility + +#pragma region Idle + +protected: + /** + * Updates idle animation. + * 更新空闲动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Idle_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates idle break relevance. + * 更新空闲打断相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void IdleBreak_AnimRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Initializes idle break animations. + * 初始化空闲打断动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void InitializeIdleBreak(); + + /** + * Refreshes idle break animations. + * 刷新空闲打断动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void RefreshIdleBreak(); + + /** + * Checks if idle break is allowed. + * 检查是否允许空闲打断。 + * @return True if allowed, false otherwise. 如果允许则返回true,否则返回false。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|IdleBreak", meta=(BlueprintThreadSafe)) + bool IsIdleBreakAllowed() const; + + /** + * Updates idle state. + * 更新空闲状态。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Idle_StateUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + +#pragma endregion + +#pragma region Turn/Rotate + + /** + * Sets up turn-in-place with specified angle. + * 使用指定角度设置原地转身。 + * @param TurnAngle The turn angle. 转身角度。 + */ + virtual void SetupTurnInPlace(float TurnAngle); + + /** + * Refreshes turn-in-place mode. + * 刷新原地转身模式。 + */ + virtual void RefreshTurnInPlaceMode(); + + /** + * Refreshes turn-in-place in view direction. + * 刷新视角方向的原地转身。 + */ + virtual void RefreshTurnInPlaceInViewDirection(); + + /** + * Refreshes turn-in-place in velocity direction. + * 刷新速度方向的原地转身。 + */ + virtual void RefreshTurnInPlaceInVelocityDirection(); + +#pragma endregion + +#pragma region Start + + /** + * Gets the start animation sequence. + * 获取开始动画序列。 + * @return Start animation sequence. 开始动画序列。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UAnimSequence* GetStartAnimation() const; + + /** + * Gets stride warping settings for start animations. + * 获取开始动画的步伐扭曲设置。 + * @return Stride warping settings.步伐扭曲设置。 + * + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + const FGMS_StrideWarpingSettings& GetStartStrideWarpingSettings() const; + + /** + * Gets steering settings for start animations. + * 获取开始动画的转向设置。 + * @return Steering settings. 转向设置。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + const FGMS_SteeringSettings& GetStartSteeringSettings() const; + + /** + * Handles start state entry. + * 处理开始状态进入。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Start_StateEntry(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates start state. + * 更新开始状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Start_StateUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates start animation. + * 更新开始动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Start_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates blend stack relevance for start animations. + * 更新开始动画的混合堆栈相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Start_BlendStackRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Sets up the start state. + * 设置开始状态。 + */ + void SetupStartState(); + +#pragma endregion + +#pragma region Cycle + + /** + * Updates cycle animation. + * 更新循环动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Cycle_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates cycle blend stack. + * 更新循环混合堆栈。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Cycle_BlendStackUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Handles cycle state entry. + * 处理循环状态进入。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Cycle_StateEntry(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates cycle state. + * 更新循环状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Cycle_StateUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Gets the cycle animation sequence. + * 获取循环动画序列。 + * @return Cycle animation sequence. 循环动画序列。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UAnimSequence* GetCycleAnimation() const; + +#pragma endregion + +#pragma region Stop + + /** + * Gets the stop animation sequence. + * 获取停止动画序列。 + * @return Stop animation sequence. 停止动画序列。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UAnimSequence* GetStopAnimation() const; + + /** + * Checks if distance matching is needed for stop animations. + * 检查停止动画是否需要距离匹配。 + * @return True if distance matching is needed, false otherwise. 如果需要距离匹配则返回true,否则返回false。 + */ + bool ShouldDistanceMatchStop() const; + + /** + * Updates stop animation relevance. + * 更新停止动画相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Stop_AnimRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates stop animation. + * 更新停止动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Stop_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates stop state relevance. + * 更新停止状态相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Stop_StateRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates stop state. + * 更新停止状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Stop_StateUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Implementation of stop state update. + * 停止状态更新的实现。 + */ + virtual void Stop_StateUpdate_Implementation(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Gets the distance to the stop target. + * 获取到停止目标的距离。 + * @return Distance to the stop target. 到停止目标的距离。 + */ + float GetDistanceToStopTarget() const; + +#pragma endregion + +#pragma region Pivot + + /** + * Updates pivot animation relevance. + * 更新枢轴动画相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Pivot_AnimRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates pivot animation. + * 更新枢轴动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Pivot_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates pivot state relevance. + * 更新枢轴状态相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Pivot_StateRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates pivot state. + * 更新枢轴状态。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Pivot_StateUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + +#pragma endregion + +#pragma region Jump + + /** + * Gets the jump start animation sequence. + * 获取起跳动画序列。 + * @return The start animation sequence. 起跳动画序列。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UAnimSequence* GetJumpStartAnimation() const; + + /** + * Updates jump start animation. + * 更新跳跃开始动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void JumpStart_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates jump start loop animation. + * 更新跳跃开始循环动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void JumpStartLoop_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates jump apex animation. + * 更新跳跃顶点动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void JumpApex_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates fall loop animation. + * 更新坠落循环动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void FallLoop_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates fall-to-land animation relevance. + * 更新从坠落到落地的动画相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void FallLand_AnimRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates fall-to-land animation. + * 更新从坠落到落地的动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void FallLand_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Checks if jump apex rule is satisfied. + * 检查跳跃顶点规则是否满足。 + * @return True if satisfied, false otherwise. 如果满足则返回true,否则返回false。 + * + */ + UFUNCTION(BlueprintPure, BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool Rule_JumpApex() const; + + /** + * Checks if fall-to-land rule is satisfied. + * 检查从坠落到落地的规则是否满足。 + * @return True if satisfied, false otherwise. 如果满足则返回true,否则返回false。 + * + */ + UFUNCTION(BlueprintPure, BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool Rule_FallToFallLand() const; + +#pragma endregion + +#pragma region Land + + /** + * Updates landing animation relevance. + * 更新着陆动画相关性。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Land_AnimRelevant(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + + /** + * Updates landing animation. + * 更新着陆动画。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void Land_AnimUpdate(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + +#pragma endregion +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View.h new file mode 100644 index 0000000..248358e --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View.h @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "UObject/Object.h" +#include "GMS_AnimLayer_View.generated.h" + +/** + * Base class for view animation layer settings. + * 视图动画层设置的基类。 + */ +UCLASS(Abstract, Blueprintable) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayerSetting_View : public UGMS_AnimLayerSetting +{ + GENERATED_BODY() +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View_Default.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View_Default.h new file mode 100644 index 0000000..b152dbe --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimLayer_View_Default.h @@ -0,0 +1,109 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_AnimLayer.h" +#include "GMS_AnimLayer_View.h" +#include "GMS_AnimLayer_View_Default.generated.h" + +class UAimOffsetBlendSpace; + +/** + * Native implementation of default view animation layer settings. + * 默认视图动画层设置的原生实现。 + */ +UCLASS(NotBlueprintable) +class UGMS_AnimLayerSetting_View_Default final : public UGMS_AnimLayerSetting_View +{ + GENERATED_BODY() + +public: + /** + * Blend space for the view animation. + * 视图动画的混合空间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View") + TObjectPtr BlendSpace; + + /** + * Yaw angle offset for the view. + * 视图的偏航角偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View") + float YawAngleOffset = 0.0f; + + /** + * Additional smoothing speed for aim offset. + * 瞄准偏移的额外平滑速度。 + * @attention Zero means no additional smoothing. 为零表示无额外平滑。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View", meta=(ClampMin=0)) + float SmoothInterpSpeed = 5.0f; + + /** + * Yaw angle limits for the view. + * 视图的偏航角限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View") + FVector2D YawAngleLimit{-90.0f, 90.0f}; +}; + +/** + * Native implementation of default view animation layer. + * 默认视图动画层的原生实现。 + */ +UCLASS(Abstract) +class GENERICMOVEMENTSYSTEM_API UGMS_AnimLayer_View_Default : public UGMS_AnimLayer +{ + GENERATED_BODY() + +public: + /** + * Applies the specified animation layer setting. + * 应用指定的动画层设置。 + * @param Setting The animation layer setting to apply. 要应用的动画层设置。 + */ + virtual void ApplySetting_Implementation(const UGMS_AnimLayerSetting* Setting) override; + + /** + * Resets the animation layer setting. + * 重置动画层设置。 + */ + virtual void ResetSetting_Implementation() override; + + /** + * Blend space for the view animation. + * 视图动画的混合空间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View") + TObjectPtr BlendSpace; + + /** + * Yaw angle offset for the view. + * 视图的偏航角偏移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Anim View") + float YawAngleOffset = 0.0f; + + /** + * Yaw angle limits for the view. + * 视图的偏航角限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Anim View") + FVector2D YawAngleLimit{ -90.0f, 90.0f }; + + /** + * Smoothing speed for the view animation. + * 视图动画的平滑速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Anim View", meta=(ClampMin=0)) + float SmoothInterpSpeed = 0.0f; + + /** + * Indicates if the blend space is valid. + * 指示混合空间是否有效。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Anim View") + bool bValidBlendSpace; +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimState.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimState.h new file mode 100644 index 0000000..5b6d8eb --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_AnimState.h @@ -0,0 +1,976 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_LocomotionEnumLibrary.h" +#include "Animation/TrajectoryTypes.h" +#include "BoneControllers/AnimNode_OffsetRootBone.h" +#include "UObject/Object.h" +#include "GMS_AnimState.generated.h" + +class UAnimSequenceBase; +class UAnimSequence; + +/** + * Stores locomotion-related animation state data. + * 存储与运动相关的动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Locomotion +{ + GENERATED_BODY() + + /** + * World-space location of the character. + * 角色的世界空间位置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector Location{ForceInit}; + + /** + * Displacement from the previous frame. + * 上一帧的位移。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float PreviousDisplacement{0.0f}; + + /** + * Speed of displacement (cm/s). + * 位移速度(厘米/秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float DisplacementSpeed{0.0f}; + + /** + * World-space rotation of the character. + * 角色的世界空间旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FRotator Rotation{ForceInit}; + + /** + * Quaternion representation of the rotation. + * 旋转的四元数表示。 + */ + UPROPERTY() + FQuat RotationQuaternion{ForceInit}; + + /** + * Current velocity vector. + * 当前速度向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector Velocity{ForceInit}; + + /** + * 2D local-space velocity vector. + * 2D本地空间速度向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector LocalVelocity2D{ForceInit}; + + /** + * Indicates if the character has velocity. + * 指示角色是否有速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bHasVelocity = false; + + /** + * Current speed of the character (cm/s). + * 角色的当前速度(厘米/秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float Speed{0.0f}; + + /** + * Yaw angle of the local velocity (degrees). + * 本地速度的偏航角(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float LocalVelocityYawAngle{0.0f}; + + /** + * Yaw angle of the local velocity with offset (degrees). + * 带偏移的本地速度偏航角(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float LocalVelocityYawAngleWithOffset{0.0f}; + + /** + * Cardinal direction of the local velocity. + * 本地速度的主要方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_MovementDirection LocalVelocityDirection{EGMS_MovementDirection::Forward}; + + /** + * Cardinal direction of the local velocity without offset. + * 无偏移的本地速度主要方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_MovementDirection LocalVelocityDirectionNoOffset{EGMS_MovementDirection::Forward}; + + /** + * Octagonal direction of the local velocity. + * 本地速度的八方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_MovementDirection_8Way LocalVelocityOctagonalDirection{EGMS_MovementDirection_8Way::Forward}; + + /** + * Indicates if there is active input. + * 指示是否有活跃输入。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bHasInput{false}; + + /** + * Velocity acceleration vector. + * 速度加速度向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector VelocityAcceleration{ForceInit}; + + /** + * 2D local-space acceleration vector. + * 2D本地空间加速度向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector LocalAcceleration2D{ForceInit}; + + /** + * Indicates if the character is moving. + * 指示角色是否在移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bMoving{false}; + + /** + * Actor's rotation yaw speed. + * Actor的旋转偏航角变化速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float YawVelocity{0.0f}; + + /** + * Scale factor for animations. + * 动画的缩放因子。 + */ + float Scale{1.0f}; + + /** + * Radius of the character's capsule. + * 角色胶囊体的半径。 + */ + float CapsuleRadius{0.0f}; + + /** + * Half-height of the character's capsule. + * 角色胶囊体的一半高度。 + */ + float CapsuleHalfHeight{0.0f}; + + /** + * Maximum acceleration of the character. + * 角色的最大加速度。 + */ + float MaxAcceleration{0.0f}; + + /** + * Maximum braking deceleration of the character. + * 角色的最大制动减速度。 + */ + float MaxBrakingDeceleration{0.0f}; + + /** + * Z value for walkable floor detection. + * 可行走地板检测的Z值。 + */ + float WalkableFloorZ{0.0f}; +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Trajectory +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FTransformTrajectory Trajectory; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector PastVelocity{FVector::ZeroVector}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector CurrentVelocity{FVector::ZeroVector}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector FutureVelocity{FVector::ZeroVector}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float DesiredControllerYaw{0.0f}; +}; + +/** + * Stores root bone animation state data. + * 存储根骨骼动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Root +{ + GENERATED_BODY() + + /** + * Translation mode for the root bone. + * 根骨骼的平移模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EOffsetRootBoneMode TranslationMode{EOffsetRootBoneMode::Release}; + + /** + * Rotation mode for the root bone. + * 根骨骼的旋转模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EOffsetRootBoneMode RotationMode{EOffsetRootBoneMode::Release}; + + /** + * Current world-space transform of the root bone. + * 根骨骼的当前世界空间变换。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FTransform RootTransform{FTransform::Identity}; + + /** + * Yaw offset relative to the actor's rotation (degrees; <0 left, >0 right). + * 相对于Actor旋转的偏航偏移(度;<0左侧,>0右侧)。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + float YawOffset{0}; + + /** + * Maximum rotation error in degrees; values <0 disable the limit. + * 最大旋转误差(度);值<0禁用限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float MaxRotationError{-1.0f}; +}; + +/** + * Stores turn-in-place animation state data. + * 存储原地转身动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_TurnInPlace +{ + GENERATED_BODY() + + /** + * Indicates if the state was updated this frame. + * 指示此帧是否更新了状态。 + */ + UPROPERTY() + uint8 bUpdatedThisFrame : 1 {false}; + + /** + * Animation sequence for turn-in-place. + * 原地转身的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation; + + /** + * Indicates if the character should turn. + * 指示角色是否应该转身。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bShouldTurn : 1 {false}; + + /** + * Accumulated time for the animation. + * 动画的累计时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float AccumulatedTime{0}; + + /** + * Playback rate for the animation. + * 动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "x")) + float PlayRate{1.0f}; + + /** + * Scaled playback rate for the animation. + * 动画的缩放播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "x")) + float ScaledPlayRate{1.0f}; + + /** + * Delay before activating the turn-in-place animation. + * 原地转身动画激活前的延迟。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ForceUnits = "s")) + float ActivationDelay{0.0f}; + + /** + * Angle that triggers the turn-in-place animation. + * 触发原地转身动画的角度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TriggeredAngle{0.0f}; + + /** + * Indicates if the turn is 180 degrees. + * 指示是否为180度转身。 + */ + bool b180{false}; +}; + +/** + * Stores in-air animation state data. + * 存储空中动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_InAir +{ + GENERATED_BODY() + + /** + * Vertical speed of the character (cm/s). + * 角色的垂直速度(厘米/秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ForceUnits = "cm/s")) + float VerticalSpeed{0.0f}; + + /** + * Indicates if the character is jumping. + * 指示角色是否在跳跃。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bJumping{false}; + + /** + * Indicates if the character is falling. + * 指示角色是否在下落。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bFalling{false}; + + /** + * Playback rate for the jump animation. + * 跳跃动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "x")) + float JumpPlayRate{1.0f}; + + /** + * Indicates if valid ground is detected. + * 指示是否检测到有效地面。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bValidGround{false}; + + /** + * Distance to the ground (cm). + * 到地面的距离(厘米)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ForceUnits = "cm")) + float GroundDistance{0.0f}; + + /** + * Time to reach the jump apex (s). + * 到达跳跃顶点的时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1, ForceUnits = "s")) + float TimeToJumpApex{0.0f}; + + /** + * Time spent falling (s). + * 下落的时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1, ForceUnits = "s")) + float FallingTime{0.0f}; +}; + +/** + * Stores idle animation state data. + * 存储空闲动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Idle +{ + GENERATED_BODY() + + /** + * Animation sequence for idle. + * 空闲动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation{nullptr}; + + /** + * Indicates if the idle animation is looped. + * 指示空闲动画是否循环。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bLoop{true}; + + /** + * Playback rate for the idle animation. + * 空闲动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float PlayRate{1.0f}; + + /** + * Blend time for the idle animation. + * 空闲动画的混合时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float BlendTime{1.0f}; + + /** + * Blend profile for the idle animation. + * 空闲动画的混合配置文件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BlendProfile{nullptr}; +}; + +/** + * Stores runtime state for idle break animations. + * 存储空闲中断动画的运行时状态。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_IdleBreak +{ + GENERATED_BODY() + + /** + * Time until the next idle break animation (s). + * 到下一个空闲中断动画的时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TimeUntilNextIdleBreak{0.0f}; + + /** + * Index of the current idle break animation. + * 当前空闲中断动画的索引。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + int32 CurrentIdleBreakIndex{0}; + + /** + * Delay between idle break animations (s). + * 空闲中断动画之间的延迟(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float IdleBreakDelayTime{0.0f}; +}; + +/** + * Stores lean animation state data. + * 存储倾斜动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Lean +{ + GENERATED_BODY() + + /** + * Horizontal acceleration amount for leaning (clamped -1 to 1). + * 用于倾斜的水平加速度量(限制在-1到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -1, ClampMax = 1)) + float RightAmount{0.0f}; + + /** + * Vertical acceleration amount for leaning (clamped -1 to 1). + * 用于倾斜的垂直加速度量(限制在-1到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -1, ClampMax = 1)) + float ForwardAmount{0.0f}; +}; + +/** + * Stores pivot animation state data. + * 存储枢轴动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Pivot +{ + GENERATED_BODY() + + /** + * Animation sequence for the pivot. + * 枢轴动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation; + + /** + * Acceleration at the start of the pivot. + * 枢轴开始时的加速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector StartingAcceleration{ForceInit}; + + /** + * Accumulated time for the pivot animation. + * 枢轴动画的累计时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float AccumulatedTime{ForceInit}; + + /** + * Time at which the pivot stops for distance matching. + * 距离匹配时枢轴停止的时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TimeAtPivotStop{ForceInit}; + + /** + * Alpha for stride warping during the pivot. + * 枢轴期间步幅适配的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float StrideWarpingAlpha{ForceInit}; + + /** + * Clamp for the playback rate (min, max). + * 播放速率的限制(最小,最大)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector2D PlayRateClamp{0.f, 0.f}; + + /** + * 2D direction of the pivot. + * 枢轴的2D方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FVector Direction2D{ForceInit}; + + /** + * Desired movement direction for the pivot. + * 枢轴的期望移动方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + EGMS_MovementDirection DesiredDirection{ForceInit}; + + /** + * Initial movement direction for the pivot. + * 枢轴的初始移动方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + EGMS_MovementDirection InitialDirection{ForceInit}; + + /** + * Remaining cooldown time for the pivot (s). + * 枢轴的剩余冷却时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + float RemainingCooldown{ForceInit}; + + /** + * Indicates if the pivot is moving perpendicular to the initial direction. + * 指示枢轴是否垂直于初始方向移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + bool bMovingPerpendicularToInitialDirection{ForceInit}; +}; + +/** + * Stores start animation state data. + * 存储开始动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Start +{ + GENERATED_BODY() + + /** + * Animation sequence for the start movement. + * 开始移动的动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation{nullptr}; + + /** + * Smooth target rotation for the start movement. + * 开始移动的平滑目标旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FRotator SmoothTargetRotation{ForceInit}; + + /** + * Yaw delta between acceleration direction and root direction at start (degrees). + * 开始时加速度方向与根方向的偏航差(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float YawDeltaToAcceleration{ForceInit}; + + /** + * Local velocity direction at the start. + * 开始时的本地速度方向。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_MovementDirection LocalVelocityDirection{ForceInit}; + + /** + * Alpha for stride warping during the start. + * 开始期间步幅适配的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float StrideWarpingAlpha{ForceInit}; + + /** + * Clamp for the playback rate (min, max). + * 播放速率的限制(最小,最大)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector2D PlayRateClamp{ForceInit}; + + /** + * Alpha for orientation warping during the start. + * 开始期间朝向扭曲的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float OrientationAlpha{1.0f}; + + /** + * Time spent in the start state (s). + * 开始状态的持续时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TimeInState{0.0f}; + + /** + * Blend profile for the start animation. + * 开始动画的混合配置文件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BlendProfile{nullptr}; + + /** + * Remaining time for the start animation (s). + * 开始动画的剩余时间(秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TimeRemaining{0.0f}; + + /** + * Playback rate for the start animation. + * 开始动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float PlayRate{0.0f}; +}; + +/** + * Stores cycle animation state data. + * 存储循环动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Cycle +{ + GENERATED_BODY() + + /** + * Animation sequence for the cycle. + * 循环动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation{nullptr}; + + /** + * Alpha for orientation warping during the cycle. + * 循环期间朝向扭曲的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float OrientationAlpha{ForceInit}; + + /** + * Alpha for stride warping during the cycle. + * 循环期间步幅适配的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float StrideWarpingAlpha{ForceInit}; + + /** + * Playback rate for the cycle animation. + * 循环动画的播放速率。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float PlayRate{1.0f}; + + /** + * Blend profile for the cycle animation. + * 循环动画的混合配置文件。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BlendProfile{nullptr}; +}; + +/** + * Stores stop animation state data. + * 存储停止动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Stop +{ + GENERATED_BODY() + + /** + * Animation sequence for the stop. + * 停止动画序列。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Animation{nullptr}; + + /** + * Alpha for orientation warping during the stop. + * 停止期间朝向扭曲的Alpha值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float OrientationAlpha{1.0f}; +}; + +/** + * Stores view-related animation state data. + * 存储与视图相关的动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_View +{ + GENERATED_BODY() + + /** + * Rotation of the view. + * 视图的旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FRotator Rotation{ForceInit}; + + /** + * Yaw angle between the actor and camera (degrees). + * Actor与相机之间的偏航角(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float YawAngle{0.0f}; + + /** + * Speed of yaw angle change (degrees/s). + * 偏航角变化速度(度/秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "deg/s")) + float YawSpeed{0.0f}; + + /** + * Pitch angle of the view (degrees). + * 视图的俯仰角(度)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -90, ClampMax = 90, ForceUnits = "deg")) + float PitchAngle{0.0f}; + + /** + * Amount of pitch applied (clamped 0 to 1). + * 应用的俯仰量(限制在0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float PitchAmount{0.5f}; +}; + +/** + * Stores layering animation state data for body parts. + * 存储身体部位的分层动画状态数据。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_Layering +{ + GENERATED_BODY() + virtual ~FGMS_AnimState_Layering() = default; + /** + * Blend amount for the head (0 to 1). + * 头部混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HeadBlendAmount{0.0f}; + + /** + * Additive blend amount for the head (0 to 1). + * 头部附加混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HeadAdditiveBlendAmount{0.0f}; + + /** + * Slot blend amount for the head (0 to 1). + * 头部槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HeadSlotBlendAmount{1.0f}; + + /** + * Blend amount for the left arm (0 to 1). + * 左臂混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftBlendAmount{0.0f}; + + /** + * Additive blend amount for the left arm (0 to 1). + * 左臂附加混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftAdditiveBlendAmount{0.0f}; + + /** + * Slot blend amount for the left arm (0 to 1). + * 左臂槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftSlotBlendAmount{1.0f}; + + /** + * Local space blend amount for the left arm (0 to 1). + * 左臂本地空间混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftLocalSpaceBlendAmount{0.0f}; + + /** + * Mesh space blend amount for the left arm (0 to 1). + * 左臂网格空间混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftMeshSpaceBlendAmount{0.0f}; + + /** + * Blend amount for the right arm (0 to 1). + * 右臂混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightBlendAmount{0.0f}; + + /** + * Additive blend amount for the right arm (0 to 1). + * 右臂附加混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightAdditiveBlendAmount{0.0f}; + + /** + * Slot blend amount for the right arm (0 to 1). + * 右臂槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightSlotBlendAmount{1.0f}; + + /** + * Local space blend amount for the right arm (0 to 1). + * 右臂本地空间混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightLocalSpaceBlendAmount{0.0f}; + + /** + * Mesh space blend amount for the right arm (0 to 1). + * 右臂网格空间混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightMeshSpaceBlendAmount{0.0f}; + + /** + * Blend amount for the left hand (0 to 1). + * 左手混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HandLeftBlendAmount{0.0f}; + + /** + * Blend amount for the right hand (0 to 1). + * 右手混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HandRightBlendAmount{0.0f}; + + /** + * Blend amount for the spine (0 to 1). + * 脊椎混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float SpineBlendAmount{0.0f}; + + /** + * Additive blend amount for the spine (0 to 1). + * 脊椎附加混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float SpineAdditiveBlendAmount{0.0f}; + + /** + * Slot blend amount for the spine (0 to 1). + * 脊椎槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float SpineSlotBlendAmount{1.0f}; + + /** + * Blend amount for the pelvis (0 to 1). + * 骨盆混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float PelvisBlendAmount{0.0f}; + + /** + * Slot blend amount for the pelvis (0 to 1). + * 骨盆槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float PelvisSlotBlendAmount{1.0f}; + + /** + * Blend amount for the legs (0 to 1). + * 腿部混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float LegsBlendAmount{0.0f}; + + /** + * Slot blend amount for the legs (0 to 1). + * 腿部槽混合量(0到1)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float LegsSlotBlendAmount{1.0f}; + + void ApplyValueFromSequence(const UAnimSequence* InSequence, float ExplicitTime); + + virtual void ZeroOut(); +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimState_LayeringSmooth +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float HeadBlendAmount{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmLeftBlendAmount{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float ArmRightBlendAmount{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float SpineBlendAmount{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float PelvisBlendAmount{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ClampMax = 1)) + float LegsBlendAmount{0.0f}; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionEnumLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionEnumLibrary.h new file mode 100644 index 0000000..40a0b58 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionEnumLibrary.h @@ -0,0 +1,37 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GMS_LocomotionEnumLibrary.generated.h" + +/** + * Defines four-directional movement directions. + * 定义四方向移动方向。 + */ +UENUM(BlueprintType) +enum class EGMS_MovementDirection : uint8 +{ + Forward, // Forward movement. 前进移动。 + Backward, // Backward movement. 后退移动。 + Left, // Left movement. 左移。 + Right // Right movement. 右移。 +}; + +/** + * Defines eight-directional movement directions. + * 定义八方向移动方向。 + */ +UENUM(BlueprintType) +enum class EGMS_MovementDirection_8Way : uint8 +{ + Forward, // Forward movement. 前进移动。 + ForwardLeft, // Forward-left movement. 前左移动。 + ForwardRight, // Forward-right movement. 前右移动。 + Backward, // Backward movement. 后退移动。 + BackwardLeft, // Backward-left movement. 后左移动。 + BackwardRight, // Backward-right movement. 后右移动。 + Left, // Left movement. 左移。 + Right // Right movement. 右移。 +}; \ No newline at end of file diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionStructLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionStructLibrary.h new file mode 100644 index 0000000..bf3266a --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_LocomotionStructLibrary.h @@ -0,0 +1,601 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Animation/CachedAnimData.h" +#include "UObject/Object.h" +#include "GMS_LocomotionStructLibrary.generated.h" + +class UBlendSpace1D; +class UAnimSequenceBase; +class UAnimSequence; + +/** + * Stores the locomotion state of a character. + * 存储角色的运动状态。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_LocomotionState +{ + GENERATED_BODY() + + /** + * Indicates if there is active input. + * 表示是否有活跃的输入。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bHasInput{false}; + + /** + * Input yaw angle in world space. + * 世界空间中的输入偏航角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float InputYawAngle{0.0f}; + + /** + * Indicates if the character has speed. + * 表示角色是否有速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bHasVelocity{false}; + + /** + * Current speed of the character. + * 角色的当前速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float Speed{0.0f}; + + /** + * Current velocity vector. + * 当前速度向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector Velocity{ForceInit}; + + /** + * Yaw angle of the character's velocity. + * 角色速度的偏航角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float VelocityYawAngle{0.0f}; + + /** + * Indicates if the character is moving. + * 表示角色是否在移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bMoving{false}; + + /** + * Target yaw angle for the actor's rotation. + * Actor旋转的目标偏航角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float TargetYawAngle{0.0f}; + + /** + * Smoothed target yaw angle for extra smooth rotation. + * 用于平滑旋转的平滑目标偏航角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float SmoothTargetYawAngle{0.0f}; + + /** + * Angle between view yaw and target yaw. + * 视角偏航角与目标偏航角之间的角度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float ViewRelativeTargetYawAngle{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bAimingLimitAppliedThisFrame : 1 {false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bResetAimingLimit : 1 {true}; + + /** + * Limit for the aiming yaw angle. + * 瞄准偏航角的限制。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float AimingYawAngleLimit{180.0f}; +}; + + +/** + * Stores the view state of a character. + * 存储角色的视图状态。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ViewState +{ + GENERATED_BODY() + + /** + * Smoothed view rotation, set by replicated view rotation. + * 平滑的视角旋转,由复制的视角旋转设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FRotator Rotation{ForceInit}; + + /** + * Speed of camera rotation from left to right. + * 相机左右旋转的速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "deg/s")) + float YawSpeed{0.0f}; + + /** + * View yaw angle from the previous frame. + * 上一帧的视角偏航角。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = -180, ClampMax = 180, ForceUnits = "deg")) + float PreviousYawAngle{0.0f}; +}; + + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MovementBaseState +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category="GMS") + TObjectPtr Primitive; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FName BoneName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bBaseChanged : 1 {false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bHasRelativeLocation : 1 {false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + uint8 bHasRelativeRotation : 1 {false}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FVector Location{ForceInit}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FQuat Rotation{ForceInit}; + + /** + * 基础对象(例如移动平台)从上一帧到当前帧的旋转变化。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + FRotator DeltaRotation{ForceInit}; +}; + + +/** + * Parameters for predicting ground movement stop location. + * 预测地面运动停止位置的参数。 + */ +USTRUCT() +struct FGMS_PredictGroundMovementStopLocationParams +{ + GENERATED_BODY() + + /** + * Current velocity vector. + * 当前速度向量。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FVector Velocity{ForceInit}; + + /** + * Whether to use separate braking friction. + * 是否使用单独的制动摩擦。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + bool bUseSeparateBrakingFriction{ForceInit}; + + /** + * Braking friction value. + * 制动摩擦值。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float BrakingFriction{ForceInit}; + + /** + * Ground friction value. + * 地面摩擦值。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float GroundFriction{ForceInit}; + + /** + * Braking friction factor. + * 制动摩擦因子。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float BrakingFrictionFactor{ForceInit}; + + /** + * Braking deceleration for walking. + * 行走时的制动减速度。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float BrakingDecelerationWalking{ForceInit}; +}; + +/** + * Parameters for predicting ground movement pivot location. + * 预测地面运动枢轴位置的参数。 + */ +USTRUCT() +struct FGMS_PredictGroundMovementPivotLocationParams +{ + GENERATED_BODY() + + /** + * Current acceleration vector. + * 当前加速度向量。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FVector Acceleration{ForceInit}; + + /** + * Current velocity vector. + * 当前速度向量。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FVector Velocity{ForceInit}; + + /** + * Ground friction value. + * 地面摩擦值。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + float GroundFriction{0.0f}; +}; + +/** + * Stores animations for four-directional movement. + * 存储四方向移动的动画。 + */ +USTRUCT(BlueprintType) +struct FGMS_Animations_4Direction +{ + GENERATED_BODY() + + /** + * Animation for forward movement. + * 前进移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Forward = nullptr; + + /** + * Animation for backward movement. + * 后退移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Backward = nullptr; + + /** + * Animation for left movement. + * 左移移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Left = nullptr; + + /** + * Animation for right movement. + * 右移移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Right = nullptr; + + /** + * Checks if all animations are valid. + * 检查所有动画是否有效。 + * @return True if all animations are set. 如果所有动画都设置返回true。 + */ + bool ValidAnimations() const; + + /** + * Checks if any animation has root motion. + * 检查是否有动画包含根运动。 + * @return True if any animation has root motion. 如果有动画包含根运动返回true。 + */ + bool HasRootMotion() const; +}; + +/** + * Stores animations for eight-directional movement. + * 存储八方向移动的动画。 + */ +USTRUCT(BlueprintType) +struct FGMS_Animations_8Direction +{ + GENERATED_BODY() + + /** + * Animation for forward movement. + * 前进移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Forward = nullptr; + + /** + * Animation for forward-left movement. + * 前左移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr ForwardLeft = nullptr; + + /** + * Animation for forward-right movement. + * 前右移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr ForwardRight = nullptr; + + /** + * Animation for backward movement. + * 后退移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Backward = nullptr; + + /** + * Animation for backward-left movement. + * 后左移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BackwardLeft = nullptr; + + /** + * Animation for backward-right movement. + * 后右移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr BackwardRight = nullptr; + + /** + * Animation for left movement. + * 左移移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Left = nullptr; + + /** + * Animation for right movement. + * 右移移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr Right = nullptr; + + /** + * Checks if all animations are valid. + * 检查所有动画是否有效。 + * @return True if all animations are set. 如果所有动画都设置返回true。 + */ + bool ValidAnimations() const; + + /** + * Checks if any animation has root motion. + * 检查是否有动画包含根运动。 + * @return True if any animation has root motion. 如果有动画包含根运动返回true。 + */ + bool HasRootMotion() const; +}; + +/** + * Stores 1D blend space animations for forward and backward movement. + * 存储用于前后移动的1D混合空间动画。 + */ +USTRUCT(BlueprintType) +struct FGMS_Animations_BS1D_FwdBwd +{ + GENERATED_BODY() + + /** + * Blend space for forward movement. + * 前进移动的混合空间。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr Forward{nullptr}; + + /** + * Blend space for backward movement. + * 后退移动的混合空间。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TObjectPtr Backward{nullptr}; +}; + +/** + * Stores animations for starting movement while facing forward. + * 存储面向前进时开始移动的动画。 + */ +USTRUCT(BlueprintType) +struct FGMS_Animations_StartForwardFacing +{ + GENERATED_BODY() + + /** + * Animation for starting movement forward. + * 前进开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForward = nullptr; + + /** + * Animation for starting movement forward with a 90-degree left turn. + * 前进并向左90度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardL90 = nullptr; + + /** + * Animation for starting movement forward with a 90-degree right turn. + * 前进并向右90度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardR90 = nullptr; + + /** + * Animation for starting movement forward with a 180-degree left turn. + * 前进并向左180度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardL180 = nullptr; + + /** + * Animation for starting movement forward with a 180-degree right turn. + * 前进并向右180度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardR180 = nullptr; +}; + +/** + * Stores animations for starting movement while facing forward in eight directions. + * 存储面向前进时八方向开始移动的动画。 + */ +USTRUCT(BlueprintType) +struct FGMS_Animations_StartForwardFacing_8Direction +{ + GENERATED_BODY() + + /** + * Animation for starting movement forward. + * 前进开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForward = nullptr; + + /** + * Animation for starting movement forward with a 90-degree left turn. + * 前进并向左90度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardL90 = nullptr; + + /** + * Animation for starting movement forward with a 90-degree right turn. + * 前进并向右90度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardR90 = nullptr; + + /** + * Animation for starting movement forward with a 180-degree left turn. + * 前进并向左180度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardL180 = nullptr; + + /** + * Animation for starting movement forward with a 180-degree right turn. + * 前进并向右180度开始移动的动画。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr StartForwardR180 = nullptr; +}; + +/** + * Stores an animation with an associated distance. + * 存储与距离关联的动画。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimationWithDistance +{ + GENERATED_BODY() + + /** + * The animation sequence. + * 动画序列。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS") + TObjectPtr Animation = nullptr; + + /** + * The distance associated with the animation. + * 与动画关联的距离。 + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="GMS", meta=(ClampMin=0)) + float Distance{200}; + +#if WITH_EDITORONLY_DATA + /** + * Editor-friendly name for the animation. + * 动画的编辑器友好名称。 + */ + UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +/** + * Maps animation state names to gameplay tags. + * 将动画状态名称映射到游戏标签。 + */ +USTRUCT(BlueprintType) +struct FGMS_AnimStateNameToTag +{ + GENERATED_BODY() + + /** + * The gameplay tag for the animation state. + * 动画状态的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(Categories="GMS.SM")) + FGameplayTag Tag; + + /** + * The cached animation state data. + * 缓存的动画状态数据。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category= "GMS") + FCachedAnimStateData State; + + /** + * Equality operator for comparing two animation state mappings. + * 比较两个动画状态映射的相等运算符。 + * @param Lhs Left-hand side mapping. 左侧映射。 + * @param RHS Right-hand side mapping. 右侧映射。 + * @return True if mappings are equal. 如果映射相等返回true。 + */ + friend bool operator==(const FGMS_AnimStateNameToTag& Lhs, const FGMS_AnimStateNameToTag& RHS) + { + return Lhs.Tag == RHS.Tag + && Lhs.State.StateMachineName == RHS.State.StateMachineName && Lhs.State.StateName == RHS.State.StateName; + } + + /** + * Inequality operator for comparing two animation state mappings. + * 比较两个动画状态映射的不等运算符。 + * @param Lhs Left-hand side mapping. 左侧映射。 + * @param RHS Right-hand side mapping. 右侧映射。 + * @return True if mappings are not equal. 如果映射不相等返回true。 + */ + friend bool operator!=(const FGMS_AnimStateNameToTag& Lhs, const FGMS_AnimStateNameToTag& RHS) + { + return !(Lhs == RHS); + } +}; + +/** + * Wrapper for a list of animation state to tag mappings. + * 动画状态到标签映射列表的包装器。 + */ +USTRUCT() +struct FGMS_AnimStateNameToTagWrapper +{ + GENERATED_BODY() + + /** + * Array of animation state to tag mappings. + * 动画状态到标签映射的数组。 + */ + UPROPERTY() + TArray AnimStateNameToTagMapping; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_MainAnimInstance.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_MainAnimInstance.h new file mode 100644 index 0000000..5795a96 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Locomotions/GMS_MainAnimInstance.h @@ -0,0 +1,651 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GMS_LocomotionStructLibrary.h" +#include "Engine/TimerHandle.h" +#include "TimerManager.h" +#include "Animation/AnimExecutionContext.h" +#include "Animation/AnimInstance.h" +#include "Animation/AnimNodeReference.h" +#include "Engine/World.h" +#include "Settings/GMS_SettingStructLibrary.h" +#include "Locomotions/GMS_AnimState.h" +#include "Utility/GMS_Tags.h" +#include "GMS_MainAnimInstance.generated.h" + +class IPoseSearchTrajectoryPredictorInterface; +class UGMS_AnimLayerSetting_Additive; +class UGMS_AnimLayer; +class UGMS_AnimLayerSetting; +class UGMS_AnimLayerSetting_View; +class UGMS_AnimLayerSetting_Overlay; +class UGMS_AnimLayerSetting_States; +class UGMS_MovementDefinition; +class UGMS_MovementSystemComponent; + +/** + * Base animation template for the main animation instance. + * 主动画实例的动画模板基类。 + */ +UCLASS(BlueprintType) +class GENERICMOVEMENTSYSTEM_API UGMS_MainAnimInstance : public UAnimInstance +{ + GENERATED_BODY() + +public: + /** + * Gets the movement system component. + * 获取运动系统组件。 + * @return The movement system component. 运动系统组件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UGMS_MovementSystemComponent* GetMovementSystemComponent() const; + + /** + * Constructor. + * 构造函数。 + */ + UGMS_MainAnimInstance(); + + /** + * Initializes the animation. + * 初始化动画。 + */ + virtual void NativeInitializeAnimation() override; + + /** + * Uninitializes the animation. + * 取消初始化动画。 + */ + virtual void NativeUninitializeAnimation() override; + + /** + * Called when the game starts. + * 游戏开始时调用。 + */ + virtual void NativeBeginPlay() override; + + /** + * Updates the animation. + * 更新动画。 + * @param DeltaTime Time since last update. 自上次更新以来的时间。 + */ + virtual void NativeUpdateAnimation(float DeltaTime) override; + + /** + * Thread-safe animation update. + * 线程安全的动画更新。 + * @param DeltaTime Time since last update. 自上次更新以来的时间。 + */ + virtual void NativeThreadSafeUpdateAnimation(float DeltaTime) override; + + /** + * Applies an animation layer setting to an animation layer instance. + * 将动画层设置应用于动画层实例。 + * @param LayerSetting The layer setting to apply. 要应用的层设置。 + * @param LayerInstance The layer instance to apply the setting to. 要应用设置的层实例。 + */ + virtual void SetAnimLayerBySetting(const UGMS_AnimLayerSetting* LayerSetting, TObjectPtr& LayerInstance); + + /** + * Registers animation state name to tag mappings for a given animation instance. + * 为给定的动画实例注册动画状态名称到标签的映射。 + * @param SourceAnimInstance The animation instance to register mappings for. 要注册映射的动画实例。 + * @param Mapping The state name to tag mappings. 状态名称到标签的映射。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|Animation") + virtual void RegisterStateNameToTagMapping(UAnimInstance* SourceAnimInstance, TArray Mapping); + + /** + * Unregisters animation state name to tag mappings for a given animation instance. + * 为给定的动画实例取消注册动画状态名称到标签的映射。 + * @param SourceAnimInstance The animation instance to unregister mappings for. 要取消注册映射的动画实例。 + */ + UFUNCTION(BlueprintCallable, Category="GMS|Animation") + virtual void UnregisterStateNameToTagMapping(UAnimInstance* SourceAnimInstance); + + /** + * Refreshes layer settings when core state changes (e.g., movement set, locomotion mode). + * 当核心状态(如运动集、运动模式)更改时刷新层设置。 + * @note Override if custom movement definitions include additional animation layer settings. 如果自定义运动定义包含额外的动画层设置,则需覆盖。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation") + void RefreshLayerSettings(); + virtual void RefreshLayerSettings_Implementation(); + + /** + * Sets the offset root bone rotation mode. + * 设置偏移根骨骼旋转模式。 + * @param NewRotationMode The new rotation mode. 新的旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void SetOffsetRootBoneRotationMode(EOffsetRootBoneMode NewRotationMode); + + /** + * Gets the current offset root bone rotation mode. + * 获取当前偏移根骨骼旋转模式。 + * @return The current rotation mode. 当前旋转模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + EOffsetRootBoneMode GetOffsetRootBoneRotationMode() const; + + /** + * Gets the current offset root bone translation mode. + * 获取当前偏移根骨骼平移模式。 + * @return The current translation mode. 当前平移模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + EOffsetRootBoneMode GetOffsetRootBoneTranslationMode() const; + + /** + * Sets the offset root bone translation mode. + * 设置偏移根骨骼平移模式。 + * @param NewTranslationMode The new translation mode. 新的平移模式。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void SetOffsetRootBoneTranslationMode(EOffsetRootBoneMode NewTranslationMode); + +protected: + /** + * Called when the locomotion mode changes. + * 运动模式更改时调用。 + * @param Prev The previous locomotion mode. 之前的运动模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|Animation") + void OnLocomotionModeChanged(const FGameplayTag& Prev); + + /** + * Called when the rotation mode changes. + * 旋转模式更改时调用。 + * @param Prev The previous rotation mode. 之前的旋转模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|Animation") + void OnRotationModeChanged(const FGameplayTag& Prev); + + /** + * Called when the movement set changes. + * 运动集更改时调用。 + * @param Prev The previous movement set. 之前的运动集。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|Animation") + void OnMovementSetChanged(const FGameplayTag& Prev); + + /** + * Called when the movement state changes. + * 运动状态更改时调用。 + * @param Prev The previous movement state. 之前的运动状态。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|Animation") + void OnMovementStateChanged(const FGameplayTag& Prev); + + /** + * Called when the overlay mode changes. + * 叠层模式更改时调用。 + * @param Prev The previous overlay mode. 之前的叠层模式。 + */ + UFUNCTION(BlueprintNativeEvent, Category="GMS|Animation") + void OnOverlayModeChanged(const FGameplayTag& Prev); + + /** + * Refreshes Trajectory-related data. + * 刷新Trajectory相关数据。 + * @param DeltaTime Time since last update. 自上次更新以来的时间。 + */ + virtual void RefreshTrajectoryState(float DeltaTime); + + /** + * Refreshes view-related data. + * 刷新视图相关数据。 + * @param DeltaTime Time since last update. 自上次更新以来的时间。 + */ + virtual void RefreshView(float DeltaTime); + + /** + * Refreshes locomotion data. + * 刷新运动数据。 + * @param DeltaTime Time since last update. 自上次更新以来的时间。 + */ + virtual void RefreshLocomotion(const float DeltaTime); + + /** + * Refreshes block state. + * 刷新阻塞状态。 + */ + virtual void RefreshBlock(); + + /** + * Gather information from game world. + * 从游戏世界获取信息。 + */ + virtual void RefreshStateOnGameThread(); + + /** + * Refreshes animation node relevance tags on the game thread. + * 在游戏线程上刷新动画节点相关性标签。 + */ + virtual void RefreshRelevanceOnGameThread(); + + /** + * Refreshes grounded state data. + * 刷新地面状态数据。 + */ + virtual void RefreshGrounded(); + + /** + * Refreshes lean data for grounded state. + * 刷新地面状态的倾斜数据。 + */ + virtual void RefreshGroundedLean(); + + /** + * Gets the relative acceleration amount for leaning. + * 获取用于倾斜的相对加速度量。 + * @return 2D vector of relative acceleration. 相对加速度的2D向量。 + */ + virtual FVector2f GetRelativeAccelerationAmount() const; + + /** + * Refreshes in-air state data. + * 刷新空中状态数据。 + */ + virtual void RefreshInAir(); + + /** + * Refreshes ground prediction data. + * 刷新地面预测数据。 + */ + virtual void RefreshGroundPrediction(); + + /** + * Refreshes lean data for in-air state. + * 刷新空中状态的倾斜数据。 + */ + virtual void RefreshInAirLean(); + + /** + * Refreshes the offset root bone state. + * 刷新偏移根骨骼状态。 + * @param Context Animation update context. 动画更新上下文。 + * @param Node Animation node reference. 动画节点引用。 + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + void RefreshOffsetRootBone(UPARAM(ref) + FAnimUpdateContext& Context, UPARAM(ref) + FAnimNodeReference& Node); + +public: + /** + * Gets the clamped curve value (0 to 1) for a given curve name. + * 获取给定曲线名称的限制曲线值(0到1)。 + * @param CurveName The name of the curve. 曲线名称。 + * @return The clamped curve value. 限制的曲线值。 + */ + float GetCurveValueClamped01(const FName& CurveName) const; + + /** + * Gets the named blend profile. + * 获取命名的混合配置文件。 + * @param BlendProfileName The name of the blend profile. 混合配置文件名称。 + * @return The blend profile. 混合配置文件。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + UBlendProfile* GetNamedBlendProfile(const FName& BlendProfileName) const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + FGameplayTagContainer GetAggregatedTags() const; + + /** + * Gets the yaw value for aim offset. + * 获取瞄准偏移的偏航值。 + * @return The aim offset yaw value. 瞄准偏移偏航值。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + float GetAOYawValue() const; + + /** + * Selects a cardinal direction based on an angle. + * 根据角度选择主要方向。 + * @param Angle The angle to evaluate. 要评估的角度。 + * @param DeadZone The dead zone for direction changes. 方向变化的死区。 + * @param CurrentDirection The current direction. 当前方向。 + * @param bUseCurrentDirection Whether to consider the current direction. 是否考虑当前方向。 + * @return The selected cardinal direction. 选择的主要方向。 + */ + EGMS_MovementDirection SelectCardinalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection CurrentDirection, bool bUseCurrentDirection) const; + + /** + * Selects an octagonal direction based on an angle. + * 根据角度选择八方向。 + * @param Angle The angle to evaluate. 要评估的角度。 + * @param DeadZone The dead zone for direction changes. 方向变化的死区。 + * @param CurrentDirection The current direction. 当前方向。 + * @param bUseCurrentDirection Whether to consider the current direction. 是否考虑当前方向。 + * @return The selected octagonal direction. 选择的八方向。 + */ + EGMS_MovementDirection_8Way SelectOctagonalDirectionFromAngle(float Angle, float DeadZone, EGMS_MovementDirection_8Way CurrentDirection, bool bUseCurrentDirection) const; + + /** + * Gets the opposite cardinal direction. + * 获取相反的主要方向。 + * @param CurrentDirection The current direction. 当前方向。 + * @return The opposite cardinal direction. 相反的主要方向。 + */ + EGMS_MovementDirection GetOppositeCardinalDirection(EGMS_MovementDirection CurrentDirection) const; + + /** + * Checks if any core state has changed (movement set, state, locomotion, rotation, or overlay mode). + * 检查是否有核心状态更改(运动集、状态、运动、旋转或叠层模式)。 + * @return True if any core state has changed. 如果任何核心状态更改则返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool HasCoreStateChanges() const; + + /** + * Checks if specified core states have changed. + * 检查指定的核心状态是否更改。 + * @param bCheckLocomotionMode Check locomotion mode changes. 检查运动模式更改。 + * @param bCheckMovementSet Check movement set changes. 检查运动集更改。 + * @param bCheckRotationMode Check rotation mode changes. 检查旋转模式更改。 + * @param bCheckMovementState Check movement state changes. 检查运动状态更改。 + * @param bCheckOverlayMode Check overlay mode changes. 检查叠层模式更改。 + * @return True if any specified state has changed. 如果任何指定状态更改则返回true。 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + bool CheckCoreStateChanges(bool bCheckLocomotionMode, bool bCheckMovementSet, bool bCheckRotationMode, bool bCheckMovementState, bool bCheckOverlayMode) const; + + /** + * Animation state name to gameplay tag mappings. + * 动画状态名称到游戏标签的映射。 + * @note Used to check if a state node is active via NodeRelevantTags. 用于通过NodeRelevantTags检查状态节点是否活跃。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(TitleProperty="Tag")) + TArray AnimStateNameToTagMapping; + + /** + * Current locomotion mode. + * 当前运动模式。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.LocomotionMode")) + FGameplayTag LocomotionMode{GMS_MovementModeTags::Grounded}; + + /** + * Container for locomotion mode (for chooser only). + * 运动模式的容器(仅用于选择器)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.LocomotionMode")) + FGameplayTagContainer LocomotionModeContainer; + + /** + * Current rotation mode. + * 当前旋转模式。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.RotationMode")) + FGameplayTag RotationMode{GMS_RotationModeTags::ViewDirection}; + + /** + * Container for rotation mode (for chooser only). + * 旋转模式的容器(仅用于选择器)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.RotationMode")) + FGameplayTagContainer RotationModeContainer; + + /** + * Current movement state. + * 当前运动状态。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.MovementState")) + FGameplayTag MovementState{GMS_MovementStateTags::Jog}; + + /** + * Container for movement state (for chooser only). + * 运动状态的容器(仅用于选择器)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.MovementState")) + FGameplayTagContainer MovementStateContainer; + + /** + * Current movement set. + * 当前运动集。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.MovementSet")) + FGameplayTag MovementSet; + + /** + * Container for movement set (for chooser only). + * 运动集的容器(仅用于选择器)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.MovementSet")) + FGameplayTagContainer MovementSetContainer; + + /** + * Current overlay mode. + * 当前叠层模式。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.OverlayMode")) + FGameplayTag OverlayMode{GMS_OverlayModeTags::Default}; + + /** + * Container for overlay mode (for chooser only). + * 叠层模式的容器(仅用于选择器)。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings", meta=(Categories="GMS.OverlayMode")) + FGameplayTagContainer OverlayModeContainer; + + /** + * Enables ground prediction. + * 启用地面预测。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings") + bool bEnableGroundPrediction{false}; + + /** + * General animation settings. + * 通用动画设置。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Settings") + FGMS_AnimDataSetting_General GeneralSetting; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings", meta=(BlueprintThreadSafe)) + TObjectPtr ControlSetting{nullptr}; + + + UPROPERTY(Transient) + FGMS_MovementBaseState MovementBase; + + /** + * Locomotion state for the game thread. + * 游戏线程的运动状态。 + */ + UPROPERTY(Transient) + FGMS_LocomotionState GameLocomotionState; + + /** + * Root bone animation state. + * 根骨骼动画状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Root RootState; + + + /** + * Locomotion animation state. + * 运动动画状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Locomotion LocomotionState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Trajectory TrajectoryState; + + /** + * View-related animation state. + * 视图相关动画状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_View ViewState; + + /** + * Lean animation state. + * 倾斜动画状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_Lean LeanState; + + /** + * Movement intent vector. + * 运动意图向量。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State", meta=(DisplayName="Movement Intent")) + FVector MovementIntent; + + /** + * In-air animation state. + * 空中动画状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="State") + FGMS_AnimState_InAir InAirState; + + /** + * Tags owned by the animation instance. + * 动画实例拥有的标签。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + FGameplayTagContainer OwnedTags; + + /** + * Tags indicating active animation nodes. + * 指示活跃动画节点的标签。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + FGameplayTagContainer NodeRelevanceTags; + + /** + * Indicates if any montage is playing. + * 指示是否有蒙太奇在播放。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bAnyMontagePlaying{false}; + + /** + * Indicates if the movement state has changed. + * 指示运动状态是否更改。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bMovementStateChanged{false}; + + /** + * Indicates if the locomotion mode has changed. + * 指示运动模式是否更改。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bLocomotionModeChanged{false}; + + /** + * Indicates if the movement set has changed. + * 指示运动集是否更改。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bMovementSetChanged{false}; + + /** + * Indicates if the rotation mode has changed. + * 指示旋转模式是否更改。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bRotationModeChanged{false}; + + /** + * Indicates if the overlay mode has changed. + * 指示叠层模式是否更改。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bOverlayModeChanged{false}; + + /** + * Indicates if movement is blocked. + * 指示移动是否被阻塞。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State", meta=(DisplayName="Movement Blocked")) + bool bBlocked{false}; + + /** + * Indicates if the character has just landed. + * 指示角色是否刚刚着陆。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="State") + bool bJustLanded{false}; + + /** + * Indicates if this is the first update. + * 指示这是否是第一次更新。 + */ + bool bFirstUpdate = true; + + UPROPERTY() + TScriptInterface TrajectoryPredictor; + +protected: + /** + * Reference to the movement system component. + * 运动系统组件的引用。 + */ + UPROPERTY() + TObjectPtr MovementSystem; + + + /** + * Reference to the owning pawn. + * 拥有Pawn的引用。 + */ + UPROPERTY(Transient) + TObjectPtr PawnOwner; + + /** + * Runtime mappings of animation state names to tags. + * 动画状态名称到标签的运行时映射。 + */ + UPROPERTY() + TMap, FGMS_AnimStateNameToTagWrapper> RuntimeAnimStateNameToTagMappings; + + /** + * Timer handle for initial updates. + * 初始更新的计时器句柄。 + */ + FTimerHandle InitialTimerHandle; + + /** + * Animation layer instance for states. + * 状态动画层实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="AnimLayers", meta=(ShowInnerProperties)) + TObjectPtr StateLayerInstance; + + /** + * Animation layer instance for overlays. + * 叠层动画层实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="AnimLayers", meta=(ShowInnerProperties)) + TObjectPtr OverlayLayerInstance; + + /** + * Animation layer instance for view. + * 视图动画层实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="AnimLayers", meta=(ShowInnerProperties)) + TObjectPtr ViewLayerInstance; + + /** + * Animation layer instance for additive animations. + * 附加动画层实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="AnimLayers", meta=(ShowInnerProperties)) + TObjectPtr AdditiveLayerInstance; + + /** + * Animation layer instance for skeletal controls. + * 骨骼控制动画层实例。 + */ + UPROPERTY(VisibleInstanceOnly, Category="AnimLayers", meta=(ShowInnerProperties)) + TObjectPtr SkeletonControlsLayerInstance; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Movement/GMS_CharacterMovementComponent.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Movement/GMS_CharacterMovementComponent.h new file mode 100644 index 0000000..4d9f9c6 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Movement/GMS_CharacterMovementComponent.h @@ -0,0 +1,57 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// #pragma once +// +// #include "CoreMinimal.h" +// #include "GameFramework/CharacterMovementComponent.h" +// #include "Locomotions/GMS_LocomotionStructLibrary.h" +// #include "GMS_CharacterMovementComponent.generated.h" +// +// class UGMS_CharacterMovementSystemComponent; +// class UGMS_CMC_MovementMode; +// +// UCLASS(ClassGroup=(GMS), Blueprintable) +// class GENERICMOVEMENTSYSTEM_API UGMS_CharacterMovementComponent : public UCharacterMovementComponent +// { +// GENERATED_BODY() +// +// friend UGMS_CMC_MovementMode; +// +// public: +// // Sets default values for this component's properties +// UGMS_CharacterMovementComponent(const FObjectInitializer& ObjectInitializer); +// +// protected: +// virtual void InitializeComponent() override; +// virtual void SetUpdatedComponent(USceneComponent* NewUpdatedComponent) override; +// virtual bool HasValidData() const override; +// +// virtual void TickCharacterPose(float DeltaTime) override; +// virtual void PhysicsRotation(float DeltaTime) override; +// +// void GMS_TurnToDesiredRotation(const FRotator& CurrentRotation, FRotator DesiredRotation, FRotator DeltaRot); +// +// void GMS_TurnToDesiredRotationWithRotationRate(const FRotator& CurrentRotation, FRotator DesiredRotation, FRotator DeltaRot); +// +// +// UFUNCTION(BlueprintNativeEvent, Category="GMS|Movement", meta=(DisplayName="Physics Rotation")) +// void GMS_PhysicsRotation(float DeltaTime); +// +// UFUNCTION(BlueprintNativeEvent, Category="GMS|Movement", meta=(DisplayName="Compute Orient To Desired Movement Rotation")) +// FRotator GMS_ComputeOrientToDesiredMovementRotation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const; +// +// UFUNCTION(BlueprintNativeEvent, Category="GMS|Movement", meta=(DisplayName="Compute Orient To Desired View Rotation")) +// FRotator GMS_ComputeOrientToDesiredViewRotation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const; +// +// UFUNCTION(BlueprintNativeEvent, Category="GMS|Movement", meta=(DisplayName="Compute Orient To Desired View Rotation")) +// bool HasAnimationRotationDeltaYawAngle(float DeltaTime, float& OutDeltaYawAngle) const; +// +// /** +// * Using the default physic rotation of CMC, instead of GMS one. +// */ +// UPROPERTY(Category="Character Movement (Rotation Settings)", EditAnywhere, BlueprintReadWrite) +// bool bUseNativeRotation{false}; +// +// UPROPERTY(Transient, DuplicateTransient) +// TObjectPtr MovementSystem; +// }; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Flying/GMS_FlyingMode.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Flying/GMS_FlyingMode.h new file mode 100644 index 0000000..246f2b1 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Flying/GMS_FlyingMode.h @@ -0,0 +1,16 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "DefaultMovementSet/Modes/FlyingMode.h" +#include "GMS_FlyingMode.generated.h" + +/** + * + */ +UCLASS() +class GENERICMOVEMENTSYSTEM_API UGMS_FlyingMode : public UFlyingMode +{ + GENERATED_BODY() +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverSettingObjectLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverSettingObjectLibrary.h new file mode 100644 index 0000000..dfdc22e --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverSettingObjectLibrary.h @@ -0,0 +1,18 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "MovementMode.h" +#include "UObject/Object.h" +#include "GMS_MoverSettingObjectLibrary.generated.h" + + +/** + * CommonLegacyMovementSettings: collection of settings that are shared between several of the legacy movement modes + */ +UCLASS(BlueprintType) +class GENERICMOVEMENTSYSTEM_API UGMS_MoverGroundedMovementSettings : public UObject +{ + GENERATED_BODY() +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverStructLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverStructLibrary.h new file mode 100644 index 0000000..842f3b8 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/GMS_MoverStructLibrary.h @@ -0,0 +1,159 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "MoverTypes.h" +#include "GMS_MoverStructLibrary.generated.h" + +/** + * Data block containing extended movement actions inputs used by Generic Movement System. + * 包含GMS所用到的运动行为输入定义的数据块。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MoverActionInputs : public FMoverDataStructBase +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY(BlueprintReadWrite, Category = Mover) + bool bIsDashJustPressed = false; + + UPROPERTY(BlueprintReadWrite, Category = Mover) + bool bIsAimPressed = false; + + UPROPERTY(BlueprintReadWrite, Category = Mover) + bool bIsVaultJustPressed = false; + + UPROPERTY(BlueprintReadWrite, Category = Mover) + bool bWantsToStartZiplining = false; + + UPROPERTY(BlueprintReadWrite, Category = Mover) + bool bWantsToBeCrouched = false; + + // @return newly allocated copy of this FGMS_MoverActionInputs. Must be overridden by child classes + virtual FMoverDataStructBase* Clone() const override + { + // TODO: ensure that this memory allocation jives with deletion method + FGMS_MoverActionInputs* CopyPtr = new FGMS_MoverActionInputs(*this); + return CopyPtr; + } + + virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override + { + Super::NetSerialize(Ar, Map, bOutSuccess); + + Ar.SerializeBits(&bIsDashJustPressed, 1); + Ar.SerializeBits(&bIsAimPressed, 1); + Ar.SerializeBits(&bIsVaultJustPressed, 1); + Ar.SerializeBits(&bWantsToStartZiplining, 1); + Ar.SerializeBits(&bWantsToBeCrouched, 1); + + bOutSuccess = true; + return true; + } + + virtual UScriptStruct* GetScriptStruct() const override { return StaticStruct(); } + + virtual void ToString(FAnsiStringBuilderBase& Out) const override + { + Super::ToString(Out); + Out.Appendf("bIsDashJustPressed: %i\n", bIsDashJustPressed); + Out.Appendf("bIsAimPressed: %i\n", bIsAimPressed); + Out.Appendf("bIsVaultJustPressed: %i\n", bIsVaultJustPressed); + Out.Appendf("bWantsToStartZiplining: %i\n", bWantsToStartZiplining); + Out.Appendf("bWantsToBeCrouched: %i\n", bWantsToBeCrouched); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override { Super::AddReferencedObjects(Collector); } +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MoverTagInputs : public FMoverDataStructBase +{ + GENERATED_USTRUCT_BODY() + + /** + * A container which you can query tag to indicate a input is requested. + */ + UPROPERTY(BlueprintReadWrite, Category = Mover) + FGameplayTagContainer Tags; + + // @return newly allocated copy of this FGMS_MoverActionInputs. Must be overridden by child classes + virtual FMoverDataStructBase* Clone() const override + { + // TODO: ensure that this memory allocation jives with deletion method + FGMS_MoverTagInputs* CopyPtr = new FGMS_MoverTagInputs(*this); + return CopyPtr; + } + + virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override + { + bool bSuccess = Super::NetSerialize(Ar, Map, bOutSuccess); + + Tags.NetSerialize(Ar, Map, bSuccess); + return bSuccess; + } + + virtual UScriptStruct* GetScriptStruct() const override { return StaticStruct(); } + + virtual void ToString(FAnsiStringBuilderBase& Out) const override + { + Super::ToString(Out); + Out.Appendf("MoverTagInputs Tags[%s] \n", *Tags.ToString()); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override { Super::AddReferencedObjects(Collector); } +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MoverMovementControlInputs : public FMoverDataStructBase +{ + GENERATED_USTRUCT_BODY() + + /** + * 当前是什么运动集?影响角色动画表现方式。 + */ + UPROPERTY(BlueprintReadWrite, Category = Mover) + FGameplayTag DesiredMovementSet; + + /** + * 想以什么状态移动?影响角色移动参数 + */ + UPROPERTY(BlueprintReadWrite, Category = Mover) + FGameplayTag DesiredMovementState; + + /** + * 想以什么方式旋转?影响角色的朝向方式。 + */ + UPROPERTY(BlueprintReadWrite, Category = Mover) + FGameplayTag DesiredRotationMode; + + // @return newly allocated copy of this FGMS_MoverActionInputs. Must be overridden by child classes + virtual FMoverDataStructBase* Clone() const override + { + // TODO: ensure that this memory allocation jives with deletion method + FGMS_MoverMovementControlInputs* CopyPtr = new FGMS_MoverMovementControlInputs(*this); + return CopyPtr; + } + + virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override + { + bool bSuccess = Super::NetSerialize(Ar, Map, bOutSuccess); + DesiredMovementSet.NetSerialize(Ar, Map, bSuccess); + DesiredMovementState.NetSerialize(Ar, Map, bSuccess); + DesiredRotationMode.NetSerialize(Ar, Map, bSuccess); + return bSuccess; + } + + virtual UScriptStruct* GetScriptStruct() const override { return StaticStruct(); } + + virtual void ToString(FAnsiStringBuilderBase& Out) const override + { + Super::ToString(Out); + Out.Appendf("DesiredMovementSet [%s] \n", *DesiredMovementSet.ToString()); + Out.Appendf("DesiredMovementState [%s] \n", *DesiredMovementState.ToString()); + Out.Appendf("DesiredRotationMode [%s] \n", *DesiredRotationMode.ToString()); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override { Super::AddReferencedObjects(Collector); } +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Modifers/GMS_MovementStateModifer.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Modifers/GMS_MovementStateModifer.h new file mode 100644 index 0000000..642edac --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Modifers/GMS_MovementStateModifer.h @@ -0,0 +1,84 @@ +// // Copyright 2025 https://yuewu.dev/en All Rights Reserved. +// +// #pragma once +// +// #include "CoreMinimal.h" +// #include "MovementModifier.h" +// #include "UObject/Object.h" +// #include "GMS_MovementStateModifer.generated.h" +// +// +// USTRUCT(BlueprintType) +// struct FGMS_MovementStateModifierEntry +// { +// GENERATED_BODY() +// +// /** Maximum speed in the movement plane */ +// UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="General", meta = (ClampMin = "0", UIMin = "0", ForceUnits = "cm/s")) +// float MaxSpeed = 800.f; +// +// /** Default max linear rate of deceleration when there is no controlled input */ +// UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="General", meta = (ClampMin = "0", UIMin = "0", ForceUnits = "cm/s^2")) +// float Deceleration = 4000.f; +// +// /** Default max linear rate of acceleration for controlled input. May be scaled based on magnitude of input. */ +// UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="General", meta = (ClampMin = "0", UIMin = "0", ForceUnits = "cm/s^2")) +// float Acceleration = 4000.f; +// +// /** Maximum rate of turning rotation (degrees per second). Negative numbers indicate instant rotation and should cause rotation to snap instantly to desired direction. */ +// UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="General", meta = (ClampMin = "-1", UIMin = "0", ForceUnits = "degrees/s")) +// float TurningRate = 500.f; +// +// /** Speeds velocity direction changes while turning, to reduce sliding */ +// UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="General", meta = (ClampMin = "0", UIMin = "0", ForceUnits = "Multiplier")) +// float TurningBoost = 8.f; +// }; +// +// +// /** +// * States: Applies settings to the actor to make them go into different states like walk or jog, sprint, affects actor speed and acceleration/deceleration, turn etc, +// */ +// USTRUCT(BlueprintType) +// struct MOVER_API FGMS_MovementStateModifier : public FMovementModifierBase +// { +// GENERATED_BODY() +// +// FGMS_MovementStateModifier(); +// +// virtual ~FGMS_MovementStateModifier() override +// { +// } +// +// UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Modifer") +// bool bRevertMovementSettingsOnEnd{false}; +// +// /** Fired when this modifier is activated. */ +// virtual void OnStart(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) override; +// +// /** Fired when this modifier is deactivated. */ +// virtual void OnEnd(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) override; +// +// /** Fired just before a Substep */ +// virtual void OnPreMovement(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep) override; +// +// /** Fired after a Substep */ +// virtual void OnPostMovement(UMoverComponent* MoverComp, const FMoverTimeStep& TimeStep, const FMoverSyncState& SyncState, const FMoverAuxStateContext& AuxState) override; +// +// // @return newly allocated copy of this FMovementModifier. Must be overridden by child classes +// virtual FMovementModifierBase* Clone() const override; +// +// virtual void NetSerialize(FArchive& Ar) override; +// +// virtual UScriptStruct* GetScriptStruct() const override; +// +// virtual FString ToSimpleString() const override; +// +// virtual void AddReferencedObjects(class FReferenceCollector& Collector) override; +// +// protected: +// // Applies any movement settings like acceleration or max speed changes +// void ApplyMovementSettings(UMoverComponent* MoverComp); +// +// // Reverts any movement settings like acceleration or max speed changes +// void RevertMovementSettings(UMoverComponent* MoverComp); +// }; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Walking/GMS_WalkingMode.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Walking/GMS_WalkingMode.h new file mode 100644 index 0000000..266ad3b --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Walking/GMS_WalkingMode.h @@ -0,0 +1,20 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "DefaultMovementSet/Modes/WalkingMode.h" +#include "GMS_WalkingMode.generated.h" + +class UGMS_MoverGroundedMovementSettings; +/** + * + */ +UCLASS() +class GENERICMOVEMENTSYSTEM_API UGMS_WalkingMode : public UWalkingMode +{ + GENERATED_BODY() + +public: + UGMS_WalkingMode(const FObjectInitializer& ObjectInitializer); +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineInterface.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineInterface.h new file mode 100644 index 0000000..1dfc75b --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineInterface.h @@ -0,0 +1,30 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GMS_ZiplineInterface.generated.h" + +// This class does not need to be modified. +UINTERFACE() +class GENERICMOVEMENTSYSTEM_API UGMS_ZiplineInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class GENERICMOVEMENTSYSTEM_API IGMS_ZiplineInterface +{ + GENERATED_BODY() + + // Add interface functions to this class. This is the class that will be inherited to implement this interface. +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Zipline") + USceneComponent* GetStartComponent(); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Zipline") + USceneComponent* GetEndComponent(); +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineModeTransition.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineModeTransition.h new file mode 100644 index 0000000..d5dc5d1 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZiplineModeTransition.h @@ -0,0 +1,51 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "GMS_ZipliningMode.h" +#include "MovementModeTransition.h" +#include "GMS_ZiplineModeTransition.generated.h" + + +/** + * Transition that handles starting ziplining based on input. Character must be airborne to catch the + * zipline, regardless of input. + */ +UCLASS(Blueprintable, BlueprintType) +class UGMS_ZiplineStartTransition : public UBaseMovementModeTransition +{ + GENERATED_UCLASS_BODY() +#if ENGINE_MINOR_VERSION >= 6 + virtual FTransitionEvalResult Evaluate_Implementation(const FSimulationTickParams& Params) const override; +#else + virtual FTransitionEvalResult OnEvaluate(const FSimulationTickParams& Params) const override; +#endif + + UPROPERTY(EditAnywhere, Category = "Ziplining") + FName ZipliningModeName = ExtendedModeNames::Ziplining; + + UPROPERTY(EditAnywhere, Category = "Ziplining") + FGameplayTag ZipliningInputTag; +}; + +/** + * Transition that handles exiting ziplining based on input + */ +UCLASS(Blueprintable, BlueprintType) +class UGMS_ZiplineEndTransition : public UBaseMovementModeTransition +{ + GENERATED_UCLASS_BODY() +#if ENGINE_MINOR_VERSION >= 6 + virtual FTransitionEvalResult Evaluate_Implementation(const FSimulationTickParams& Params) const override; + virtual void Trigger_Implementation(const FSimulationTickParams& Params) override; +#else + virtual FTransitionEvalResult OnEvaluate(const FSimulationTickParams& Params) const override; + virtual void OnTrigger(const FSimulationTickParams& Params) override; +#endif + + // Mode to enter when exiting the zipline + UPROPERTY(EditAnywhere, Category = "Ziplining") + FName AutoExitToMode = DefaultModeNames::Falling; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZipliningMode.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZipliningMode.h new file mode 100644 index 0000000..35a2f9f --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Mover/Zipline/GMS_ZipliningMode.h @@ -0,0 +1,55 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "MovementMode.h" +#include "GMS_ZipliningMode.generated.h" + +namespace ExtendedModeNames +{ + const FName Ziplining = TEXT("Ziplining"); +} + + +// ZipliningMode: movement mode that traverses an actor implementing the IZipline interface +UCLASS(Blueprintable, BlueprintType) +class UGMS_ZipliningMode : public UBaseMovementMode +{ + GENERATED_UCLASS_BODY() +#if ENGINE_MINOR_VERSION >=6 + virtual void GenerateMove_Implementation(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const override; + virtual void SimulationTick_Implementation(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) override; +#else + virtual void OnGenerateMove(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const override; + virtual void OnSimulationTick(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) override; +#endif + + // Maximum speed + UPROPERTY(EditAnywhere, Category = "Ziplining", meta = (ClampMin = "1", UIMin = "1", ForceUnits = "cm/s")) + float MaxSpeed = 1000.0f; +}; + + +// Data block containing ziplining state info, used while ZipliningMode is active +USTRUCT() +struct FGMS_ZipliningState : public FMoverDataStructBase +{ + GENERATED_USTRUCT_BODY() + + TObjectPtr ZiplineActor; + bool bIsMovingAtoB; + + FGMS_ZipliningState() + : bIsMovingAtoB(true) + { + } + + virtual FMoverDataStructBase* Clone() const override; + virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override; + virtual UScriptStruct* GetScriptStruct() const override { return StaticStruct(); } + virtual void ToString(FAnsiStringBuilderBase& Out) const override; + virtual bool ShouldReconcile(const FMoverDataStructBase& AuthorityState) const override; + virtual void Interpolate(const FMoverDataStructBase& From, const FMoverDataStructBase& To, float Pct) override; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_CurvesBlend.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_CurvesBlend.h new file mode 100644 index 0000000..5182574 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_CurvesBlend.h @@ -0,0 +1,61 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + + +#pragma once + +#include "Animation/AnimNodeBase.h" +#include "GMS_AnimNode_CurvesBlend.generated.h" + +UENUM(BlueprintType) +enum class EGMS_CurvesBlendMode : uint8 +{ + // Blend poses using blend amount. Same as ECurveBlendOption::BlendByWeight. + BlendByAmount, + // Only set the value if the curves pose has the curve value. Same as ECurveBlendOption::Override. + Combine, + // Only set the value if the source pose doesn't have the curve value. Same as ECurveBlendOption::DoNotOverride. + CombinePreserved, + // Find the highest curve value from multiple poses and use that. Same as ECurveBlendOption::UseMaxValue. + UseMaxValue, + // Find the lowest curve value from multiple poses and use that. Same as ECurveBlendOption::UseMinValue. + UseMinValue, + // Completely override source pose. Same as ECurveBlendOption::UseBasePose. + Override +}; + +USTRUCT(BlueprintInternalUseOnly) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimNode_CurvesBlend : public FAnimNode_Base +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings") + FPoseLink SourcePose; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings") + FPoseLink CurvesPose; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = "Settings", Meta = (ClampMin = 0, ClampMax = 1, FoldProperty, PinShownByDefault)) + float BlendAmount{1.0f}; + + UPROPERTY(EditAnywhere, Category = "Settings", Meta = (FoldProperty)) + EGMS_CurvesBlendMode BlendMode{EGMS_CurvesBlendMode::BlendByAmount}; +#endif + +public: + virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override; + + virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override; + + virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override; + + virtual void Evaluate_AnyThread(FPoseContext& Output) override; + + virtual void GatherDebugData(FNodeDebugData& DebugData) override; + +public: + float GetBlendAmount() const; + + EGMS_CurvesBlendMode GetBlendMode() const; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_GameplayTagsBlend.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_GameplayTagsBlend.h new file mode 100644 index 0000000..a3893a4 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_GameplayTagsBlend.h @@ -0,0 +1,34 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "AnimNodes/AnimNode_BlendListBase.h" +#include "GMS_AnimNode_GameplayTagsBlend.generated.h" + +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimNode_GameplayTagsBlend : public FAnimNode_BlendListBase +{ + GENERATED_BODY() + +public: +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category="Settings", Meta = (FoldProperty, PinShownByDefault)) + FGameplayTag ActiveTag; + + UPROPERTY(EditAnywhere, Category="Settings", Meta = (FoldProperty)) + TArray Tags; +#endif + +protected: + virtual int32 GetActiveChildIndex() override; + +public: + const FGameplayTag& GetActiveTag() const; + + const TArray& GetTags() const; + +#if WITH_EDITOR + void RefreshPoses(); +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_LayeredBoneBlend.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_LayeredBoneBlend.h new file mode 100644 index 0000000..9260122 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_LayeredBoneBlend.h @@ -0,0 +1,168 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Animation/AnimTypes.h" +#include "Animation/AnimNodeBase.h" +#include "Animation/AnimData/BoneMaskFilter.h" +#include "Settings/GMS_SettingStructLibrary.h" +#include "GMS_AnimNode_LayeredBoneBlend.generated.h" + + +// Layered blend (per bone); has dynamic number of blendposes that can blend per different bone sets +USTRUCT(BlueprintInternalUseOnly) +struct FGMS_AnimNode_LayeredBoneBlend : public FAnimNode_Base +{ + GENERATED_USTRUCT_BODY() + +public: + /** The source pose */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Links) + FPoseLink BasePose; + + /** Each layer's blended pose */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, editfixedsize, Category=Links, meta=(BlueprintCompilerGeneratedDefaults)) + TArray BlendPoses; + + /** Whether to use branch filters or a blend mask to specify an input pose per-bone influence */ + // UPROPERTY(EditAnywhere, Category = Config) + // ELayeredBoneBlendMode BlendMode; + + /** + * The blend masks to use for our layer inputs. Allows the use of per-bone alphas. + * Blend masks are used when BlendMode is BlendMask. + */ + // UPROPERTY(EditAnywhere, editfixedsize, Category=Config, meta=(UseAsBlendMask=true)) + // TArray> BlendMasks; + + /** + * Configuration for the parts of the skeleton to blend for each layer. Allows + * certain parts of the tree to be blended out or omitted from the pose. + * LayerSetup is used when BlendMode is BranchFilter. + */ + UPROPERTY() + TArray LayerSetup; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(PinShownByDefault), Category=GMS) + FGMS_InputBlendPose ExternalLayerSetup; + + /** The weights of each layer */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, editfixedsize, Category=GMS, meta=(BlueprintCompilerGeneratedDefaults, PinShownByDefault)) + TArray BlendWeights; + + /** Whether to blend bone rotations in mesh space or in local space */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Config, meta=(PinShownByDefault)) + bool bMeshSpaceRotationBlend; + + /** Whether to blend bone scales in mesh space or in local space */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Config) + bool bMeshSpaceScaleBlend; + + /** How to blend the layers together */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Config) + TEnumAsByte CurveBlendOption; + + /** Whether to incorporate the per-bone blend weight of the root bone when lending root motion */ + UPROPERTY(EditAnywhere, Category = Config) + bool bBlendRootMotionBasedOnRootBone; + + bool bHasRelevantPoses; + + /* + * Max LOD that this node is allowed to run + * For example if you have LODThreshold to be 2, it will run until LOD 2 (based on 0 index) + * when the component LOD becomes 3, it will stop update/evaluate + * currently transition would be issue and that has to be re-visited + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Performance, meta = (DisplayName = "LOD Threshold")) + int32 LODThreshold; + +protected: + // Per-bone weights for the skeleton. Serialized as these are only relative to the skeleton, but can potentially + // be regenerated at runtime if the GUIDs dont match + UPROPERTY() + TArray PerBoneBlendWeights; + + // Guids for skeleton used to determine whether the PerBoneBlendWeights need rebuilding + UPROPERTY() + FGuid SkeletonGuid; + + // Guid for virtual bones used to determine whether the PerBoneBlendWeights need rebuilding + UPROPERTY() + FGuid VirtualBoneGuid; + + // transient data to handle weight and target weight + // this array changes based on required bones + TArray DesiredBoneBlendWeights; + TArray CurrentBoneBlendWeights; + + // Per-curve source pose index + TBaseBlendedCurve CurvePoseSourceIndices; + + // Serial number of the required bones container + uint16 RequiredBonesSerialNumber; + +public: + FGMS_AnimNode_LayeredBoneBlend() + : bMeshSpaceRotationBlend(false) + , bMeshSpaceScaleBlend(false) + , CurveBlendOption(ECurveBlendOption::Override) + , bBlendRootMotionBasedOnRootBone(true) + , bHasRelevantPoses(false) + , LODThreshold(INDEX_NONE) + , RequiredBonesSerialNumber(0) + { + } + + // FAnimNode_Base interface + GENERICMOVEMENTSYSTEM_API virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override; + GENERICMOVEMENTSYSTEM_API virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override; + GENERICMOVEMENTSYSTEM_API virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override; + GENERICMOVEMENTSYSTEM_API virtual void Evaluate_AnyThread(FPoseContext& Output) override; + GENERICMOVEMENTSYSTEM_API virtual void GatherDebugData(FNodeDebugData& DebugData) override; + virtual int32 GetLODThreshold() const override { return LODThreshold; } + // End of FAnimNode_Base interface + + void AddPose() + { + BlendWeights.Add(1.f); + BlendPoses.AddDefaulted(); + LayerSetup.AddDefaulted(); + } + + void RemovePose(int32 PoseIndex) + { + BlendWeights.RemoveAt(PoseIndex); + BlendPoses.RemoveAt(PoseIndex); + + if (LayerSetup.IsValidIndex(PoseIndex)) + { + LayerSetup.RemoveAt(PoseIndex); + } + } + + // Invalidate the cached per-bone blend weights from the skeleton + void InvalidatePerBoneBlendWeights() + { + RequiredBonesSerialNumber = 0; + SkeletonGuid = FGuid(); + VirtualBoneGuid = FGuid(); + } + + // Invalidates the cached bone data so it is recalculated the next time this node is updated + void InvalidateCachedBoneData() { RequiredBonesSerialNumber = 0; } + +public: + // Rebuild cache per bone blend weights from the skeleton + GENERICMOVEMENTSYSTEM_API void RebuildPerBoneBlendWeights(const USkeleton* InSkeleton); + + // Check whether per-bone blend weights are valid according to the skeleton (GUID check) + GENERICMOVEMENTSYSTEM_API bool ArePerBoneBlendWeightsValid(const USkeleton* InSkeleton) const; + + // Update cached data if required + GENERICMOVEMENTSYSTEM_API void UpdateCachedBoneData(const FBoneContainer& RequiredBones, const USkeleton* Skeleton); + + friend class UAnimGraphNode_LayeredBoneBlend; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_OrientationWarping.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_OrientationWarping.h new file mode 100644 index 0000000..0978b28 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Nodes/GMS_AnimNode_OrientationWarping.h @@ -0,0 +1,205 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "BoneControllers/AnimNode_OrientationWarping.h" +#include "BoneControllers/BoneControllerTypes.h" +#include "BoneControllers/AnimNode_SkeletalControlBase.h" +#include "Settings/GMS_SettingStructLibrary.h" +#include "GMS_AnimNode_OrientationWarping.generated.h" + +struct FAnimationInitializeContext; +struct FComponentSpacePoseContext; +struct FNodeDebugData; + +USTRUCT(BlueprintInternalUseOnly) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimNode_OrientationWarping : public FAnimNode_SkeletalControlBase +{ + GENERATED_BODY() + + // Orientation warping evaluation mode (Graph or Manual) + UPROPERTY(EditAnywhere,BlueprintReadWrite, Category=Evaluation, meta=(PinShownByDefault)) + EWarpingEvaluationMode Mode = EWarpingEvaluationMode::Manual; + + // The desired orientation angle (in degrees) to warp by relative to the specified RotationAxis + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinShownByDefault)) + float OrientationAngle = 0.f; + + // The character locomotion angle (in degrees) relative to the specified RotationAxis + // This will be used in the following equation for computing the orientation angle: [Orientation = RotationBetween(RootMotionDirection, LocomotionDirection)] + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinShownByDefault)) + float LocomotionAngle = 0.f; + + // The character movement direction vector in world space + // This will be used to compute LocomotionAngle automatically + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinShownByDefault)) + FVector LocomotionDirection = { 0.f, 0.f, 0.f }; + + // Minimum root motion speed required to apply orientation warping + // This is useful to prevent unnatural re-orientation when the animation has a portion with no root motion (i.e starts/stops/idles) + // When this value is greater than 0, it's recommended to enable interpolation with RotationInterpSpeed > 0 + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta = (ClampMin = "0.0", PinHiddenByDefault)) + float MinRootMotionSpeedThreshold = 10.0f; + + // Specifies an angle threshold to prevent erroneous over-rotation of the character, disabled with a value of 0 + // + // When the effective orientation warping angle is detected to be greater than this value (default: 90 degrees) the locomotion direction will be inverted prior to warping + // This will be used in the following equation: [Orientation = RotationBetween(RootMotionDirection, -LocomotionDirection)] + // + // Example: Playing a forward running animation while the motion is going backward + // Rather than orientation warping by 180 degrees, the system will warp by 0 degrees + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinHiddenByDefault), meta=(ClampMin="0.0", ClampMax="180.0")) + float LocomotionAngleDeltaThreshold = 90.f; + + // Spine bone definitions + // Used to counter rotate the body in order to keep the character facing forward + // The amount of counter rotation applied is driven by DistributedBoneOrientationAlpha + // UPROPERTY(EditAnywhere, Category="Settings") 由ExternalBoneReference代替代替。 + UPROPERTY() + TArray SpineBones; + + // IK Foot Root Bone definition + // UPROPERTY(EditAnywhere, Category="Settings", meta=(DisplayName="IK Foot Root Bone")) 由ExternalBoneReference代替。 + UPROPERTY() + FBoneReference IKFootRootBone; + + // IK Foot definitions + // UPROPERTY(EditAnywhere, Category="Settings", meta=(DisplayName="IK Foot Bones")) 由ExternalBoneReference代替。 + UPROPERTY() + TArray IKFootBones; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category= Settings, meta=(PinShownByDefault)) + FGMS_OrientationWarpingBoneReference ExternalBoneReference; + + // Rotation axis used when rotating the character body + UPROPERTY(EditAnywhere, Category=Settings) + TEnumAsByte RotationAxis = EAxis::Z; + + // Specifies how much rotation is applied to the character body versus IK feet + UPROPERTY(EditAnywhere, Category=Settings, meta=(ClampMin="0.0", ClampMax="1.0", PinHiddenByDefault)) + float DistributedBoneOrientationAlpha = 0.5f; + + // Specifies the interpolation speed (in Alpha per second) towards reaching the final warped rotation angle + // A value of 0 will cause instantaneous rotation, while a greater value will introduce smoothing + UPROPERTY(EditAnywhere, Category=Settings, meta=(ClampMin="0.0")) + float RotationInterpSpeed = 10.f; + + // Max correction we're allowed to do per-second when using interpolation. + // This minimizes pops when we have a large difference between current and target orientation. + UPROPERTY(EditAnywhere, Category=Settings, meta=(ClampMin="0.0", EditCondition="RotationInterpSpeed > 0.0f")) + float MaxCorrectionDegrees = 180.f; + + // Don't compensate our interpolator when the instantaneous root motion delta is higher than this. This is likely a pivot. + UPROPERTY(EditAnywhere, Category=Settings, meta=(ClampMin="0.0", EditCondition="RotationInterpSpeed > 0.0f")) + float MaxRootMotionDeltaToCompensateDegrees = 45.f; + + // Whether to counter compensate interpolation by the animated root motion angle change over time. + // This helps to conserve the motion from our animation. + // Disable this if your root motion is expected to be jittery, and you want orientation warping to smooth it out. + UPROPERTY(EditAnywhere, Category=Settings, meta=(EditCondition="RotationInterpSpeed > 0.0f")) + bool bCounterCompenstateInterpolationByRootMotion = true; + + UPROPERTY(EditAnywhere, Category=Experimental, meta=(PinHiddenByDefault)) + bool bScaleByGlobalBlendWeight = false; + + UPROPERTY(EditAnywhere, Category=Experimental, meta=(PinHiddenByDefault)) + bool bUseManualRootMotionVelocity = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Experimental, meta=(PinHiddenByDefault)) + FVector ManualRootMotionVelocity = FVector::ZeroVector; + + //RootBoneTransform is the same as CustomTransform as + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinHiddenByDefault)) + EOrientationWarpingSpace WarpingSpace = EOrientationWarpingSpace::ComponentTransform; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Evaluation, meta=(PinHiddenByDefault)) + FTransform WarpingSpaceTransform; + +#if WITH_EDITORONLY_DATA + // Scale all debug drawing visualization by a factor + UPROPERTY(EditAnywhere, Category=Debug, meta=(ClampMin="0.0")) + float DebugDrawScale = 1.f; + + // Enable/Disable orientation warping debug drawing + UPROPERTY(EditAnywhere, Category=Debug) + bool bEnableDebugDraw = false; +#endif + +public: + // FAnimNode_Base interface + virtual void GatherDebugData(FNodeDebugData& DebugData) override; + virtual void UpdateInternal(const FAnimationUpdateContext& Context) override; + // End of FAnimNode_Base interface + + // FAnimNode_SkeletalControlBase interface + virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override; + virtual void EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray& OutBoneTransforms) override; + virtual bool IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) override; + // End of FAnimNode_SkeletalControlBase interface + +private: + // FAnimNode_SkeletalControlBase interface + virtual void InitializeBoneReferences(const FBoneContainer& RequiredBones) override; + // End of FAnimNode_SkeletalControlBase interface + + struct FOrientationWarpingSpineBoneData + { + FCompactPoseBoneIndex BoneIndex; + float Weight; + + FOrientationWarpingSpineBoneData() + : BoneIndex(INDEX_NONE) + , Weight(0.f) + { + } + + FOrientationWarpingSpineBoneData(FCompactPoseBoneIndex InBoneIndex) + : BoneIndex(InBoneIndex) + , Weight(0.f) + { + } + + // Comparison Operator for Sorting + struct FCompareBoneIndex + { + FORCEINLINE bool operator()(const FOrientationWarpingSpineBoneData& A, const FOrientationWarpingSpineBoneData& B) const + { + return A.BoneIndex < B.BoneIndex; + } + }; + }; + + struct FOrientationWarpingFootData + { + TArray IKFootBoneIndexArray; + FCompactPoseBoneIndex IKFootRootBoneIndex; + + FOrientationWarpingFootData() + : IKFootBoneIndexArray() + , IKFootRootBoneIndex(INDEX_NONE) + { + } + }; + + // Computed spine bone indices and alpha weights for the specified spine definition + TArray SpineBoneDataArray; + + // Computed IK bone indices for the specified foot definitions + FOrientationWarpingFootData IKFootData; + + // Internal current frame root motion delta direction + FVector RootMotionDeltaDirection = FVector::ZeroVector; + + // Internal orientation warping angle + float ActualOrientationAngleRad = 0.f; + float BlendWeight = 0.0f; + + FGraphTraversalCounter UpdateCounter; + bool bIsFirstUpdate = false; + void Reset(const FAnimationBaseContext& Context); + +#if WITH_EDITORONLY_DATA + // Whether we found a root motion delta attribute in the attribute stream on graph driven mode + bool bFoundRootMotionAttribute = false; +#endif +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingEnumLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingEnumLibrary.h new file mode 100644 index 0000000..6cdfde5 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingEnumLibrary.h @@ -0,0 +1,78 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "GMS_SettingEnumLibrary.generated.h" + +/** + * Defines rotation behavior when the character is in the air. + * 定义角色在空中的旋转行为。 + */ +UENUM(BlueprintType) +enum class EGMS_InAirRotationMode : uint8 +{ + // Rotate to velocity direction on jump. 在跳跃时旋转到速度方向。 + RotateToVelocityOnJump, + // Maintain relative rotation. 保持相对旋转。 + KeepRelativeRotation, + // Maintain world rotation. 保持世界旋转。 + KeepWorldRotation +}; + +/** + * Defines how turn-in-place animations are triggered. + * 定义如何触发原地转向动画。 + */ +UENUM(BlueprintType) +enum class EGMS_TurnInPlacePlayMethod : uint8 +{ + Graph, // Trigger turn-in-place in animation graph. 在动画图中触发原地转向。 + Montage // Trigger turn-in-place as a dynamic slot montage. 作为动态槽蒙太奇触发原地转向。 +}; + +/** + * Defines how overlay animations are played. + * 定义如何播放覆盖动画。 + */ +UENUM(BlueprintType) +enum class EGMS_OverlayPlayMode : uint8 +{ + SequencePlayer, // Play as a sequence player. 作为序列播放器播放。 + SequenceEvaluator // Play as a sequence evaluator. 作为序列评估器播放。 +}; + +/** + * Defines the blending mode for layered bone animations. + * 定义分层骨骼动画的混合模式。 + */ +UENUM(BlueprintType) +enum class EGMS_LayeredBoneBlendMode : uint8 +{ + BranchFilter, // Use branch filter for blending. 使用分支过滤器进行混合。 + BlendMask // Use blend mask for blending. 使用混合蒙版进行混合。 +}; + +/** + * Defines how velocity direction is determined. + * 定义如何确定速度方向。 + */ +UENUM(BlueprintType) +enum class EGMS_VelocityDirectionMode_DEPRECATED : uint8 +{ + OrientToLastVelocityDirection, // Orient to the last velocity direction. 朝向最后的速度方向。 + OrientToInputDirection, // Orient to the input direction. 朝向输入方向。 + TurningCircle // Use a turning circle for orientation. 使用转向圆进行朝向。 +}; + +/** + * Defines view direction modes. + * 定义视图方向模式。 + */ +UENUM(BlueprintType) +enum class EGMS_ViewDirectionMode_DEPRECATED : uint8 +{ + Default, // Default view direction mode. 默认视图方向模式。 + Aiming // Aiming view direction mode. 瞄准视图方向模式。 +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingObjectLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingObjectLibrary.h new file mode 100644 index 0000000..207d362 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingObjectLibrary.h @@ -0,0 +1,221 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_SettingStructLibrary.h" +#include "UObject/Object.h" +#include "Engine/DataAsset.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MINOR_VERSION < 5 +#include "InstancedStruct.h" +#else +#include "StructUtils/InstancedStruct.h" +#endif +#include "Utility/GMS_Tags.h" +#include "GMS_SettingObjectLibrary.generated.h" + +#pragma region CommonSettings + +class UGMS_AnimLayer; +class UGMS_AnimLayerSetting; + +/** + * Defines multiple movement set settings for a character. + * 定义角色的多个运动集设置。 + */ +UCLASS(BlueprintType, Const) +class GENERICMOVEMENTSYSTEM_API UGMS_MovementDefinition : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Map of gameplay tags to movement set settings. + * 游戏标签到运动集设置的映射。 + */ + UPROPERTY(EditAnywhere, Category="GMS", meta=(ForceInlineRow)) + TMap MovementSets; + +#if WITH_EDITOR + /** + * Called before saving the asset in the editor. + * 在编辑器中保存资产前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; + +/** + * Stores animation graph-specific settings, one per unique skeleton. + * 存储动画图特定的设置,每个唯一骨架一个。 + */ +UCLASS() +class GENERICMOVEMENTSYSTEM_API UGMS_AnimGraphSetting : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Maps animation layer settings to their corresponding animation layer implementations. + * 将动画层设置映射到对应的动画层实现。 + * @note Add custom animation layer settings/implementations to this mapping. 将自定义动画层设置/实现添加到此映射。 + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings") + TMap, TSubclassOf> AnimLayerSettingToInstanceMapping; + + /** + * Settings for orientation warping. + * 朝向扭曲的设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings") + FGMS_OrientationWarpingSettings OrientationWarping; +}; + +#pragma endregion + +#pragma region ControlSettings + +/** + * Default Movement control settings. + * 默认运动控制设置。 + */ +UCLASS(BlueprintType, Blueprintable, Const, DisplayName="GMS Movement Control Setting") +class GENERICMOVEMENTSYSTEM_API UGMS_MovementControlSetting_Default : public UDataAsset +{ + GENERATED_BODY() + +public: + /** + * Matches a speed to a movement state tag based on a threshold. + * 根据阈值将速度匹配到运动状态标签。 + * @param Speed The current speed. 当前速度。 + * @param Threshold The speed threshold. 速度阈值。 + * @return The matching gameplay tag. 匹配的游戏标签。 + */ + FGameplayTag MatchStateTagBySpeed(float Speed, float Threshold) const; + + /** + * Gets a movement state setting by index. + * 通过索引获取运动状态设置。 + * @param Index The index of the state. 状态索引。 + * @param OutSetting The output movement state setting. 输出的运动状态设置。 + * @return True if found. 如果找到返回true。 + */ + bool GetStateByIndex(const int32& Index, FGMS_MovementStateSetting& OutSetting) const; + + /** + * Gets a movement state setting by speed level. + * 通过速度级别获取运动状态设置。 + * @param Level The speed level. 速度级别。 + * @param OutSetting The output movement state setting. 输出的运动状态设置。 + * @return True if found. 如果找到返回true。 + */ + bool GetStateBySpeedLevel(const int32& Level, FGMS_MovementStateSetting& OutSetting) const; + + /** + * Gets a movement state setting by tag. + * 通过标签获取运动状态设置。 + * @param Tag The gameplay tag. 游戏标签。 + * @param OutSetting The output movement state setting. 输出的运动状态设置。 + * @return True if found. 如果找到返回true。 + */ + bool GetStateByTag(const FGameplayTag& Tag, FGMS_MovementStateSetting& OutSetting) const; + + const FGMS_MovementStateSetting* GetMovementStateSetting(const FGameplayTag& Tag) const; + + const FGMS_MovementStateSetting* GetMovementStateSetting(const FGameplayTag& Tag, bool bHasFallback) const; + + /** + * Speed threshold for determining movement. + * 确定移动的速度阈值。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Control", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float MovingSpeedThreshold{50.0f}; + + /** + * Array of movement states, sorted by speed. + * 按速度排序的运动状态数组。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Control", meta=(TitleProperty="EditorFriendlyName")) + TArray MovementStates; + + /** + * Setting for velocity based rotation mode. + * 基于速率的旋转模式设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="RotationControl", meta=(ExcludeBaseStruct)) + TInstancedStruct VelocityDirectionSetting; + + /** + * Setting for view based rotation mode. + * 基于视角的旋转模式设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="RotationControl") + TInstancedStruct ViewDirectionSetting; + + /** + * Rotation mode when in air. + * 空中时的旋转模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="RotationControl") + EGMS_InAirRotationMode InAirRotationMode{EGMS_InAirRotationMode::KeepRelativeRotation}; + + /** + * Interpolation speed for in-air rotation. + * 空中旋转的插值速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="RotationControl", meta=(ClampMin=0)) + float InAirRotationInterpolationSpeed = 5.0f; + + /** + * Maps speed levels to movement state indices. + * 将速度级别映射到运动状态索引。 + */ + UPROPERTY(EditAnywhere, Category="Advanced", meta=(EditCondition=false, ForceInlineRow)) + TMap SpeedLevelToArrayIndex; + +#if WITH_EDITORONLY_DATA + + float MigrateRotationInterpolationSpeed(float Old); + /** + * Called before saving the asset in the editor. + * 在编辑器中保存资产前调用。 + * @param SaveContext The save context. 保存上下文。 + */ + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + + /** + * Validates data in the editor. + * 在编辑器中验证数据。 + * @param Context The validation context. 验证上下文。 + * @return The validation result. 验证结果。 + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; + +#pragma endregion + +#pragma region AnimSettings + +/** + * Base class for custom movement set user settings. + * 自定义运动集用户设置的基类。 + * @note Subclass to add custom settings without modifying movement set settings. 子类化以添加自定义设置,而无需修改运动集设置。 + */ +UCLASS(BlueprintType, Blueprintable, Abstract, Const, EditInlineNew, DefaultToInstanced) +class GENERICMOVEMENTSYSTEM_API UGMS_MovementSetUserSetting : public UObject +{ + GENERATED_BODY() +}; +#pragma endregion diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingStructLibrary.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingStructLibrary.h new file mode 100644 index 0000000..a68a942 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Settings/GMS_SettingStructLibrary.h @@ -0,0 +1,871 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GMS_SettingEnumLibrary.h" +#include "Animation/AnimData/BoneMaskFilter.h" +#include "UObject/Object.h" +#include "Curves/CurveFloat.h" +#include "Engine/EngineTypes.h" +#include "BoneControllers/BoneControllerTypes.h" +#include "Utility/GMS_Tags.h" +#include "GMS_SettingStructLibrary.generated.h" + +class UGMS_MovementSetUserSetting; +class UGMS_AnimLayerSetting_SkeletalControls; +class UGMS_MovementControlSetting_Default; +class UGMS_AnimLayerSetting_Additive; +class UGMS_CharacterMovementSetting; +class UGMS_CharacterRotationSetting; +class UGMS_AnimLayerSetting_View; +class UGMS_AnimLayerSetting_Overlay; +class UGMS_AnimLayerSetting_StateOverlays; +class UGMS_AnimLayerSetting_States; + +/** + * Stores bone references for orientation warping. + * 存储用于朝向扭曲的骨骼引用。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_OrientationWarpingBoneReference +{ + GENERATED_USTRUCT_BODY() + + /** + * Spine bones used to counter-rotate the body to keep facing forward. + * 用于反向旋转身体以保持向前朝向的脊椎骨骼。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TArray SpineBones; + + /** + * IK foot root bone definition. + * IK脚根骨骼定义。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference IKFootRootBone; + + /** + * IK foot bone definitions. + * IK脚骨骼定义。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + TArray IKFootBones; +}; + +/** + * Settings for the orientation warping node in the animation graph. + * 动画图中朝向扭曲节点的设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_OrientationWarpingSettings +{ + GENERATED_BODY() + + /** + * Evaluation mode for orientation warping (Graph or Manual). + * 朝向扭曲的评估模式(Graph或Manual)。 + * @note Use Manual mode for animations without root motion. 对于无根运动的动画使用Manual模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EWarpingEvaluationMode Mode = EWarpingEvaluationMode::Graph; + + /** + * Bone references for orientation warping. + * 朝向扭曲的骨骼引用。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS") + FGMS_OrientationWarpingBoneReference BoneReference; + + /** + * Specifies how much rotation is applied to the body versus IK feet. + * 指定身体与IK脚的旋转分配比例。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS", meta=(ClampMin="0.0", ClampMax="1.0")) + float DistributedBoneOrientationAlpha = 0.75f; + + /** + * Interpolation speed for reaching the final warped rotation angle (alpha per second). + * 达到最终扭曲旋转角度的插值速度(每秒alpha)。 + * @note 0 means instantaneous rotation; higher values introduce smoothing. 0表示瞬时旋转;较高值引入平滑。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS", meta=(ClampMin="0.0")) + float RotationInterpSpeed = 10.f; +}; + +/** + * Bone references for stride warping. + * 步幅适配的骨骼引用。 + */ +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_StrideWarpingBoneReference +{ + GENERATED_BODY() + + /** + * Pelvis bone reference. + * 骨盆骨骼引用。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference PelvisBone; + + /** + * IK foot root bone reference. + * IK脚根骨骼引用。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference IKFootRootBone; +}; + +/** + * Foot bone definitions for stride warping. + * 步幅适配的脚骨骼定义。 + */ +USTRUCT() +struct GENERICMOVEMENTSYSTEM_API FGMS_StrideWarpingFootDefinition +{ + GENERATED_BODY() + + /** + * IK foot bone. + * IK脚骨骼。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference IKFootBone; + + /** + * FK foot bone. + * FK脚骨骼。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference FKFootBone; + + /** + * Thigh bone. + * 大腿骨骼。 + */ + UPROPERTY(EditAnywhere, Category="GMS") + FBoneReference ThighBone; +}; + +/** + * Settings for the stride warping node in the animation graph. + * 动画图中步幅适配节点的设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_StrideWarpingSettings +{ + GENERATED_BODY() + + /** + * Enables or disables stride warping. + * 启用或禁用步幅适配。 + * @note Disable for non-humanoid characters. 对于非人形角色禁用。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS") + bool bEnabled{true}; + + /** + * Start time for blending in stride warping. + * 步幅适配混入的开始时间。 + * @note For animations with turns, set to when the character moves in a straight line. 对于带转身的动画,设置为角色直线移动的时刻。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS", meta=(ClampMin=0)) + float BlendInStartOffset{0.15f}; + + /** + * Duration for fully blending in stride warping. + * 步幅适配完全混入的持续时间。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS") + float BlendInDurationScaled{0.2f}; +}; + +/** + * Settings for the steering node in the animation graph. + * 动画图中Steering节点的设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_SteeringSettings +{ + GENERATED_BODY() + + /** + * Default constructor. + * 默认构造函数。 + */ + FGMS_SteeringSettings() + { + bEnabled = true; + ProceduralTargetTime = 0.2f; + AnimatedTargetTime = 0.5f; + } + + /** + * Constructor with parameters. + * 带参数的构造函数。 + * @param bInEnabled Whether steering is enabled. 是否启用Steering。 + * @param InProceduralTargetTime Procedural target time. 程序化目标时间。 + * @param InAnimatedTargetTime Animated target time. 动画目标时间。 + */ + FGMS_SteeringSettings(bool bInEnabled, float InProceduralTargetTime, float InAnimatedTargetTime) + { + bEnabled = bInEnabled; + ProceduralTargetTime = InProceduralTargetTime; + AnimatedTargetTime = InAnimatedTargetTime; + }; + + /** + * Enables or disables steering. + * 启用或禁用Steering。 + */ + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="GMS") + bool bEnabled{true}; + + /** + * Time to reach target orientation for animations without root motion rotation. + * 无根运动旋转动画达到目标朝向的时间。 + * @note Large values disable procedural turns. 大值禁用程序化转身。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float ProceduralTargetTime = 0.2f; + + /** + * Time to reach target orientation for animations with root motion rotation. + * 有根运动旋转动画达到目标朝向的时间。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float AnimatedTargetTime = 0.5f; +}; + +/** + * Wrapper for branch filters used in blueprint. + * 用于蓝图的分支过滤器包装器。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_InputBlendPose +{ + GENERATED_USTRUCT_BODY() + + /** + * Array of bone branch filters. + * 骨骼分支过滤器数组。 + */ + UPROPERTY(EditAnywhere, Category=Filter) + TArray BranchFilters; +}; + + +/** + * General settings for the main animation instance. + * 主动画实例的通用设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_AnimDataSetting_General +{ + GENERATED_BODY() + + /** + * Allows the pawn's root bone to rotate independently of the actor's rotation. + * 允许Pawn的根骨骼独立于Actor旋转进行旋转。 + * @note Global setting; animation layers should only change root rotation mode if enabled. 全局设置;动画层仅在启用时更改根旋转模式。 + * @details Enables complex rotational animations like turn-in-place via root motion. 通过根运动支持复杂的旋转动画,如原地转身。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common") + bool bEnableOffsetRootBoneRotation{true}; + + /** + * Controls the speed of blending out root bone rotation offset. + * 控制根骨骼旋转偏移混出的速度。 + * @note Closer to 0 is faster; 0 means instant blend out. 越接近0越快;0表示立即混出。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", Meta = (ClampMin = 0, ClampMax = 1, EditCondition="bEnableOffsetRootBoneRotation")) + float RootBoneRotationHalfLife{0.2}; + + /** + * Allows the pawn's root bone to translate independently of the actor's location. + * 允许Pawn的根骨骼独立于Actor位置进行平移。 + * @note Global setting; animation layers should only change root translation mode if enabled. 全局设置;动画层仅在启用时更改根平移模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common") + bool bEnableOffsetRootBoneTranslation{true}; + + /** + * Controls the speed of blending out root bone translation offset. + * 控制根骨骼位移偏移混出的速度。 + * @note Closer to 0 is faster; 0 means instant blend out. 越接近0越快;0表示立即混出。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", Meta = (ClampMin = 0, ClampMax = 1, EditCondition="bEnableOffsetRootBoneTranslation")) + float RootBoneTranslationHalfLife{0.2}; + + /** + * Controls the speed of lean interpolation in grounded or in-air states. + * 控制地面或空中状态下倾斜插值的速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", Meta = (ClampMin = 0)) + float LeanInterpolationSpeed{4.0f}; + + /** + * Speed threshold for determining movement in animation states. + * 动画状态中确定移动的速度阈值。 + */ + // UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Common", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + // float MovingSmoothSpeedThreshold{150.0f}; + + /** + * Curve mapping vertical velocity to lean amount in air. + * 空中垂直速度到倾斜量的曲线映射。 + * @note If empty, lean state is not refreshed in air. 如果为空,空中不刷新倾斜状态。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="InAir") + TObjectPtr InAirLeanAmountCurve{nullptr}; + + /** + * Collision channel for ground prediction sweep. + * 地面预测扫描的碰撞通道。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="InAir") + TEnumAsByte GroundPredictionSweepChannel{ECC_Visibility}; + + /** + * Response channels for ground prediction. + * 地面预测的响应通道。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="InAir") + TArray> GroundPredictionResponseChannels; + + /** + * Collision response container for ground prediction sweep. + * 地面预测扫描的碰撞响应容器。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="InAir") + FCollisionResponseContainer GroundPredictionSweepResponses{ECR_Ignore}; + +#if WITH_EDITOR + /** + * Handles property changes in the editor. + * 处理编辑器中的属性更改。 + * @param PropertyChangedEvent The property change event. 属性更改事件。 + */ + void PostEditChangeProperty(const FPropertyChangedEvent& PropertyChangedEvent); +#endif +}; + + +#pragma region Rotation Mode Settings + +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICMOVEMENTSYSTEM_API FGMS_RotationModeSetting +{ + GENERATED_BODY() + + /** + * Determines if turn-in-place animation follows actor rotation or drives it. + * 确定原地转身动画是跟随Actor旋转还是驱动Actor旋转。 + * @note If enabled, animation catches up with actor rotation; if disabled, it drives rotation. 如果启用,动画跟随Actor旋转;如果禁用,动画驱动旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bEnableRotationWhenNotMoving{true}; +}; + +/** + * Setting for view direction based rotation mode. + * 基于视角方向的旋转模式设置 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICMOVEMENTSYSTEM_API FGMS_ViewDirectionSetting_Base : public FGMS_RotationModeSetting +{ + GENERATED_BODY() + + /** + * Primary rotation interpolation speed for character's rotation. + * 角色的主要旋转插值速度。 + * @details Smaller value means faster InterpolationSpeed. 值越小,插值速度越大。 + * @note <=0 disables smoothing. <=0禁用平滑。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + float RotationInterpolationSpeed = 0.1f; + + /** + * Secondary(Extras) rotation interpolation speed for character's rotation. + * 角色的次要(额外)旋转插值速度。 + * @note <=0 disables smoothing <=0禁用平滑。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + float TargetYawAngleRotationSpeed = 800.0f; +}; + +/** + * Setting for deprecated view direction based rotation mode. + * 基于弃用的视角方向的旋转模式设置 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct FGMS_ViewDirectionSetting : public FGMS_ViewDirectionSetting_Base +{ + GENERATED_BODY() + + /** + * View direction mode (Default or Aiming). + * 视图方向模式(默认或瞄准)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_ViewDirectionMode_DEPRECATED DirectionMode{EGMS_ViewDirectionMode_DEPRECATED::Default}; + + /** + * Determines if turn-in-place animation follows actor rotation or drives it. + * 确定原地转身动画是跟随Actor旋转还是驱动Actor旋转。 + * @note If enabled, animation catches up with actor rotation; if disabled, it drives rotation. 如果启用,动画跟随Actor旋转;如果禁用,动画驱动旋转。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + bool bRotateToViewDirectionWhileNotMoving{true}; + + /** + * Minimum angle limit between character and camera orientation in aiming mode. + * 瞄准模式下角色与相机朝向的最小夹角限制。 + * @note Ensures character rotation keeps up with fast camera movement. 确保角色旋转跟上快速相机移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="DirectionMode == EGMS_ViewDirectionMode_DEPRECATED::Aiming", EditConditionHides)) + float MinAimingYawAngleLimit{90.0f}; +}; + +/** + * Setting for default view direction based rotation mode. + * 基于默认的视角方向的旋转模式设置 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ViewDirectionSetting_Default : public FGMS_ViewDirectionSetting_Base +{ + GENERATED_BODY() + + /** + * Primary rotation interpolation speed curve for character's rotation. + * 角色的主要旋转插值速度曲线。 + * @details This curve maps the speed level of each movement state to InterpolationSpeed, allowing different rotation rate on different movement state. 此曲线将每个运动状态的速度等级映射为插值速度,允许不同运动状态下有不同的旋转速度。 + * @note Only used when character is moving and have higher priority than "RotationInterpolationSpeed". 仅在角色移动时使用,比“RotationInterpolationSpeed”优先级更高。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + TObjectPtr RotationInterpolationSpeedCurve{nullptr}; +}; + +/** + * Setting for Aiming view direction based rotation mode. + * 基于瞄准视角方向的旋转模式设置 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_ViewDirectionSetting_Aiming : public FGMS_ViewDirectionSetting_Base +{ + GENERATED_BODY() + + /** + * Minimum angle limit between character and camera orientation in aiming mode. + * 瞄准模式下角色与相机朝向的最小夹角限制。 + * @note Ensures character rotation keeps up with fast camera movement. 确保角色旋转跟上快速相机移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float MinAimingYawAngleLimit{90.0f}; +}; + +/** + * Setting for velocity direction based rotation mode. + * 基于速率方向的旋转模式设置 + * @note Velocity can come from input ,acceleration, or last movement velocity; + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct GENERICMOVEMENTSYSTEM_API FGMS_VelocityDirectionSetting_Base : public FGMS_RotationModeSetting +{ + GENERATED_BODY() + + // If checked, the character will rotate relative to the object it is standing on in the velocity + // direction rotation mode, otherwise the character will ignore that object and keep its world rotation. + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings") + bool bInheritBaseRotation{false}; +}; + +/** + * Default setting for velocity direction based rotation mode. + * 基于默认速率方向的旋转模式设置 + */ +USTRUCT(BlueprintType, meta=(Hidden)) +struct FGMS_VelocityDirectionSetting : public FGMS_VelocityDirectionSetting_Base +{ + GENERATED_BODY() + + /** + * Velocity direction mode (e.g., orient to velocity or input). + * 速度方向模式(例如,朝向速度或输入)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + EGMS_VelocityDirectionMode_DEPRECATED DirectionMode{EGMS_VelocityDirectionMode_DEPRECATED::OrientToLastVelocityDirection}; + + /** + * Rotation rate for turning circle mode (degrees per second). + * 转向圆模式的旋转速率(度每秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(EditCondition="DirectionMode == EGMS_VelocityDirectionMode_DEPRECATED::TurningCircle", EditConditionHides)) + float TurningRate{30}; +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_VelocityDirectionSetting_Default : public FGMS_VelocityDirectionSetting_Base +{ + GENERATED_BODY() + + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMS") + bool bOrientateToMoveInputIntent = false; + + /** + * If true, the actor will continue orienting towards the last intended orientation (from input) even after movement intent input has ceased. + * This makes the character finish orienting after a quick stick flick from the player. If false, character will not turn without input. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMS") + bool bMaintainLastInputOrientation = false; + + /** + * Primary rotation interpolation speed for character's rotation. + * 角色的主要旋转插值速度。 + * @details Smaller value means faster InterpolationSpeed. 值越小,插值速度越大。 + * @note <=0 disables smoothing. <=0禁用平滑。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + float RotationInterpolationSpeed = 0.1f; + + /** + * Primary rotation interpolation speed curve for character's rotation. + * 角色的主要旋转插值速度曲线。 + * @details This curve maps the speed level of each movement state to InterpolationSpeed, allowing different rotation rate on different movement state. 此曲线将每个运动状态的速度等级映射为插值速度,允许不同运动状态下有不同的旋转速度。 + * @note Only used when character is moving and have higher priority than "RotationInterpolationSpeed". 仅在角色移动时使用,比“RotationInterpolationSpeed”优先级更高。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + TObjectPtr RotationInterpolationSpeedCurve{nullptr}; + + /** + * Secondary(Extras) moothing for character's rotation speed. + * 角色旋转速度的次平滑。 + * @note <=0 disables smoothing <=0禁用平滑。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + float TargetYawAngleRotationSpeed = 800.0f; +}; + +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_VelocityDirectionSetting_RateBased : public FGMS_VelocityDirectionSetting_Base +{ + GENERATED_BODY() + + /** + * Rotation rate for turning(degrees per second). + * 转向圆模式的旋转速率(度每秒)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float TurnRate{30}; + + /** + * Rotation rate for turning at different speed. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(ClampMin=0)) + TObjectPtr TurnRateSpeedCurve{nullptr}; +}; + +#pragma endregion + +/** + * Settings for in-air movement. + * 空中移动的设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_InAirSetting +{ + GENERATED_BODY() + + /** + * Curve mapping vertical velocity to lean amount. + * 垂直速度到倾斜量的曲线映射。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TObjectPtr LeanAmountCurve{nullptr}; + + /** + * Collision channel for ground prediction sweep. + * 地面预测扫描的碰撞通道。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TEnumAsByte GroundPredictionSweepChannel{ECC_Visibility}; + + /** + * Response channels for ground prediction. + * 地面预测的响应通道。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TArray> GroundPredictionResponseChannels; + + /** + * Collision response container for ground prediction sweep. + * 地面预测扫描的碰撞响应容器。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="GMS") + FCollisionResponseContainer GroundPredictionSweepResponses{ECR_Ignore}; +}; + +#pragma region Movement State Settings + +/** + * Settings for a single movement state (e.g., walk, jog, sprint). + * 单一运动状态的设置(例如,走、慢跑、冲刺)。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MovementStateSetting +{ + GENERATED_BODY() + + /** + * Gameplay tag identifying the movement state. + * 标识运动状态的游戏标签。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", meta=(Categories="GMS.MovementState")) + FGameplayTag Tag; + + /** + * Speed level of the movement state; <0 indicates reverse movement. + * 运动状态的速度级别;<0表示反向移动。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + int32 SpeedLevel = INDEX_NONE; + + /** + * Speed of the movement state. + * 运动状态的速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float Speed{375.0f}; + + /** + * Acceleration of the movement state. + * 运动状态的加速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float Acceleration = 800.f; + + /** + * Deceleration of the movement state. + * 运动状态的减速度。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + float BrakingDeceleration = 1024.f; + + /** + * Allowed rotation modes for this movement state. + * 此运动状态允许的旋转模式。 + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GMS") + TArray AllowedRotationModes{GMS_RotationModeTags::VelocityDirection, GMS_RotationModeTags::ViewDirection}; + + /** + * Equality operator for comparing with another movement state setting. + * 与另一个运动状态设置比较的相等运算符。 + * @param Lhs Left-hand side setting. 左侧设置。 + * @param RHS Right-hand side setting. 右侧设置。 + * @return True if tags match. 如果标签匹配返回true。 + */ + friend bool operator==(const FGMS_MovementStateSetting& Lhs, const FGMS_MovementStateSetting& RHS) + { + return Lhs.Tag == RHS.Tag; + } + + /** + * Inequality operator for comparing with another movement state setting. + * 与另一个运动状态设置比较的不等运算符。 + * @param Lhs Left-hand side setting. 左侧设置。 + * @param RHS Right-hand side setting. 右侧设置。 + * @return True if tags do not match. 如果标签不匹配返回true。 + */ + friend bool operator!=(const FGMS_MovementStateSetting& Lhs, const FGMS_MovementStateSetting& RHS) + { + return !(Lhs == RHS); + } + + /** + * Equality operator for comparing with a gameplay tag. + * 与游戏标签比较的相等运算符。 + * @param Other The gameplay tag to compare. 要比较的游戏标签。 + * @return True if tags match. 如果标签匹配返回true。 + */ + bool operator==(const FGameplayTag& Other) const + { + return Tag == Other; + } + + /** + * Inequality operator for comparing with a gameplay tag. + * 与游戏标签比较的不等运算符。 + * @param Other The gameplay tag to compare. 要比较的游戏标签。 + * @return True if tags do not match. 如果标签不匹配返回true。 + */ + bool operator!=(const FGameplayTag& Other) const + { + return Tag != Other; + } + +#if WITH_EDITORONLY_DATA + + /** + * Velocity direction settings for this movement state. + * 此运动状态的速度方向设置。 + */ + UE_DEPRECATED(1.5, "Settings for rotation mode was decoupd from movementstate, see Movement Control Setting.") + UPROPERTY(VisibleAnywhere, Category="Deprecated", meta=(EditCondition=false, EditConditionHides)) + FGMS_VelocityDirectionSetting VelocityDirectionSetting; + + /** + * View direction settings for this movement state. + * 此运动状态的视图方向设置。 + */ + UE_DEPRECATED(1.5, "Settings for rotation mode was decoupd from movementstate, see Movement Control Setting.") + UPROPERTY(VisibleAnywhere, Category="Deprecated", meta=(EditCondition=false, EditConditionHides)) + FGMS_ViewDirectionSetting ViewDirectionSetting; + + /** + * Primary smoothing for character's rotation speed. + * 角色旋转的主平滑速度。 + * @note <=0 disables smoothing. <=0禁用平滑。 + */ + UE_DEPRECATED(1.5, "Settings for rotation mode was decoupd from movementstate, see Movement Control Setting.") + UPROPERTY(VisibleAnywhere, Category="Deprecated", meta=(ClampMin=0, EditCondition=false, EditConditionHides)) + float RotationInterpolationSpeed = 12.0f; + + /** + * Speed for smoothing SmoothTargetYawAngle to TargetYawAngle. + * 将SmoothTargetYawAngle平滑到TargetYawAngle的速度。 + * @note <=0 disables smoothing (instant transition). <=0禁用平滑(瞬时过渡)。 + */ + UE_DEPRECATED(1.5, "Settings for rotation mode was decoupd from movementstate, see Movement Control Setting.") + UPROPERTY(VisibleAnywhere, Category="Deprecated", meta=(ClampMin=0, EditCondition=false, EditConditionHides)) + float TargetYawAngleRotationSpeed = 800.0f; + + /** + * Editor-friendly name for the movement state. + * 运动状态的编辑器友好名称。 + */ + UPROPERTY(EditAnywhere, Category="Settings", meta=(EditCondition=false, EditConditionHides)) + FString EditorFriendlyName; +#endif +}; + +#pragma endregion + +/** + * Defines control and animation settings for a movement set. + * 定义运动集的控制和动画设置。 + */ +USTRUCT(BlueprintType) +struct GENERICMOVEMENTSYSTEM_API FGMS_MovementSetSetting +{ + GENERATED_BODY() + + /** + * Default movement control setting. + * 默认运动控制设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Control") + TObjectPtr ControlSetting; + + /** + * Enables per-overlay mode control settings. + * 启用按叠层模式的控制设置。 + * @note If no control setting is found for an overlay mode, the default is used. 如果未找到叠层模式的控制设置,则使用默认设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Control") + bool bControlSettingPerOverlayMode{false}; + + /** + * Maps overlay modes to specific control settings. + * 将叠层模式映射到特定控制设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Control", meta=(Categories="GMS.OverlayMode", EditCondition="bControlSettingPerOverlayMode")) + TMap> ControlSettings; + + /** + * General animation settings shared across the movement set. + * 运动集共享的通用动画设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation") + FGMS_AnimDataSetting_General AnimDataSetting_General; + + /** + * Determines if instanced states settings are used. + * 确定是否使用实例化的状态设置。 + * @note Uncheck if multiple movement sets share the same states layer setting. 如果多个运动集共享相同的状态层设置,则取消勾选。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation") + bool bUseInstancedStatesSetting{true}; + + /** + * Settings for the states animation layer (instanced). + * 状态动画层的设置(实例化)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Animation", meta = (EditCondition="bUseInstancedStatesSetting", EditConditionHides, DisplayName="Anim Layer Setting (States)")) + TObjectPtr AnimLayerSetting_States; + + /** + * Settings for the states animation layer (non-instanced). + * 状态动画层的设置(非实例化)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation", + meta = (EditCondition="!bUseInstancedStatesSetting", EditConditionHides, DisplayName="Anim Layer Setting (States)")) + TObjectPtr DA_AnimLayerSetting_States; + + /** + * Determines if instanced overlay settings are used. + * 确定是否使用实例化的叠层设置。 + * @note Uncheck if multiple movement sets share the same overlay layer setting. 如果多个运动集共享相同的叠层设置,则取消勾选。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation") + bool bUseInstancedOverlaySetting{true}; + + /** + * Settings for the overlay animation layer (non-instanced). + * 叠层动画层的设置(非实例化)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation", + meta = (EditCondition="!bUseInstancedOverlaySetting", EditConditionHides, DisplayName="Anim Layer Setting (Overlay)")) + TObjectPtr DA_AnimLayerSetting_Overlay; + + /** + * Settings for the overlay animation layer (instanced). + * 叠层动画层的设置(实例化)。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Animation", + meta = (EditCondition="bUseInstancedOverlaySetting", EditConditionHides, DisplayName="Anim Layer Setting (Overlay)")) + TObjectPtr AnimLayerSetting_Overlay; + + /** + * Settings for the view animation layer. + * 视图动画层的设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Animation", meta = (DisplayName="Anim Layer Setting (View)")) + TObjectPtr AnimLayerSetting_View; + + /** + * Settings for the additive animation layer. + * 附加动画层的设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Animation", meta = (DisplayName="Anim Layer Setting (Additive)")) + TObjectPtr AnimLayerSetting_Additive; + + /** + * Settings for the skeletal controls animation layer. + * 骨骼控制动画层的设置。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Animation", meta = (DisplayName="Anim Layer Setting (SkeletalControls)")) + TObjectPtr AnimLayerSetting_SkeletalControls; + + /** + * Custom user settings for the movement set. + * 运动集的自定义用户设置。 + * @note Subclass UGMS_MovementSetUserSetting and consume in animation layer blueprints. 子类化UGMS_MovementSetUserSetting并在动画层蓝图中使用。 + * @example Access via GetMovementSystemComponent()->GetMovementSetSetting()->GetMovementSetUserSetting(SettingClass). 通过GetMovementSystemComponent()->GetMovementSetSetting()->GetMovementSetUserSetting(SettingClass)访问。 + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="Extension", meta=(AllowAbstract=false)) + TArray> UserSettings; +}; diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Constants.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Constants.h new file mode 100644 index 0000000..5537a84 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Constants.h @@ -0,0 +1,61 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GMS_Constants.generated.h" + +UCLASS(Meta = (BlueprintThreadSafe)) +class GENERICMOVEMENTSYSTEM_API UGMS_Constants : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Constants|Animation Slots", Meta = (ReturnDisplayName = "Slot Name")) + static const FName& TurnInPlaceSlotName(); + + // Other Animation Curves + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Constants|Animation Curves", Meta = (ReturnDisplayName = "Curve Name")) + static const FName& RotationYawSpeedCurveName(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Constants|Animation Curves", Meta = (ReturnDisplayName = "Curve Name")) + static const FName& RotationYawOffsetCurveName(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Constants|Animation Curves", Meta = (ReturnDisplayName = "Curve Name")) + static const FName& AllowTurnInPlaceCurveName(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Constants|Animation Curves", Meta = (ReturnDisplayName = "Curve Name")) + static const FName& AllowAimingCurveName(); +}; + +inline const FName& UGMS_Constants::TurnInPlaceSlotName() +{ + static const FName Name{TEXTVIEW("TurnInPlace")}; + return Name; +} + +inline const FName& UGMS_Constants::RotationYawSpeedCurveName() +{ + static const FName Name{TEXTVIEW("RotationYawSpeed")}; + return Name; +} + +inline const FName& UGMS_Constants::RotationYawOffsetCurveName() +{ + static const FName Name{TEXTVIEW("RotationYawOffset")}; + return Name; +} + +inline const FName& UGMS_Constants::AllowTurnInPlaceCurveName() +{ + static const FName Name{TEXTVIEW("AllowTurnInPlace")}; + return Name; +} + +inline const FName& UGMS_Constants::AllowAimingCurveName() +{ + static const FName Name{TEXTVIEW("AllowAiming")}; + return Name; +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Log.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Log.h new file mode 100644 index 0000000..da1c271 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Log.h @@ -0,0 +1,38 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" + +namespace GMSLog +{ + GENERICMOVEMENTSYSTEM_API extern const FName MessageLogName; +} + +DECLARE_STATS_GROUP(TEXT("GMS"), STATGROUP_GMS, STATCAT_Advanced) + +FString GetGMSLogContextString(const UObject* ContextObject = nullptr); + +GENERICMOVEMENTSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGMS, Log, All) + +GENERICMOVEMENTSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGMS_Animation, Log, All) + +#define GMS_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGMS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GMS_ANIMATION_LOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGMS_Animation, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GMS_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGMS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGMSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} + +#define GMS_ANIMATION_CLOG(Verbosity, Format, ...) \ +{ \ +UE_LOG(LogGMS_Animation, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGMSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \ +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Math.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Math.h new file mode 100644 index 0000000..cf41468 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Math.h @@ -0,0 +1,57 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GMS_Math.generated.h" + +UCLASS(Meta = (BlueprintThreadSafe)) +class GENERICMOVEMENTSYSTEM_API UGMS_Math : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + static constexpr auto Ln2{0.6931471805599453f}; // FMath::Loge(2.0f). + +public: + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Math Utility", Meta = (ReturnDisplayName = "Value")) + static float Clamp01(float Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Math Utility", Meta = (ReturnDisplayName = "Value")) + static float LerpClamped(float From, float To, float Ratio); + + //Output the frame-rate stable alpha used for smoothly lerp current to target. + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Math Utility", Meta = (ReturnDisplayName = "Alpha")) + static float DamperExactAlpha(float DeltaTime, float HalfLife); + + // HalfLife is the time it takes for the distance to the target to be reduced by half. + template + static ValueType DamperExact(const ValueType& Current, const ValueType& Target, float DeltaTime, float HalfLife); +}; + +inline float UGMS_Math::Clamp01(const float Value) +{ + return Value > 0.0f + ? Value < 1.0f + ? Value + : 1.0f + : 0.0f; +} + +inline float UGMS_Math::LerpClamped(const float From, const float To, const float Ratio) +{ + return From + (To - From) * Clamp01(Ratio); +} + +inline float UGMS_Math::DamperExactAlpha(const float DeltaTime, const float HalfLife) +{ + // https://theorangeduck.com/page/spring-roll-call#exactdamper + + return 1.0f - FMath::InvExpApprox(Ln2 / (HalfLife + UE_SMALL_NUMBER) * DeltaTime); +} + +template +ValueType UGMS_Math::DamperExact(const ValueType& Current, const ValueType& Target, const float DeltaTime, const float HalfLife) +{ + return FMath::Lerp(Current, Target, DamperExactAlpha(DeltaTime, HalfLife)); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Rotation.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Rotation.h new file mode 100644 index 0000000..a012afb --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Rotation.h @@ -0,0 +1,233 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GMS_Math.h" +#include "GMS_Rotation.generated.h" + +UCLASS(Meta = (BlueprintThreadSafe)) +class GENERICMOVEMENTSYSTEM_API UGMS_Rotation : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + static constexpr auto CounterClockwiseRotationAngleThreshold{5.0f}; + +public: + template requires std::is_floating_point_v + static constexpr ValueType RemapAngleForCounterClockwiseRotation(ValueType Angle); + + static VectorRegister4Double RemapRotationForCounterClockwiseRotation(const VectorRegister4Double& Rotation); + + // Remaps the angle from the [175, 180] range to [-185, -180]. Used to + // make the character rotate counterclockwise during a 180 degree turn. + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (ReturnDisplayName = "Angle")) + static float RemapAngleForCounterClockwiseRotation(float Angle); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (ReturnDisplayName = "Angle")) + static float LerpAngle(float From, float To, float Ratio); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (AutoCreateRefTerm = "From, To", ReturnDisplayName = "Rotation")) + static FRotator LerpRotation(const FRotator& From, const FRotator& To, float Ratio); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (ReturnDisplayName = "Angle")) + static float InterpolateAngleConstant(float Current, float Target, float DeltaTime, float Speed); + + /** + * Smoothly lerp current to target in fame-rate stable way. + * 以帧率稳定方式平滑地从Current过渡到Target。 + * @param HalfLife How fast to reach target. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (ReturnDisplayName = "Angle")) + static float DamperExactAngle(float Current, float Target, float DeltaTime, float HalfLife); + + // HalfLife is the time it takes for the distance to the target to be reduced by half. + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", + Meta = (AutoCreateRefTerm = "Current, Target", ReturnDisplayName = "Rotation")) + static FRotator DamperExactRotation(const FRotator& Current, const FRotator& Target, float DeltaTime, float HalfLife); + + // Same as FMath::QInterpTo(), but uses FQuat::FastLerp() instead of FQuat::Slerp(). + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (ReturnDisplayName = "Quaternion")) + static FQuat InterpolateQuaternionFast(const FQuat& Current, const FQuat& Target, float DeltaTime, float Speed); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Rotation Utility", Meta = (AutoCreateRefTerm = "TwistAxis", ReturnDisplayName = "Twist")) + static FQuat GetTwist(const FQuat& Quaternion, const FVector& TwistAxis = FVector::UpVector); +}; + +template requires std::is_floating_point_v +constexpr ValueType UGMS_Rotation::RemapAngleForCounterClockwiseRotation(const ValueType Angle) +{ + if (Angle > 180.0f - CounterClockwiseRotationAngleThreshold) + { + return Angle - 360.0f; + } + + return Angle; +} + +inline VectorRegister4Double UGMS_Rotation::RemapRotationForCounterClockwiseRotation(const VectorRegister4Double& Rotation) +{ + static constexpr auto RemapThreshold{ + MakeVectorRegisterDoubleConstant(180.0f - CounterClockwiseRotationAngleThreshold, 180.0f - CounterClockwiseRotationAngleThreshold, + 180.0f - CounterClockwiseRotationAngleThreshold, 180.0f - CounterClockwiseRotationAngleThreshold) + }; + + static constexpr auto RemapAngles{MakeVectorRegisterDoubleConstant(360.0f, 360.0f, 360.0f, 0.0f)}; + + const auto ReverseRotationMask{VectorCompareGE(Rotation, RemapThreshold)}; + + const auto ReversedRotation{VectorSubtract(Rotation, RemapAngles)}; + + return VectorSelect(ReverseRotationMask, ReversedRotation, Rotation); +} + +inline float UGMS_Rotation::RemapAngleForCounterClockwiseRotation(const float Angle) +{ + return RemapAngleForCounterClockwiseRotation(Angle); +} + +inline float UGMS_Rotation::LerpAngle(const float From, const float To, const float Ratio) +{ + auto Delta{FMath::UnwindDegrees(To - From)}; + Delta = RemapAngleForCounterClockwiseRotation(Delta); + + return FMath::UnwindDegrees(From + Delta * Ratio); +} + +inline FRotator UGMS_Rotation::LerpRotation(const FRotator& From, const FRotator& To, const float Ratio) +{ +#if PLATFORM_ENABLE_VECTORINTRINSICS + const auto FromRegister{VectorLoadFloat3_W0(&From)}; + const auto ToRegister{VectorLoadFloat3_W0(&To)}; + + auto Delta{VectorSubtract(ToRegister, FromRegister)}; + Delta = VectorNormalizeRotator(Delta); + + if (!VectorAnyGreaterThan(VectorAbs(Delta), GlobalVectorConstants::DoubleKindaSmallNumber)) + { + return To; + } + + Delta = RemapRotationForCounterClockwiseRotation(Delta); + + auto ResultRegister{VectorMultiplyAdd(Delta, VectorLoadFloat1(&Ratio), FromRegister)}; + ResultRegister = VectorNormalizeRotator(ResultRegister); + + FRotator Result; + VectorStoreFloat3(ResultRegister, &Result); + + return Result; +#else + auto Result{To - From}; + Result.Normalize(); + + Result.Pitch = RemapAngleForCounterClockwiseRotation(Result.Pitch); + Result.Yaw = RemapAngleForCounterClockwiseRotation(Result.Yaw); + Result.Roll = RemapAngleForCounterClockwiseRotation(Result.Roll); + + Result *= Ratio; + Result += From; + Result.Normalize(); + + return Result; +#endif +} + +inline float UGMS_Rotation::InterpolateAngleConstant(const float Current, const float Target, const float DeltaTime, const float Speed) +{ + auto Delta{FMath::UnwindDegrees(Target - Current)}; + const auto MaxDelta{Speed * DeltaTime}; + + if (Speed <= 0.0f || FMath::Abs(Delta) <= MaxDelta) + { + return Target; + } + + Delta = RemapAngleForCounterClockwiseRotation(Delta); + return FMath::UnwindDegrees(Current + FMath::Sign(Delta) * MaxDelta); +} + +inline float UGMS_Rotation::DamperExactAngle(const float Current, const float Target, const float DeltaTime, const float HalfLife) +{ + auto Delta{FMath::UnwindDegrees(Target - Current)}; + + if (FMath::IsNearlyZero(Delta, UE_KINDA_SMALL_NUMBER)) + { + return Target; + } + + Delta = RemapAngleForCounterClockwiseRotation(Delta); + + const auto Alpha{UGMS_Math::DamperExactAlpha(DeltaTime, HalfLife)}; + return FMath::UnwindDegrees(Current + Delta * Alpha); +} + +inline FRotator UGMS_Rotation::DamperExactRotation(const FRotator& Current, const FRotator& Target, + const float DeltaTime, const float HalfLife) +{ +#if PLATFORM_ENABLE_VECTORINTRINSICS + const auto CurrentRegister{VectorLoadFloat3_W0(&Current)}; + const auto TargetRegister{VectorLoadFloat3_W0(&Target)}; + + auto Delta{VectorSubtract(TargetRegister, CurrentRegister)}; + Delta = VectorNormalizeRotator(Delta); + + if (!VectorAnyGreaterThan(VectorAbs(Delta), GlobalVectorConstants::DoubleKindaSmallNumber)) + { + return Target; + } + + Delta = RemapRotationForCounterClockwiseRotation(Delta); + + const double Alpha{UGMS_Math::DamperExactAlpha(DeltaTime, HalfLife)}; + + auto ResultRegister{VectorMultiplyAdd(Delta, VectorLoadDouble1(&Alpha), CurrentRegister)}; + ResultRegister = VectorNormalizeRotator(ResultRegister); + + FRotator Result; + VectorStoreFloat3(ResultRegister, &Result); + + return Result; +#else + auto Result{Target - Current}; + Result.Normalize(); + + if (FMath::IsNearlyZero(Result.Pitch, UE_KINDA_SMALL_NUMBER) && + FMath::IsNearlyZero(Result.Yaw, UE_KINDA_SMALL_NUMBER) && + FMath::IsNearlyZero(Result.Roll, UE_KINDA_SMALL_NUMBER)) + { + return Target; + } + + Result.Pitch = RemapAngleForCounterClockwiseRotation(Result.Pitch); + Result.Yaw = RemapAngleForCounterClockwiseRotation(Result.Yaw); + Result.Roll = RemapAngleForCounterClockwiseRotation(Result.Roll); + + const auto Alpha{UGMS_Math::DamperExactAlpha(DeltaTime, HalfLife)}; + + Result *= Alpha; + Result += Current; + Result.Normalize(); + + return Result; +#endif +} + +inline FQuat UGMS_Rotation::InterpolateQuaternionFast(const FQuat& Current, const FQuat& Target, const float DeltaTime, const float Speed) +{ + if (Speed <= 0.0f || Current.Equals(Target)) + { + return Target; + } + + return FQuat::FastLerp(Current, Target, UGMS_Math::Clamp01(Speed * DeltaTime)).GetNormalized(); +} + +inline FQuat UGMS_Rotation::GetTwist(const FQuat& Quaternion, const FVector& TwistAxis) +{ + // Based on TQuat::ToSwingTwist(). + + const auto Projection{(TwistAxis | FVector{Quaternion.X, Quaternion.Y, Quaternion.Z}) * TwistAxis}; + + return FQuat{Projection.X, Projection.Y, Projection.Z, Quaternion.W}.GetNormalized(); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Tags.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Tags.h new file mode 100644 index 0000000..e6c22d3 --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Tags.h @@ -0,0 +1,49 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "NativeGameplayTags.h" + +namespace GMS_MovementModeTags +{ + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(None) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InAir) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Flying) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Swimming) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Zipline) +} + +namespace GMS_RotationModeTags +{ + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(VelocityDirection) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(ViewDirection) +} + +namespace GMS_MovementStateTags +{ + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Walk) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Jog) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Sprint) +} + +namespace GMS_OverlayModeTags +{ + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(None) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Default) +} + +namespace GMS_SMTags +{ + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Root) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InAir) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InAir_Jump) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InAir_Fall) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Idle) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Start) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Cycle) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Stop) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Pivot) + GENERICMOVEMENTSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Grounded_Land) +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Utility.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Utility.h new file mode 100644 index 0000000..217657a --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Utility.h @@ -0,0 +1,76 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Locomotions/GMS_LocomotionStructLibrary.h" +#include "Settings/GMS_SettingStructLibrary.h" +#include "GMS_Utility.generated.h" + +class UGMS_MovementSetUserSetting; +class UGMS_AnimLayer; +class UGMS_MainAnimInstance; +class UChooserTable; +class UPoseSearchDatabase; +class UAnimSequenceBase; + + +UCLASS() +class GENERICMOVEMENTSYSTEM_API UGMS_Utility : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + static constexpr auto DrawImpactPointSize{32.0f}; + static constexpr auto DrawLineThickness{1.0f}; + static constexpr auto DrawArrowSize{50.0f}; + static constexpr auto DrawCircleSidesCount{16}; + + static constexpr FStringView BoolToString(bool bValue); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Utility", Meta = (AutoCreateRefTerm = "Name", ReturnDisplayName = "Display String")) + static FString NameToDisplayString(const FName& Name, bool bNameIsBool); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Utility", + Meta = (DefaultToSelf = "Character", AutoCreateRefTerm = "CurveName", ReturnDisplayName = "Curve Value")) + static float GetAnimationCurveValueFromCharacter(const ACharacter* Character, const FName& CurveName); + + UFUNCTION(BlueprintCallable, Category="GMS|Utility", Meta = (AutoCreateRefTerm = "Tag", ReturnDisplayName = "Child Tags")) + static FGameplayTagContainer GetChildTags(const FGameplayTag& Tag); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Utility", Meta = (AutoCreateRefTerm = "Tag", ReturnDisplayName = "Tag Name")) + static FName GetSimpleTagName(const FGameplayTag& Tag); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Utility", + Meta = (DefaultToSelf = "Actor", AutoCreateRefTerm = "DisplayName", ReturnDisplayName = "Value")) + static bool ShouldDisplayDebugForActor(const AActor* Actor, const FName& DisplayName); + + UFUNCTION(BlueprintCallable, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + static float CalculateAnimatedSpeed(const UAnimSequenceBase* AnimSequence); + + /** + * @param Animations List of Animations to select. + * @param ReferenceValue The animation with distance closest to ReferenceValue will be selected. + * @return Selected AnimSequence. + */ + UFUNCTION(BlueprintCallable, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + static UAnimSequence* SelectAnimationWithFloat(const TArray& Animations, const float& ReferenceValue); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + static bool ValidatePoseSearchDatabasesChooser(const UChooserTable* ChooserTable, FText& OutMessage); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + static bool IsValidPoseSearchDatabasesChooser(const UChooserTable* ChooserTable); + + UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GMS|Animation", meta=(BlueprintThreadSafe)) + static TArray EvaluatePoseSearchDatabasesChooser(const UGMS_MainAnimInstance* MainAnimInstance, const UGMS_AnimLayer* AnimLayerInstance, UChooserTable* ChooserTable); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMS|Utility", meta=(BlueprintThreadSafe, DeterminesOutputType=DesiredClass, DynamicOutputParam="ReturnValue")) + static const UGMS_MovementSetUserSetting* GetMovementSetUserSetting(const FGMS_MovementSetSetting& MovementSetSetting, TSubclassOf DesiredClass); +}; + +constexpr FStringView UGMS_Utility::BoolToString(const bool bValue) +{ + return bValue ? TEXTVIEW("True") : TEXTVIEW("False"); +} diff --git a/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Vector.h b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Vector.h new file mode 100644 index 0000000..ffb68ca --- /dev/null +++ b/Plugins/GMS/Source/GenericMovementSystem/Public/Utility/GMS_Vector.h @@ -0,0 +1,163 @@ +// Copyright 2025 https://yuewu.dev/en All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GMS_Vector.generated.h" + +UCLASS(Meta = (BlueprintThreadSafe)) +class GENERICMOVEMENTSYSTEM_API UGMS_Vector : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "Vector", ReturnDisplayName = "Vector")) + static FVector ClampMagnitude01(const FVector& Vector); + + static FVector3f ClampMagnitude01(const FVector3f& Vector); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", DisplayName = "Clamp Magnitude 01 2D", + Meta = (AutoCreateRefTerm = "Vector", ReturnDisplayName = "Vector")) + static FVector2D ClampMagnitude012D(const FVector2D& Vector); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (ReturnDisplayName = "Direction")) + static FVector2D RadianToDirection(float Radian); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (ReturnDisplayName = "Direction")) + static FVector RadianToDirectionXY(float Radian); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (ReturnDisplayName = "Direction")) + static FVector2D AngleToDirection(float Angle); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (ReturnDisplayName = "Direction")) + static FVector AngleToDirectionXY(float Angle); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "Direction", ReturnDisplayName = "Angle")) + static double DirectionToAngle(const FVector2D& Direction); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "Direction", ReturnDisplayName = "Angle")) + static double DirectionToAngleXY(const FVector& Direction); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "Vector", ReturnDisplayName = "Vector")) + static FVector PerpendicularClockwiseXY(const FVector& Vector); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "Vector", ReturnDisplayName = "Vector")) + static FVector PerpendicularCounterClockwiseXY(const FVector& Vector); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", DisplayName = "Angle Between (Skip Normalization)", + Meta = (AutoCreateRefTerm = "From, To", ReturnDisplayName = "Angle")) + static double AngleBetweenSkipNormalization(const FVector& From, const FVector& To); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", Meta = (AutoCreateRefTerm = "From, To", ReturnDisplayName = "Angle")) + static float AngleBetweenSignedXY(const FVector3f& From, const FVector3f& To); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GMS|Vector Utility", DisplayName = "Slerp (Skip Normalization)", + Meta = (AutoCreateRefTerm = "From, To", ReturnDisplayName = "Direction")) + static FVector SlerpSkipNormalization(const FVector& From, const FVector& To, float Ratio); +}; + +inline FVector UGMS_Vector::ClampMagnitude01(const FVector& Vector) +{ + const auto MagnitudeSquared{Vector.SizeSquared()}; + + if (MagnitudeSquared <= 1.0f) + { + return Vector; + } + + const auto Scale{FMath::InvSqrt(MagnitudeSquared)}; + + return {Vector.X * Scale, Vector.Y * Scale, Vector.Z * Scale}; +} + +inline FVector3f UGMS_Vector::ClampMagnitude01(const FVector3f& Vector) +{ + const auto MagnitudeSquared{Vector.SizeSquared()}; + + if (MagnitudeSquared <= 1.0f) + { + return Vector; + } + + const auto Scale{FMath::InvSqrt(MagnitudeSquared)}; + + return {Vector.X * Scale, Vector.Y * Scale, Vector.Z * Scale}; +} + +inline FVector2D UGMS_Vector::ClampMagnitude012D(const FVector2D& Vector) +{ + const auto MagnitudeSquared{Vector.SizeSquared()}; + + if (MagnitudeSquared <= 1.0f) + { + return Vector; + } + + const auto Scale{FMath::InvSqrt(MagnitudeSquared)}; + + return {Vector.X * Scale, Vector.Y * Scale}; +} + +inline FVector2D UGMS_Vector::RadianToDirection(const float Radian) +{ + float Sin, Cos; + FMath::SinCos(&Sin, &Cos, Radian); + + return {Cos, Sin}; +} + +inline FVector UGMS_Vector::RadianToDirectionXY(const float Radian) +{ + float Sin, Cos; + FMath::SinCos(&Sin, &Cos, Radian); + + return {Cos, Sin, 0.0f}; +} + +inline FVector2D UGMS_Vector::AngleToDirection(const float Angle) +{ + return RadianToDirection(FMath::DegreesToRadians(Angle)); +} + +inline FVector UGMS_Vector::AngleToDirectionXY(const float Angle) +{ + return RadianToDirectionXY(FMath::DegreesToRadians(Angle)); +} + +inline double UGMS_Vector::DirectionToAngle(const FVector2D& Direction) +{ + return FMath::RadiansToDegrees(FMath::Atan2(Direction.Y, Direction.X)); +} + +inline double UGMS_Vector::DirectionToAngleXY(const FVector& Direction) +{ + return FMath::RadiansToDegrees(FMath::Atan2(Direction.Y, Direction.X)); +} + +inline FVector UGMS_Vector::PerpendicularClockwiseXY(const FVector& Vector) +{ + return {Vector.Y, -Vector.X, Vector.Z}; +} + +inline FVector UGMS_Vector::PerpendicularCounterClockwiseXY(const FVector& Vector) +{ + return {-Vector.Y, Vector.X, Vector.Z}; +} + +inline double UGMS_Vector::AngleBetweenSkipNormalization(const FVector& From, const FVector& To) +{ + return FMath::RadiansToDegrees(FMath::Acos(From | To)); +} + +inline float UGMS_Vector::AngleBetweenSignedXY(const FVector3f& From, const FVector3f& To) +{ + FVector2f FromXY{From}; + FromXY.Normalize(); + + FVector2f ToXY{To}; + ToXY.Normalize(); + + // return FMath::RadiansToDegrees(FMath::Atan2(FromXY ^ ToXY, FromXY | ToXY)); + + return FMath::RadiansToDegrees(FMath::Acos(FromXY | ToXY)) * FMath::Sign(FromXY ^ ToXY); +} diff --git a/Plugins/UGC/AuroraDevs_UGC.uplugin b/Plugins/UGC/AuroraDevs_UGC.uplugin new file mode 100644 index 0000000..e02961e --- /dev/null +++ b/Plugins/UGC/AuroraDevs_UGC.uplugin @@ -0,0 +1,61 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.88", + "FriendlyName": "Ultimate Gameplay Camera", + "Description": "This project allows you to easily and quickly add a AAA-standard, dynamic third person camera to your game! These features are smooth and non-intrusive, always prioritizing the player’s input.", + "Category": "Code Plugin", + "CreatedBy": "Aurora Devs", + "CreatedByURL": "https://www.fab.com/sellers/Aurora%20Devs", + "DocsURL": "https://coda.io/@aurora-devs/documentation-ultimate-gameplay-camera", + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/291971d9abb1443897deb57e80731270", + "SupportURL": "mailto:support@auroradevs.eu", + "EngineVersion": "5.7.0", + "CanContainContent": true, + "Installed": true, + "SupportedTargetPlatforms": [ + "Win64" + ], + "Modules": [ + { + "Name": "AuroraDevs_UGC", + "Type": "Runtime", + "LoadingPhase": "PostConfigInit", + "PlatformAllowList": [ + "Win64" + ], + "AdditionalDependencies": [ + "Engine", + "EngineCameras", + "EnhancedInput", + "TemplateSequence" + ] + }, + { + "Name": "AuroraDevs_UGCEditor", + "Type": "UncookedOnly", + "LoadingPhase": "PreDefault", + "PlatformAllowList": [ + "Win64" + ], + "AdditionalDependencies": [ + "Engine", + "BlueprintGraph" + ] + } + ], + "Plugins": [ + { + "Name": "EngineCameras", + "Enabled": true + }, + { + "Name": "EnhancedInput", + "Enabled": true + }, + { + "Name": "TemplateSequence", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/UGC/Config/FilterPlugin.ini b/Plugins/UGC/Config/FilterPlugin.ini new file mode 100644 index 0000000..b878ff2 --- /dev/null +++ b/Plugins/UGC/Config/FilterPlugin.ini @@ -0,0 +1,2 @@ +[FilterPlugin] +/Config/* \ No newline at end of file diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_ResetControlRotation.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_ResetControlRotation.uasset new file mode 100644 index 0000000..a850acb Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_ResetControlRotation.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_SetControlRotation.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_SetControlRotation.uasset new file mode 100644 index 0000000..7a8e15c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_SetControlRotation.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_UGCCameraDataAsset.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_UGCCameraDataAsset.uasset new file mode 100644 index 0000000..acb4fb4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/ANS_UGCCameraDataAsset.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/AN_UGCPlayCameraAnimation.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/AN_UGCPlayCameraAnimation.uasset new file mode 100644 index 0000000..a31c079 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/AnimNotifies/AN_UGCPlayCameraAnimation.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/BP_UGCCameraManager.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/BP_UGCCameraManager.uasset new file mode 100644 index 0000000..fd9028c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/BP_UGCCameraManager.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToArmLengthCurveNormalized_Default.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToArmLengthCurveNormalized_Default.uasset new file mode 100644 index 0000000..48dde26 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToArmLengthCurveNormalized_Default.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToFOVCurveNormalized_Default.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToFOVCurveNormalized_Default.uasset new file mode 100644 index 0000000..768e30c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/PitchCurves/C_PitchToFOVCurveNormalized_Default.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOut.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOut.uasset new file mode 100644 index 0000000..c639459 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOut.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOutDoubled.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOutDoubled.uasset new file mode 100644 index 0000000..ea322b1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AccelerateOutDoubled.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AxisRampCurve.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AxisRampCurve.uasset new file mode 100644 index 0000000..b7809d4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_AxisRampCurve.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_EaseInQuint.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_EaseInQuint.uasset new file mode 100644 index 0000000..ae461d4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_EaseInQuint.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Hermite.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Hermite.uasset new file mode 100644 index 0000000..89c6724 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Hermite.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Linear.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Linear.uasset new file mode 100644 index 0000000..eb3b0ad Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/Curves/Presets/C_Linear.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_AllDisabled.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_AllDisabled.uasset new file mode 100644 index 0000000..5608ff8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_AllDisabled.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_Default.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_Default.uasset new file mode 100644 index 0000000..fbfba90 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Data/UGC_Camera_Default.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/BP_UGCModifierBase.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/BP_UGCModifierBase.uasset new file mode 100644 index 0000000..0152232 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/BP_UGCModifierBase.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/Deprecated_UGC_CameraCollisionModifier_BP.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/Deprecated_UGC_CameraCollisionModifier_BP.uasset new file mode 100644 index 0000000..2b5e9e6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/Deprecated_UGC_CameraCollisionModifier_BP.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ActionCameraModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ActionCameraModifier.uasset new file mode 100644 index 0000000..86800b1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ActionCameraModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_AnglesConstraintModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_AnglesConstraintModifier.uasset new file mode 100644 index 0000000..700ad21 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_AnglesConstraintModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLagModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLagModifier.uasset new file mode 100644 index 0000000..011fab8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLagModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLengthAnimNotifyStateModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLengthAnimNotifyStateModifier.uasset new file mode 100644 index 0000000..cee72f3 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmLengthAnimNotifyStateModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetAnimNotifyStateModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetAnimNotifyStateModifier.uasset new file mode 100644 index 0000000..ef263b8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetAnimNotifyStateModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetModifier.uasset new file mode 100644 index 0000000..f598f7e Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_ArmOffsetModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraDitheringModifier_BP.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraDitheringModifier_BP.uasset new file mode 100644 index 0000000..b34aa06 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraDitheringModifier_BP.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraShakeModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraShakeModifier.uasset new file mode 100644 index 0000000..87b5ed4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_CameraShakeModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FOVAnimNotifyStateModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FOVAnimNotifyStateModifier.uasset new file mode 100644 index 0000000..27b6840 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FOVAnimNotifyStateModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FocusCameraModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FocusCameraModifier.uasset new file mode 100644 index 0000000..efce71f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_FocusCameraModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchFollowModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchFollowModifier.uasset new file mode 100644 index 0000000..a73b899 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchFollowModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchToArmLengthAndFOVModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchToArmLengthAndFOVModifier.uasset new file mode 100644 index 0000000..bcfd044 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_PitchToArmLengthAndFOVModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_YawFollowModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_YawFollowModifier.uasset new file mode 100644 index 0000000..e6ca333 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/Modifiers/UGC_YawFollowModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/UGC_CameraDataAsset.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/UGC_CameraDataAsset.uasset new file mode 100644 index 0000000..974659d Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Camera/UGC_CameraDataAsset.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Components/BP_UGCSpringArmComponent.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Components/BP_UGCSpringArmComponent.uasset new file mode 100644 index 0000000..b4038f2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Components/BP_UGCSpringArmComponent.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Input/Modifiers/UGC_RampModifier.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Input/Modifiers/UGC_RampModifier.uasset new file mode 100644 index 0000000..9cbe2a4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Input/Modifiers/UGC_RampModifier.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Circle.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Circle.uasset new file mode 100644 index 0000000..cd0a1f2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Circle.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Whole.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Whole.uasset new file mode 100644 index 0000000..e32b916 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MF_CameraDithering_Whole.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MPC_Dithering.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MPC_Dithering.uasset new file mode 100644 index 0000000..ca0c0b5 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Dithering/MPC_Dithering.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Enums/EPitchFollowState.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Enums/EPitchFollowState.uasset new file mode 100644 index 0000000..c3054ce Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Enums/EPitchFollowState.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Interface/BPI_UGC_Interface.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Interface/BPI_UGC_Interface.uasset new file mode 100644 index 0000000..f1860b1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Interface/BPI_UGC_Interface.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Library/BPFL_UGCHelpers.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Library/BPFL_UGCHelpers.uasset new file mode 100644 index 0000000..c910d1b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Blueprints/Misc/Library/BPFL_UGCHelpers.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo.umap b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo.umap new file mode 100644 index 0000000..4a2bb5e Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo.umap differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo_BuiltData.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo_BuiltData.uasset new file mode 100644 index 0000000..260c04f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Demo_BuiltData.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/BP_DocumentationActor.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/BP_DocumentationActor.uasset new file mode 100644 index 0000000..c65b2db Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/BP_DocumentationActor.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/LS_CinematicsTest.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/LS_CinematicsTest.uasset new file mode 100644 index 0000000..cfb542a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Actors/LS_CinematicsTest.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_ThirdPersonGameMode.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_ThirdPersonGameMode.uasset new file mode 100644 index 0000000..afb5afb Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_ThirdPersonGameMode.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_UGC_ThirdPersonController.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_UGC_ThirdPersonController.uasset new file mode 100644 index 0000000..81e6a14 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/BP_UGC_ThirdPersonController.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_Circle.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_Circle.uasset new file mode 100644 index 0000000..5c1f669 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_Circle.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart.uasset new file mode 100644 index 0000000..7c060a9 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart_Left.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart_Left.uasset new file mode 100644 index 0000000..a26b1c2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraAnimations/CA_EchoLightDart_Left.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_CameraTrigger.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_CameraTrigger.uasset new file mode 100644 index 0000000..42acc26 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_CameraTrigger.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test1.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test1.uasset new file mode 100644 index 0000000..0bfc50c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test1.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test2.uasset new file mode 100644 index 0000000..62331c7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/BP_FocusTargetMethod_Test2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Crouch.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Crouch.uasset new file mode 100644 index 0000000..971573b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Crouch.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test.uasset new file mode 100644 index 0000000..9b5363c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test_NoInput.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test_NoInput.uasset new file mode 100644 index 0000000..7887931 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Focus_Test_NoInput.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Pawn2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Pawn2.uasset new file mode 100644 index 0000000..73efb33 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Pawn2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Prone.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Prone.uasset new file mode 100644 index 0000000..a8fa79f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Camera/CameraData/UGC_Camera_Prone.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/BP_UGC_ThirdPersonCharacter.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/BP_UGC_ThirdPersonCharacter.uasset new file mode 100644 index 0000000..545380c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/BP_UGC_ThirdPersonCharacter.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/ABP_Manny.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/ABP_Manny.uasset new file mode 100644 index 0000000..31e39c6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/ABP_Manny.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart.uasset new file mode 100644 index 0000000..78e6335 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart_Montage.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart_Montage.uasset new file mode 100644 index 0000000..cc64bd6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Abilities/Anim_EchoLightDart_Montage.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_CrouchLeans.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_CrouchLeans.uasset new file mode 100644 index 0000000..860b8be Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_CrouchLeans.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_JogLeans.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_JogLeans.uasset new file mode 100644 index 0000000..43bb9cd Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_JogLeans.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_Crouch.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_Crouch.uasset new file mode 100644 index 0000000..8cf6c13 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_Crouch.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_WalkRun.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_WalkRun.uasset new file mode 100644 index 0000000..937f180 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/BS_MM_WalkRun.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Fall_Loop.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Fall_Loop.uasset new file mode 100644 index 0000000..5f03222 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Fall_Loop.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Idle.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Idle.uasset new file mode 100644 index 0000000..3a35a2f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Idle.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Jump.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Jump.uasset new file mode 100644 index 0000000..5dc8ff9 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Jump.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Land.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Land.uasset new file mode 100644 index 0000000..21f2504 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Land.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Run_Fwd.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Run_Fwd.uasset new file mode 100644 index 0000000..2008719 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Run_Fwd.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry.uasset new file mode 100644 index 0000000..8c7130c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry_Montage.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry_Montage.uasset new file mode 100644 index 0000000..c1bd274 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Entry_Montage.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit.uasset new file mode 100644 index 0000000..a9106b2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit_Montage.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit_Montage.uasset new file mode 100644 index 0000000..bd75bc3 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Exit_Montage.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Idle.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Idle.uasset new file mode 100644 index 0000000..3a24562 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Idle.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Center.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Center.uasset new file mode 100644 index 0000000..1bb3aec Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Center.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Left.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Left.uasset new file mode 100644 index 0000000..85cf875 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Left.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Right.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Right.uasset new file mode 100644 index 0000000..a2c5f71 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Lean_Right.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd.uasset new file mode 100644 index 0000000..2cdbdf0 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Start.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Start.uasset new file mode 100644 index 0000000..3f440a7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Start.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Stop.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Stop.uasset new file mode 100644 index 0000000..fc7ec8d Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Crouch_Walk_Fwd_Stop.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Start.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Start.uasset new file mode 100644 index 0000000..5b43e1f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Start.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Stop.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Stop.uasset new file mode 100644 index 0000000..fad9059 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Fwd_Stop.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Center.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Center.uasset new file mode 100644 index 0000000..4cadcb8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Center.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Left.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Left.uasset new file mode 100644 index 0000000..38d4553 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Left.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Right.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Right.uasset new file mode 100644 index 0000000..fe8e083 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Unarmed_Jog_Lean_Right.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_Fwd.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_Fwd.uasset new file mode 100644 index 0000000..bd1e0bc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_Fwd.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_InPlace.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_InPlace.uasset new file mode 100644 index 0000000..d4e97f4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Animations/Locomotion/MM_Walk_InPlace.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinEdge.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinEdge.uasset new file mode 100644 index 0000000..177eaa2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinEdge.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinHitMacro.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinHitMacro.uasset new file mode 100644 index 0000000..ee25bcc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinHitMacro.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinLaunchEdgeGlow.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinLaunchEdgeGlow.uasset new file mode 100644 index 0000000..e76c95a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/MaterialFunctions/MF_MannequinLaunchEdgeGlow.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/Textures/General/squares.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/Textures/General/squares.uasset new file mode 100644 index 0000000..0d00c4d Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Effects/Textures/General/squares.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Mannequin_LODSettings.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Mannequin_LODSettings.uasset new file mode 100644 index 0000000..dcfadad Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Mannequin_LODSettings.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin.uasset new file mode 100644 index 0000000..13a9113 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin_new.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin_new.uasset new file mode 100644 index 0000000..985e4f6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/CA_Mannequin_new.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve.uasset new file mode 100644 index 0000000..1555a43 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve2.uasset new file mode 100644 index 0000000..773bbf7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ChromaticCurve2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_BaseColorFallOff.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_BaseColorFallOff.uasset new file mode 100644 index 0000000..5b3a1fa Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_BaseColorFallOff.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_Diffraction.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_Diffraction.uasset new file mode 100644 index 0000000..9324c26 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_Diffraction.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo.uasset new file mode 100644 index 0000000..75a2b87 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo3layers.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo3layers.uasset new file mode 100644 index 0000000..bdd98c4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/MF_logo3layers.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ML_BaseColorFallOff.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ML_BaseColorFallOff.uasset new file mode 100644 index 0000000..505f08a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Functions/ML_BaseColorFallOff.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01.uasset new file mode 100644 index 0000000..85e07f9 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Black.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Black.uasset new file mode 100644 index 0000000..9925cb8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Black.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Blue.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Blue.uasset new file mode 100644 index 0000000..49a59e3 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Blue.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Green.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Green.uasset new file mode 100644 index 0000000..71715bc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Green.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Red.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Red.uasset new file mode 100644 index 0000000..e6b8ac5 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_01_Red.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02.uasset new file mode 100644 index 0000000..fd18852 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Black.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Black.uasset new file mode 100644 index 0000000..b29c411 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Black.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Blue.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Blue.uasset new file mode 100644 index 0000000..03d3d3d Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Blue.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Red.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Red.uasset new file mode 100644 index 0000000..57bfdda Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Instances/Manny/MI_Manny_02_Red.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/M_LyraMannequin.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/M_LyraMannequin.uasset new file mode 100644 index 0000000..c60882f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/M_LyraMannequin.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_D.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_D.uasset new file mode 100644 index 0000000..acfb377 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_D.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_N.uasset new file mode 100644 index 0000000..96575ca Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_01_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_D.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_D.uasset new file mode 100644 index 0000000..e56fecc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_D.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_N.uasset new file mode 100644 index 0000000..04e47d3 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_LyraManny_02_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK.uasset new file mode 100644 index 0000000..98d93ff Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK1.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK1.uasset new file mode 100644 index 0000000..0e88787 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK1.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK2.uasset new file mode 100644 index 0000000..6cd28a0 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK3.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK3.uasset new file mode 100644 index 0000000..641a3af Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_01_MSK3.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK.uasset new file mode 100644 index 0000000..697179f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK1.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK1.uasset new file mode 100644 index 0000000..8f0c467 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK1.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK2.uasset new file mode 100644 index 0000000..ebf98c5 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK3.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK3.uasset new file mode 100644 index 0000000..637d7c6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Manny/T_Manny_02_MSK3.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/Aurora_LOGO_auto_x2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/Aurora_LOGO_auto_x2.uasset new file mode 100644 index 0000000..af21f01 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/Aurora_LOGO_auto_x2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_Detail_Normal.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_Detail_Normal.uasset new file mode 100644 index 0000000..773351f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_Detail_Normal.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_OrangePeel_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_OrangePeel_N.uasset new file mode 100644 index 0000000..e7e1e76 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_OrangePeel_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_M.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_M.uasset new file mode 100644 index 0000000..222aa94 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_M.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_V2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_V2.uasset new file mode 100644 index 0000000..86d54ee Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Materials/Textures/Shared/T_UE_Logo_V2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/ABP_Manny_PostProcess.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/ABP_Manny_PostProcess.uasset new file mode 100644 index 0000000..8506b1f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/ABP_Manny_PostProcess.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_BasicFootIK.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_BasicFootIK.uasset new file mode 100644 index 0000000..0acc06b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_BasicFootIK.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Body.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Body.uasset new file mode 100644 index 0000000..5fc0050 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Body.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Procedural.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Procedural.uasset new file mode 100644 index 0000000..02d1091 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/CR_Mannequin_Procedural.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/IK_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/IK_Mannequin_UGC.uasset new file mode 100644 index 0000000..1b6c327 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/IK_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/PA_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/PA_Mannequin_UGC.uasset new file mode 100644 index 0000000..29785c7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/PA_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_anim.uasset new file mode 100644 index 0000000..0d4cf42 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_pose.uasset new file mode 100644 index 0000000..d29619e Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_anim.uasset new file mode 100644 index 0000000..0f78603 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_pose.uasset new file mode 100644 index 0000000..567aa99 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_calf_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset new file mode 100644 index 0000000..ee66faa Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset new file mode 100644 index 0000000..28d5c56 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset new file mode 100644 index 0000000..ae347df Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset new file mode 100644 index 0000000..b318692 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_anim.uasset new file mode 100644 index 0000000..d2efcaa Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_pose.uasset new file mode 100644 index 0000000..a470b19 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_anim.uasset new file mode 100644 index 0000000..690e625 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_pose.uasset new file mode 100644 index 0000000..267222a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_foot_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_anim.uasset new file mode 100644 index 0000000..b59f595 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_pose.uasset new file mode 100644 index 0000000..4c971db Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_anim.uasset new file mode 100644 index 0000000..f19f361 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_pose.uasset new file mode 100644 index 0000000..95e3ef1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_hand_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset new file mode 100644 index 0000000..87adf5b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset new file mode 100644 index 0000000..11b7253 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset new file mode 100644 index 0000000..fa43755 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset new file mode 100644 index 0000000..5266293 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset new file mode 100644 index 0000000..bce2bc7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset new file mode 100644 index 0000000..9c15c20 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset new file mode 100644 index 0000000..2fd4408 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset new file mode 100644 index 0000000..12a33db Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset new file mode 100644 index 0000000..cd70296 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset new file mode 100644 index 0000000..4edaef1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset new file mode 100644 index 0000000..d9b515c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset new file mode 100644 index 0000000..a46279c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/RTG_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/RTG_Mannequin_UGC.uasset new file mode 100644 index 0000000..0a3b339 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/Rigs/RTG_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_Simple_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_Simple_UGC.uasset new file mode 100644 index 0000000..593284f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_Simple_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_UGC.uasset new file mode 100644 index 0000000..70a3596 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SKM_Manny_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SK_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SK_Mannequin_UGC.uasset new file mode 100644 index 0000000..4b337a0 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny/Meshes/SK_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_Latex_Black.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_Latex_Black.uasset new file mode 100644 index 0000000..0037223 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_Latex_Black.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige.uasset new file mode 100644 index 0000000..4d755df Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset new file mode 100644 index 0000000..8704990 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_SoftMetal.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_SoftMetal.uasset new file mode 100644 index 0000000..9224fe0 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Layers/ML_SoftMetal.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MI_Male_Body.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MI_Male_Body.uasset new file mode 100644 index 0000000..e368ad7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MI_Male_Body.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_Male_Body.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_Male_Body.uasset new file mode 100644 index 0000000..bfce0a7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_Male_Body.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_Body.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_Body.uasset new file mode 100644 index 0000000..7befa54 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_Body.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_ChestLogo.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_ChestLogo.uasset new file mode 100644 index 0000000..340b1a2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_MannequinUE4_ChestLogo.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_UE4Man_ChestLogo.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_UE4Man_ChestLogo.uasset new file mode 100644 index 0000000..1e99057 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/M_UE4Man_ChestLogo.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset new file mode 100644 index 0000000..8370d4a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset new file mode 100644 index 0000000..0a69d38 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset new file mode 100644 index 0000000..afc3a4d Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset new file mode 100644 index 0000000..93235ef Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01.uasset new file mode 100644 index 0000000..e527a0f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset new file mode 100644 index 0000000..42278cc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 0000000..41988d9 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 0000000..fc9cea4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169.uasset new file mode 100644 index 0000000..a839c0b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset new file mode 100644 index 0000000..0c1ef21 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01.uasset new file mode 100644 index 0000000..5bcb789 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01_N.uasset new file mode 100644 index 0000000..fc13ab7 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Aluminum01_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_D.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 0000000..9bba5f3 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 0000000..0c44ef4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_Mask.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_Mask.uasset new file mode 100644 index 0000000..ba0966b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_Mask.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_N.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_N.uasset new file mode 100644 index 0000000..d27a007 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_Male_N.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin_MAT_MASKA.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin_MAT_MASKA.uasset new file mode 100644 index 0000000..bc3149a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin_MAT_MASKA.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin__normals.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin__normals.uasset new file mode 100644 index 0000000..88d5a53 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UE4_Mannequin__normals.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_Mask.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_Mask.uasset new file mode 100644 index 0000000..afd17e2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_Mask.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_N_TGA.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_N_TGA.uasset new file mode 100644 index 0000000..9db04af Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Materials/Textures/T_UELogo_N_TGA.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/IK_UE4_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/IK_UE4_Mannequin_UGC.uasset new file mode 100644 index 0000000..d77d871 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/IK_UE4_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE4Manny_UE5Manny_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE4Manny_UE5Manny_UGC.uasset new file mode 100644 index 0000000..e666e6f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE4Manny_UE5Manny_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE5Manny_UE4Manny_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE5Manny_UE4Manny_UGC.uasset new file mode 100644 index 0000000..7b9695a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/Rigs/RTG_UE5Manny_UE4Manny_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_PhysicsAsset_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_PhysicsAsset_UGC.uasset new file mode 100644 index 0000000..06a9b46 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_PhysicsAsset_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_Skeleton_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_Skeleton_UGC.uasset new file mode 100644 index 0000000..44ddfe6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_Skeleton_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_UGC.uasset new file mode 100644 index 0000000..46961c6 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Characters/Manny_UE4/Meshes/SK_Mannequin_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Crouch.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Crouch.uasset new file mode 100644 index 0000000..9fbfe6c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Crouch.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Jump.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Jump.uasset new file mode 100644 index 0000000..cd80883 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Jump.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Look.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Look.uasset new file mode 100644 index 0000000..4f89007 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Look.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Move.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Move.uasset new file mode 100644 index 0000000..ee34c17 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/Actions/UGC_IA_Move.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/UGC_IMC_Default.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/UGC_IMC_Default.uasset new file mode 100644 index 0000000..8f37ff1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Game/Input/UGC_IMC_Default.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MF_ObjectAlignedTexture.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MF_ObjectAlignedTexture.uasset new file mode 100644 index 0000000..c20edad Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MF_ObjectAlignedTexture.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_AuroraDecal_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_AuroraDecal_UGC.uasset new file mode 100644 index 0000000..fe60fe4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_AuroraDecal_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Blue.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Blue.uasset new file mode 100644 index 0000000..ed381f4 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Blue.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Green.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Green.uasset new file mode 100644 index 0000000..0fc914a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Green.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Grey.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Grey.uasset new file mode 100644 index 0000000..d4ed56a Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Grey.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Red.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Red.uasset new file mode 100644 index 0000000..1ea06c8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_Red.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_White.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_White.uasset new file mode 100644 index 0000000..72950e0 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/MI_Grid_2_White.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_AuroraDecal_Logo.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_AuroraDecal_Logo.uasset new file mode 100644 index 0000000..f7a1458 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_AuroraDecal_Logo.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_Grid_2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_Grid_2.uasset new file mode 100644 index 0000000..762f7bc Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/M_Grid_2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Decal_UGC.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Decal_UGC.uasset new file mode 100644 index 0000000..c84ada2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Decal_UGC.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Grid_Crosses.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Grid_Crosses.uasset new file mode 100644 index 0000000..9dc29bf Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Grid_Crosses.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Hologrid.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Hologrid.uasset new file mode 100644 index 0000000..27955d5 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Hologrid.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Diffuse.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Diffuse.uasset new file mode 100644 index 0000000..8a5b6c5 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Diffuse.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Glossiness.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Glossiness.uasset new file mode 100644 index 0000000..35f9bf1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Glossiness.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Normal.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Normal.uasset new file mode 100644 index 0000000..62edcc2 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_Paint_Normal.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_sky_01_8k.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_sky_01_8k.uasset new file mode 100644 index 0000000..67a5b46 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/Materials/Textures/T_sky_01_8k.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/SM_Cube.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/SM_Cube.uasset new file mode 100644 index 0000000..939abfa Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/Meshes/SM_Cube.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_LONG.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_LONG.uasset new file mode 100644 index 0000000..65398f1 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_LONG.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset new file mode 100644 index 0000000..1e795ae Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LOGO_NO-SUBS_NO-BG-169_N_2.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LQ.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LQ.uasset new file mode 100644 index 0000000..146d36f Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Aurora_LQ.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Controls.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Controls.uasset new file mode 100644 index 0000000..dcba25b Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Controls.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_ControlsButton.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_ControlsButton.uasset new file mode 100644 index 0000000..d62c0d8 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_ControlsButton.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Documentation.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Documentation.uasset new file mode 100644 index 0000000..7b6f3f9 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Documentation.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Marketplace.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Marketplace.uasset new file mode 100644 index 0000000..ee7c712 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Marketplace.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Quit.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Quit.uasset new file mode 100644 index 0000000..747ac1c Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Quit.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Resume.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Resume.uasset new file mode 100644 index 0000000..eee3865 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/Textures/T_Resume.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoPauseMenu.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoPauseMenu.uasset new file mode 100644 index 0000000..712b886 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoPauseMenu.uasset differ diff --git a/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoUI.uasset b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoUI.uasset new file mode 100644 index 0000000..81b4f39 Binary files /dev/null and b/Plugins/UGC/Content/AuroraDevs_UGC/Demo/UI/WBP_DemoUI.uasset differ diff --git a/Plugins/UGC/Resources/Icon128.png b/Plugins/UGC/Resources/Icon128.png new file mode 100644 index 0000000..bf1f800 Binary files /dev/null and b/Plugins/UGC/Resources/Icon128.png differ diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/AuroraDevs_UGC.Build.cs b/Plugins/UGC/Source/AuroraDevs_UGC/AuroraDevs_UGC.Build.cs new file mode 100644 index 0000000..40b61cf --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/AuroraDevs_UGC.Build.cs @@ -0,0 +1,39 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +using UnrealBuildTool; + +public class AuroraDevs_UGC : ModuleRules +{ + public AuroraDevs_UGC(ReadOnlyTargetRules Target) : base(Target) + { + OptimizeCode = CodeOptimization.Never; + + PCHUsage = PCHUsageMode.NoPCHs; + MinFilesUsingPrecompiledHeaderOverride = 1; + bUseUnity = false; + IWYUSupport = IWYUSupport.Full; + PrecompileForTargets = PrecompileTargetsType.Any; + + PrivateIncludePaths.Add("AuroraDevs_UGC"); + if (Target.Version.MajorVersion >= 5 && Target.Version.MinorVersion >= 5) + { + PublicDependencyModuleNames.AddRange(new string[] + { + "EngineCameras" + }); + } + + PrivateDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + "EnhancedInput", + "InputCore" , + "LevelSequence", + "MovieScene", + "MovieSceneTracks", + "TemplateSequence" + }); + } +} diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.cpp new file mode 100644 index 0000000..124cb62 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.cpp @@ -0,0 +1,162 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.h" +#include "Components/SkeletalMeshComponent.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.h" +#include "Camera/UGC_PlayerCameraManager.h" + +FLinearColor UUGC_FOVRequestAnimNotifyState::GetEditorColor() +{ +#if WITH_EDITORONLY_DATA + const uint8 Ratio = 255 - static_cast(FMath::GetMappedRangeValueClamped(FVector2D(5.f, 170.f), FVector2D(0.f, 255.f), TargetFOV)); + NotifyColor = FColor(255, Ratio, Ratio, 255); + return NotifyColor; +#else + return FLinearColor::Black; +#endif +} + +void UUGC_FOVRequestAnimNotifyState::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) +{ + AActor* Owner = MeshComp->GetOwner(); + if (APawn* OwnerPawn = Cast(Owner)) + { + if (APlayerController* PC = OwnerPawn->GetController()) + { + if (AUGC_PlayerCameraManager* CameraManager = Cast(PC->PlayerCameraManager)) + { + if (UUGC_FOVAnimNotifyCameraModifier* Modifier = CameraManager->FindCameraModifierOfType()) + { + Modifier->PushFOVAnimNotifyRequest(RequestId, TargetFOV, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); + } + } + } + } +} + +FString UUGC_FOVRequestAnimNotifyState::GetNotifyName_Implementation() const +{ +#if WITH_EDITORONLY_DATA + if (bShowRequestIdInName) + { + return FString::Printf(TEXT("[%s][UGC] FOV Request: %.2f"), *RequestId.ToString(), TargetFOV); + } + else +#endif + { + return FString::Printf(TEXT("[UGC] FOV Request: %.2f"), TargetFOV); + } +} + +FLinearColor UUGC_ArmOffsetRequestAnimNotifyState::GetEditorColor() +{ +#if WITH_EDITORONLY_DATA + NotifyColor = FColor(255, 212, 105, 255); + return NotifyColor; +#else + return FLinearColor::Black; +#endif +} + +void UUGC_ArmOffsetRequestAnimNotifyState::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) +{ + AActor* Owner = MeshComp->GetOwner(); + if (APawn* OwnerPawn = Cast(Owner)) + { + if (APlayerController* PC = OwnerPawn->GetController()) + { + if (AUGC_PlayerCameraManager* CameraManager = Cast(PC->PlayerCameraManager)) + { + if (UUGC_ArmOffsetAnimNotifyCameraModifier* Modifier = CameraManager->FindCameraModifierOfType()) + { + if (bModifySocketOffset) + { + Modifier->PushArmSocketOffsetAnimNotifyRequest(RequestId, TargetSocketOffset, TotalDuration, SocketOffsetBlendInDuration, SocketOffsetBlendInCurve, SocketOffsetBlendOutDuration, SocketOffsetBlendOutCurve); + } + + if (bModifyTargetOffset) + { + Modifier->PushArmTargetOffsetAnimNotifyRequest(RequestId, TargetTargetOffset, TotalDuration, TargetOffsetBlendInDuration, TargetOffsetBlendInCurve, TargetOffsetBlendOutDuration, TargetOffsetBlendOutCurve); + } + } + } + } + } +} + +FString UUGC_ArmOffsetRequestAnimNotifyState::GetNotifyName_Implementation() const +{ +#if WITH_EDITORONLY_DATA + if (bShowRequestIdInName) + { + FString Text = FString::Printf(TEXT("[%s][UGC]"), *RequestId.ToString()); + if (bModifySocketOffset) + { + Text.Append(FString::Printf(TEXT(" Socket: %s"), *TargetSocketOffset.ToCompactString())); + } + if (bModifyTargetOffset) + { + Text.Append(FString::Printf(TEXT(" Target: %s"), *TargetTargetOffset.ToCompactString())); + } + return Text; + } + else +#endif + { + FString Text = TEXT("[UGC]"); + if (bModifySocketOffset) + { + Text.Append(FString::Printf(TEXT(" Socket: %s"), *TargetSocketOffset.ToCompactString())); + } + if (bModifyTargetOffset) + { + Text.Append(FString::Printf(TEXT(" Target: %s"), *TargetTargetOffset.ToCompactString())); + } + return Text; + } +} + +FLinearColor UUGC_ArmLengthRequestAnimNotifyState::GetEditorColor() +{ +#if WITH_EDITORONLY_DATA + NotifyColor = FColor(55, 219, 33, 255); + return NotifyColor; +#else + return FLinearColor::Black; +#endif +} + +void UUGC_ArmLengthRequestAnimNotifyState::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) +{ + AActor* Owner = MeshComp->GetOwner(); + if (APawn* OwnerPawn = Cast(Owner)) + { + if (APlayerController* PC = OwnerPawn->GetController()) + { + if (AUGC_PlayerCameraManager* CameraManager = Cast(PC->PlayerCameraManager)) + { + if (UUGC_ArmLengthAnimNotifyCameraModifier* Modifier = CameraManager->FindCameraModifierOfType()) + { + Modifier->PushArmLengthAnimNotifyRequest(RequestId, TargetArmLength, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); + } + } + } + } +} + +FString UUGC_ArmLengthRequestAnimNotifyState::GetNotifyName_Implementation() const +{ +#if WITH_EDITORONLY_DATA + if (bShowRequestIdInName) + { + return FString::Printf(TEXT("[%s][UGC] Arm Length: %.2f"), *RequestId.ToString(), TargetArmLength); + } + else +#endif + { + return FString::Printf(TEXT("[UGC] Arm Length: %.2f"), TargetArmLength); + } +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/AuroraDevs_UGC.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/AuroraDevs_UGC.cpp new file mode 100644 index 0000000..5d86342 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/AuroraDevs_UGC.cpp @@ -0,0 +1,20 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "AuroraDevs_UGC.h" +#include "Modules/ModuleManager.h" + +DEFINE_LOG_CATEGORY(AuroraUGC); + +#define LOCTEXT_NAMESPACE "FAuroraDevs_UGCModule" + +void FAuroraDevs_UGCModule::StartupModule() +{ +} + +void FAuroraDevs_UGCModule::ShutdownModule() +{ +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FAuroraDevs_UGCModule, AuroraDevs_UGC) \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Components/UGC_SpringArmComponentBase.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Components/UGC_SpringArmComponentBase.cpp new file mode 100644 index 0000000..d6d7508 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Components/UGC_SpringArmComponentBase.cpp @@ -0,0 +1,331 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Components/UGC_SpringArmComponentBase.h" +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/Pawn.h" +#include "Kismet/KismetMathLibrary.h" +#include "Math/RotationMatrix.h" +#include "PhysicsEngine/PhysicsSettings.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "Kismet/GameplayStatics.h" + +void UUGC_SpringArmComponentBase::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); +} + +void UUGC_SpringArmComponentBase::UpdateDesiredArmLocation(bool bDoTrace, bool bDoLocationLag, bool bDoRotationLag, float DeltaTime) +{ + FRotator DesiredRot = GetTargetRotation(); + + // If our viewtarget is simulating using physics, we may need to clamp deltatime + if (bClampToMaxPhysicsDeltaTime) + { + // Use the same max timestep cap as the physics system to avoid camera jitter when the viewtarget simulates less time than the camera + DeltaTime = FMath::Min(DeltaTime, UPhysicsSettings::Get()->MaxPhysicsDeltaTime); + } + + // Apply 'lag' to rotation if desired + if (bDoRotationLag) + { + if (bUseCameraLagSubstepping && DeltaTime > CameraLagMaxTimeStep && CameraRotationLagSpeed > 0.f) + { + const FRotator ArmRotStep = (DesiredRot - PreviousDesiredRot).GetNormalized() * (1.f / DeltaTime); + FRotator LerpTarget = PreviousDesiredRot; + float RemainingTime = DeltaTime; + while (RemainingTime > UE_KINDA_SMALL_NUMBER) + { + const float LerpAmount = FMath::Min(CameraLagMaxTimeStep, RemainingTime); + LerpTarget += ArmRotStep * LerpAmount; + RemainingTime -= LerpAmount; + + DesiredRot = FRotator(FMath::QInterpTo(FQuat(PreviousDesiredRot), FQuat(LerpTarget), LerpAmount, CameraRotationLagSpeed)); + PreviousDesiredRot = DesiredRot; + } + } + else + { + DesiredRot = FRotator(FMath::QInterpTo(FQuat(PreviousDesiredRot), FQuat(DesiredRot), DeltaTime, CameraRotationLagSpeed)); + } + } + PreviousDesiredRot = DesiredRot; + + // Get the spring arm 'origin', the target we want to look at + FVector const OriginalArmOrigin = GetComponentLocation() + TargetOffset; + FVector const ArmOriginWithSocketOffset = OriginalArmOrigin + FRotationMatrix(DesiredRot).TransformVector(SocketOffset); + + // Get the spring arm 'origin', the target we want to look at + FVector ArmOrigin = OriginalArmOrigin; + // We lag the target, not the actual camera position, so rotating the camera around does not have lag + FVector DesiredLoc = ArmOrigin; + if (bDoLocationLag) + { + if (bUseCameraLagSubstepping && DeltaTime > CameraLagMaxTimeStep && CameraLagSpeed > 0.f) + { + const FVector ArmMovementStep = (DesiredLoc - PreviousDesiredLoc) * (1.f / DeltaTime); + FVector LerpTarget = PreviousDesiredLoc; + + float RemainingTime = DeltaTime; + while (RemainingTime > UE_KINDA_SMALL_NUMBER) + { + const float LerpAmount = FMath::Min(CameraLagMaxTimeStep, RemainingTime); + LerpTarget += ArmMovementStep * LerpAmount; + RemainingTime -= LerpAmount; + + DesiredLoc = FMath::VInterpTo(PreviousDesiredLoc, LerpTarget, LerpAmount, CameraLagSpeed); + PreviousDesiredLoc = DesiredLoc; + } + } + else + { + DesiredLoc = FMath::VInterpTo(PreviousDesiredLoc, DesiredLoc, DeltaTime, CameraLagSpeed); + } + + // Clamp distance if requested + bool bClampedDist = false; + if (CameraLagMaxDistance > 0.f) + { + const FVector FromOrigin = DesiredLoc - ArmOrigin; + if (FromOrigin.SizeSquared() > FMath::Square(CameraLagMaxDistance)) + { + DesiredLoc = ArmOrigin + FromOrigin.GetClampedToMaxSize(CameraLagMaxDistance); + bClampedDist = true; + } + } + +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + if (bDrawDebugLagMarkers) + { + DrawDebugSphere(GetWorld(), ArmOrigin, 5.f, 8, FColor::Green); + DrawDebugSphere(GetWorld(), DesiredLoc, 5.f, 8, FColor::Yellow); + + const FVector ToOrigin = ArmOrigin - DesiredLoc; + DrawDebugDirectionalArrow(GetWorld(), DesiredLoc, DesiredLoc + ToOrigin * 0.5f, 7.5f, bClampedDist ? FColor::Red : FColor::Green); + DrawDebugDirectionalArrow(GetWorld(), DesiredLoc + ToOrigin * 0.5f, ArmOrigin, 7.5f, bClampedDist ? FColor::Red : FColor::Green); + } +#endif + } + + PreviousArmOrigin = ArmOrigin; + PreviousDesiredLoc = DesiredLoc; + + // Now offset camera position back along our rotation + DesiredLoc -= DesiredRot.Vector() * TargetArmLength; + // Add socket offset in local space + DesiredLoc += FRotationMatrix(DesiredRot).TransformVector(SocketOffset); + + // Do a sweep to ensure we are not penetrating the world + FVector ResultLoc; + if (bDoTrace && (TargetArmLength != 0.0f)) + { + bIsCameraFixed = true; + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(SpringArm), false, GetOwner()); + + FHitResult Result; + // It's important to use the OriginalArmOrigin for the traces otherwise jumping over obstacles will trigger collisions (because ArmOrigin stays low). + GetWorld()->SweepSingleByChannel(Result, OriginalArmOrigin, DesiredLoc, FQuat::Identity, ProbeChannel, FCollisionShape::MakeSphere(ProbeSize), QueryParams); + + UnfixedCameraPosition = DesiredLoc; + + ResultLoc = BlendLocations(DesiredLoc, Result.Location, Result.bBlockingHit, DeltaTime); + + if (ResultLoc == DesiredLoc) + { + bIsCameraFixed = false; + } + } + else + { + ResultLoc = DesiredLoc; + bIsCameraFixed = false; + UnfixedCameraPosition = ResultLoc; + } + + // Form a transform for new world transform for camera + FTransform WorldCamTM(DesiredRot, ResultLoc); + // Convert to relative to component + FTransform RelCamTM = WorldCamTM.GetRelativeTransform(GetComponentTransform()); + + // Update socket location/rotation + RelativeSocketLocation = RelCamTM.GetLocation(); + RelativeSocketRotation = RelCamTM.GetRotation(); + + UpdateChildTransforms(); +} + +FVector UUGC_SpringArmComponentBase::BlendLocations(const FVector& DesiredArmLocation, const FVector& TraceHitLocation, bool bHitSomething, float DeltaTime) +{ + if (!bDoCollisionTest || !CameraCollisionSettings.bPreventPenetration) + { + return DesiredArmLocation; + } + + const bool bShouldComplexTrace = CameraCollisionSettings.bDoPredictiveAvoidance && CameraCollisionSettings.PenetrationAvoidanceFeelers.Num() > 0 && IsPlayerControlled(); + if (!bShouldComplexTrace) + { + return Super::BlendLocations(DesiredArmLocation, TraceHitLocation, bHitSomething, DeltaTime); + } + + FVector SafeLoc = FVector::ZeroVector; + if (bMaintainFramingDuringCollisions) + { + FRotator DesiredRot = GetTargetRotation(); + const FVector OriginalArmOrigin = GetComponentLocation() + TargetOffset; + SafeLoc = OriginalArmOrigin + FRotationMatrix(DesiredRot).TransformVector(SocketOffset); + } + else + { + SafeLoc = PreviousArmOrigin; + } + + const FVector CameraLoc = DesiredArmLocation; + const FVector BaseRay = CameraLoc - SafeLoc; + + if (BaseRay.IsNearlyZero()) + { + return DesiredArmLocation; + } + + const FRotationMatrix BaseMatrix(BaseRay.Rotation()); + const FVector Right = BaseMatrix.GetUnitAxis(EAxis::Y); + const FVector Up = BaseMatrix.GetUnitAxis(EAxis::Z); + + float HardBlockedPct = DistBlockedPct; + float SoftBlockedPct = DistBlockedPct; + float BlockedThisFrame = 1.f; + + UWorld* World = GetWorld(); + int32 NbrHits = 0; + const AActor* OwningActor = GetOwner(); + + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(CameraPen), false, OwningActor); + QueryParams.AddIgnoredActor(OwningActor); + +#if WITH_EDITORONLY_DATA + const int32 NewCapacity = CameraCollisionSettings.PenetrationAvoidanceFeelers.Num(); + const int32 OldCapacity = HitActors.Max(); + HitActors.Reset(FMath::Max(NewCapacity, OldCapacity)); // Reset array elements but keep allocation to max capacity. +#endif + + for (int32 i = 0; i < CameraCollisionSettings.PenetrationAvoidanceFeelers.Num(); ++i) + { + const FPenetrationAvoidanceFeeler& Feeler = CameraCollisionSettings.PenetrationAvoidanceFeelers[i]; + FRotator OffsetRot = Feeler.AdjustmentRot; + + if (i == 0 && Feeler.AdjustmentRot != FRotator::ZeroRotator) + { +#if ENABLE_DRAW_DEBUG + if (GEngine != nullptr) + { + GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Red, TEXT("DSA_SpringArmComponent: First Penetration Avoidance Feeler should always have an adjustment roation equal to 0,0,0!.")); + } +#endif + OffsetRot = FRotator::ZeroRotator; + } + + FVector RayTarget = BaseRay.RotateAngleAxis(OffsetRot.Yaw, Up).RotateAngleAxis(OffsetRot.Pitch, Right) + SafeLoc; + + FHitResult Hit; + FCollisionShape Shape = FCollisionShape::MakeSphere(Feeler.ProbeRadius); + bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, ProbeChannel, Shape, QueryParams); + + if (bHit && Hit.GetActor()) + { + if (Hit.GetActor()->ActorHasTag(CameraCollisionSettings.IgnoreCameraCollisionTag)) + { + QueryParams.AddIgnoredActor(Hit.GetActor()); + continue; + } + + ++NbrHits; + const float Weight = Hit.GetActor()->IsA() ? Feeler.PawnWeight : Feeler.WorldWeight; + +#if WITH_EDITORONLY_DATA + HitActors.AddUnique(Hit.GetActor()); +#endif + + float NewBlockPct = Hit.Time + (1.f - Hit.Time) * (1.f - Weight); + NewBlockPct = (Hit.Location - SafeLoc).Size() / (RayTarget - SafeLoc).Size(); + + BlockedThisFrame = FMath::Min(NewBlockPct, BlockedThisFrame); + if (i == 0) + HardBlockedPct = BlockedThisFrame; + else + SoftBlockedPct = BlockedThisFrame; + } + } + +#if ENABLE_DRAW_DEBUG + if (NbrHits > 0) + { + if (bPrintCollisionDebug && GEngine != nullptr) + { + if (bPrintHitActors) + { +#if WITH_EDITORONLY_DATA + for (int i = HitActors.Num() - 1; i >= 0; --i) + { + if (AActor* HitActor = HitActors[i]) + { + FString const DebugText = FString::Printf(TEXT("DSA_SpringArmComponent: Colliding with %s."), *GetNameSafe(HitActor)); + GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor(150, 150, 200), DebugText); + } + } +#endif + } + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6 + FString const DebugText = FString::Printf(TEXT("DSA_SpringArmComponent: %d feeler%hs colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#else + FString const DebugText = FString::Printf(TEXT("DSA_SpringArmComponent: %d feeler%s colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#endif + GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor(150, 150, 200), DebugText); + } + } +#endif + + if (DistBlockedPct < BlockedThisFrame) + { + float BlendOutTime = FMath::Max(CameraCollisionSettings.PenetrationBlendOutTime, UE_KINDA_SMALL_NUMBER); + DistBlockedPct += DeltaTime / BlendOutTime * (BlockedThisFrame - DistBlockedPct); + } + else if (DistBlockedPct > HardBlockedPct) + { + DistBlockedPct = HardBlockedPct; + } + else if (DistBlockedPct > SoftBlockedPct) + { + float BlendInTime = FMath::Max(CameraCollisionSettings.PenetrationBlendInTime, UE_KINDA_SMALL_NUMBER); + DistBlockedPct -= DeltaTime / BlendInTime * (DistBlockedPct - SoftBlockedPct); + } + + DistBlockedPct = FMath::Clamp(DistBlockedPct, 0.f, 1.f); + return SafeLoc + (CameraLoc - SafeLoc) * DistBlockedPct; +} + +bool UUGC_SpringArmComponentBase::IsPlayerControlled() const +{ + APawn* PawnOwner = Cast(GetOwner()); + AController* Controller = PawnOwner ? PawnOwner->GetController() : nullptr; + + bool bPlayerControlled = (PawnOwner && Controller && PawnOwner->IsPlayerControlled()); + if (!bPlayerControlled) + { + if (GEngine) + { + if (APlayerController* PC = GEngine->GetFirstLocalPlayerController(GetWorld())) + { + if (APlayerCameraManager* PCM = PC->PlayerCameraManager) + { + bPlayerControlled = PCM->PendingViewTarget.Target == GetOwner() || (Controller != nullptr && PCM->PendingViewTarget.Target == Controller); + } + } + } + } + return bPlayerControlled; +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Data/UGC_CameraData.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Data/UGC_CameraData.cpp new file mode 100644 index 0000000..e1d76c4 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Data/UGC_CameraData.cpp @@ -0,0 +1,39 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + + +#include "Camera/Data/UGC_CameraData.h" + +FPenetrationAvoidanceFeeler::FPenetrationAvoidanceFeeler() + : AdjustmentRot(ForceInit) + , WorldWeight(0.f) + , PawnWeight(0.f) + , ProbeRadius(0) +{ +} + +FPenetrationAvoidanceFeeler::FPenetrationAvoidanceFeeler(const FRotator& InAdjustmentRot, const float& InWorldWeight, const float& InPawnWeight, const float& InExtent) + : AdjustmentRot(InAdjustmentRot) + , WorldWeight(InWorldWeight) + , PawnWeight(InPawnWeight) + , ProbeRadius(InExtent) +{ +} + +FCameraCollisionSettings::FCameraCollisionSettings() +{ + // AdjusmentRotation, WorldWeight, PawnWeight, Extent + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, +00.00f, 0.00f), 0.50f, 1.00f, 15.00f)); + + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, +5.00f, 0.00f), 0.20f, 0.75f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, +10.0f, 0.00f), 0.20f, 0.75f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, -5.00f, 0.00f), 0.20f, 0.75f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, -10.0f, 0.00f), 0.20f, 0.75f, 15.00f)); + + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, +5.00f, 0.00f), 0.15f, 0.50f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, +10.0f, 0.00f), 0.15f, 0.50f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, -5.00f, 0.00f), 0.15f, 0.50f, 15.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+0.00f, -10.0f, 0.00f), 0.15f, 0.50f, 15.00f)); + + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(+15.0f, +0.00f, 0.00f), 0.50f, 1.00f, 10.00f)); + PenetrationAvoidanceFeelers.Add(FPenetrationAvoidanceFeeler(FRotator(-10.0f, +0.00f, 0.00f), 0.50f, 0.50f, 10.00f)); +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IFocusTargetMethod.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IFocusTargetMethod.cpp new file mode 100644 index 0000000..76dacd3 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IFocusTargetMethod.cpp @@ -0,0 +1,18 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Methods/UGC_IFocusTargetMethod.h" +#include "Engine/World.h" + +AActor* UUGC_IFocusTargetMethod::GetTargetLocation_Implementation(AActor* InOwner, FVector OwnerLocation, FVector ViewPointLocation, FRotator ViewPointRotation, FVector& OutTargetLocation) +{ + return nullptr; +} + +UWorld* UUGC_IFocusTargetMethod::GetWorld() const +{ + if (GWorld && GWorld->IsGameWorld() && GWorld->HasBegunPlay()) + { + return GWorld; + } + return nullptr; +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IGetActorsMethod.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IGetActorsMethod.cpp new file mode 100644 index 0000000..ec8b1a9 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Methods/UGC_IGetActorsMethod.cpp @@ -0,0 +1,17 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Methods/UGC_IGetActorsMethod.h" +#include "Engine/World.h" + +void UUGC_IGetActorsMethod::GetActors_Implementation(AActor* InOwner, FVector OwnerLocation, FVector ViewPointLocation, FRotator ViewPointRotation, TArray& OutActors) +{ +} + +UWorld* UUGC_IGetActorsMethod::GetWorld() const +{ + if (GWorld && GWorld->IsGameWorld() && GWorld->HasBegunPlay()) + { + return GWorld; + } + return nullptr; +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraAnimationModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraAnimationModifier.cpp new file mode 100644 index 0000000..29ffa3b --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraAnimationModifier.cpp @@ -0,0 +1,602 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Modifiers/UGC_CameraAnimationModifier.h" +#include "Camera/CameraAnimationHelper.h" +#include "Camera/Modifiers/UGC_CameraCollisionModifier.h" +#include "Camera/PlayerCameraManager.h" +#include "CameraAnimationSequence.h" +#include "CameraAnimationSequencePlayer.h" +#include "Components/SkeletalMeshComponent.h" +#include "DisplayDebugHelpers.h" +#include "Engine/Canvas.h" +#include "Engine/Engine.h" +#include "EntitySystem/MovieSceneEntitySystemLinker.h" +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" +#include "MovieSceneFwd.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/SpringArmComponent.h" +#include "Kismet/GameplayStatics.h" +#include "Kismet/KismetMathLibrary.h" +#include "ProfilingDebugging/CountersTrace.h" +#include "UObject/UObjectGlobals.h" +#include "Runtime/Launch/Resources/Version.h" + +namespace UGCCameraAnimationHelper +{ + FCameraAnimationHandle const UGCInvalid(MAX_int16, 0); + FPenetrationAvoidanceFeeler const CollisionProbe = FPenetrationAvoidanceFeeler(FRotator(+0.00f, +00.00f, 0.00f), 0.50f, 1.00f, 15.00f); + float constexpr CollisionBlendInTime = 0.05f; + float constexpr CollisionBlendOutTime = 0.5f; + FName const CollisionIgnoreActorWithTag = FName("UGC_AnimIgnoreCollision"); +} + +FCameraAnimationHandle UUGC_CameraAnimationModifier::PlaySingleCameraAnimation(UCameraAnimationSequence* Sequence, FCameraAnimationParams Params, ECameraAnimationResetType ResetType, bool bInterruptOthers, bool bDoCollisionChecks) +{ + if (!ensure(Sequence)) + { + return UGCCameraAnimationHelper::UGCInvalid; + } + + TArray InterruptedSequences; + if (IsAnyCameraAnimationSequence()) + { + if (bInterruptOthers) + { + InterruptedSequences.Reserve(ActiveAnimations.Num()); + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + if (ActiveAnimations[i].IsValid() && ActiveAnimations[i].Player != nullptr && ActiveAnimations[i].Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Playing) + { + InterruptedSequences.Add(ActiveAnimations[i].Sequence); + } + } + } + } + + // Always play one animation only + int32 const NewIndex = FindInactiveCameraAnimation(); + check(NewIndex < MAX_uint16); + + const uint16 InstanceSerial = NextInstanceSerialNumber++; + FCameraAnimationHandle InstanceHandle{ (uint16)NewIndex, InstanceSerial }; + + FActiveCameraAnimationInfo& NewCameraAnimation = ActiveAnimations[NewIndex]; + + // Update UGCAnimInfo size + if (ActiveAnimations.Num() > UGCAnimInfo.Num()) + { + int const InfoIndex = UGCAnimInfo.Emplace(); + ensure(InfoIndex == NewIndex); + } + + UGCAnimInfo[NewIndex].ResetType = ResetType; + UGCAnimInfo[NewIndex].bWasEasingOut = false; + UGCAnimInfo[NewIndex].bDoCollisionChecks = bDoCollisionChecks; + UGCAnimInfo[NewIndex].DistBlockedPct = 1.f; + + NewCameraAnimation.Sequence = Sequence; + NewCameraAnimation.Params = Params; + NewCameraAnimation.Handle = InstanceHandle; + + const FName PlayerName = MakeUniqueObjectName(this, UCameraAnimationSequencePlayer::StaticClass(), TEXT("CameraAnimationPlayer")); + NewCameraAnimation.Player = NewObject(this, PlayerName); + const FName CameraStandInName = MakeUniqueObjectName(this, UCameraAnimationSequenceCameraStandIn::StaticClass(), TEXT("CameraStandIn")); + NewCameraAnimation.CameraStandIn = NewObject(this, CameraStandInName); + + // Start easing in immediately if there's any defined. + NewCameraAnimation.bIsEasingIn = (Params.EaseInDuration > 0.f); + NewCameraAnimation.EaseInCurrentTime = 0.f; + NewCameraAnimation.bIsEasingOut = false; + NewCameraAnimation.EaseOutCurrentTime = 0.f; + + // Initialize our stand-in object. + NewCameraAnimation.CameraStandIn->Initialize(Sequence); + + // Make the player always use our stand-in object whenever a sequence wants to spawn or possess an object. + NewCameraAnimation.Player->SetBoundObjectOverride(NewCameraAnimation.CameraStandIn); + + // Initialize it and start playing. + NewCameraAnimation.Player->Initialize(Sequence); + NewCameraAnimation.Player->Play(Params.bLoop, Params.bRandomStartTime); + LastIndex = NewIndex; + + if (bInterruptOthers && InterruptedSequences.Num() > 0) + { + static bool constexpr bInterrupt = true; + for (int i = 0; i < InterruptedSequences.Num(); ++i) + { + OnAnimationEnded.ExecuteIfBound(InterruptedSequences[i], bInterrupt); + } + } + + return InstanceHandle; +} + +void UUGC_CameraAnimationModifier::StopCameraAnimationSequence(UCameraAnimationSequence* Sequence, bool bImmediate) +{ + if (!ActiveAnimations.IsEmpty()) + { + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + if (ActiveAnimations[i].IsValid() && (Sequence == nullptr || ActiveAnimations[i].Sequence == Sequence)) + { + if (UCameraAnimationSequence* InterruptedSequence = ActiveAnimations[i].Sequence) + { + StopCameraAnimation(ActiveAnimations[i].Handle, bImmediate); + static bool constexpr bInterrupt = true; + OnAnimationEnded.ExecuteIfBound(InterruptedSequence, bInterrupt); + } + } + } + } +} + +void UUGC_CameraAnimationModifier::GetCurrentCameraAnimations(TArray& OutAnimations) const +{ + if (!ActiveAnimations.IsEmpty()) + { + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + if (ActiveAnimations[i].IsValid() && ActiveAnimations[i].Player && ActiveAnimations[i].Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Playing) + { + OutAnimations.Add(ActiveAnimations[i].Sequence); + } + } + } +} + +bool UUGC_CameraAnimationModifier::IsCameraAnimationSequenceActive(UCameraAnimationSequence* Sequence) const +{ + if (!ActiveAnimations.IsEmpty()) + { + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + if (ActiveAnimations[i].IsValid() && ActiveAnimations[i].Sequence == Sequence && ActiveAnimations[i].Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Playing) + { + return true; + } + } + } + return false; +} + +bool UUGC_CameraAnimationModifier::IsAnyCameraAnimationSequence() const +{ + if (!ActiveAnimations.IsEmpty()) + { + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + if (ActiveAnimations[i].IsValid() && ActiveAnimations[i].Player && ActiveAnimations[i].Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Playing) + { + return true; + } + } + } + return false; +} + +bool UUGC_CameraAnimationModifier::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + UCameraModifier::ModifyCamera(DeltaTime, InOutPOV); + if (UGCCameraManager) + { + bool const bAnyActive = IsAnyCameraAnimationSequence(); + if (!CollisionModifier) + { + CollisionModifier = UGCCameraManager->FindCameraModifierOfType(); + } + + if (CollisionModifier) + { + bAnyActive ? CollisionModifier->AddSingleRayOverrider(this) : CollisionModifier->RemoveSingleRayOverrider(this); + } + } + UGCTickActiveAnimation(DeltaTime, InOutPOV); + return false; +} + +void UUGC_CameraAnimationModifier::CameraAnimation_SetEasingOutDelegate(FOnCameraAnimationEaseOutStarted& InOnAnimationEaseOutStarted, FCameraAnimationHandle AnimationHandle) +{ + if (AnimationHandle.IsValid() && IsCameraAnimationActive(AnimationHandle)) + { + OnAnimationEaseOutStarted = InOnAnimationEaseOutStarted; + } +} + +void UUGC_CameraAnimationModifier::CameraAnimation_SetEndedDelegate(FOnCameraAnimationEnded& InOnAnimationEnded, FCameraAnimationHandle AnimationHandle) +{ + if (AnimationHandle.IsValid() && IsCameraAnimationActive(AnimationHandle)) + { + OnAnimationEnded = InOnAnimationEnded; + } +} + +void UUGC_CameraAnimationModifier::UGCDeactivateCameraAnimation(FActiveCameraAnimationInfo& ActiveAnimation) +{ + for (auto& a : ActiveAnimations) + { + if (a.Handle == ActiveAnimation.Handle) + { + if (a.Player && !ensure(a.Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Stopped)) + { + a.Player->Stop(); + } + + a = FActiveCameraAnimationInfo(); + } + } +} + +void UUGC_CameraAnimationModifier::UGCTickActiveAnimation(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + UGCCameraManager = Cast(CameraOwner); + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + if (!UGCCameraManager) + { + return; + } + + if (ActiveAnimations.Num() >= 1) + { + for (int i = 0; i < ActiveAnimations.Num(); ++i) + { + + FActiveCameraAnimationInfo& ActiveAnimation = ActiveAnimations[i]; + if (ActiveAnimation.IsValid()) + { +#if ENABLE_DRAW_DEBUG + UGCDebugAnimation(ActiveAnimation, DeltaTime); +#endif + // float const Dilation = UGameplayStatics::GetGlobalTimeDilation(GetWorld()); + // float const UndilatedDeltaTime = FMath::IsNearlyZero(Dilation) ? 0.f : DeltaTime / Dilation; + UGCTickAnimation(ActiveAnimation, DeltaTime, InOutPOV, i); + UGCTickAnimCollision(ActiveAnimation, DeltaTime, InOutPOV, i); + + if (ActiveAnimation.Player->GetPlaybackStatus() == EMovieScenePlayerStatus::Stopped) + { + // Here animation has just finished (ease out has completed as well) + UGCDeactivateCameraAnimation(ActiveAnimation); + static bool constexpr bInterrupt = false; + OnAnimationEnded.ExecuteIfBound(ActiveAnimation.Sequence, bInterrupt); + } + } + } + } +} + +void UUGC_CameraAnimationModifier::UGCTickAnimation(FActiveCameraAnimationInfo& CameraAnimation, float DeltaTime, FMinimalViewInfo& InOutPOV, int Index) +{ + check(CameraAnimation.Player); + check(CameraAnimation.CameraStandIn); + + const FCameraAnimationParams Params = CameraAnimation.Params; + UCameraAnimationSequencePlayer* Player = CameraAnimation.Player; + UCameraAnimationSequenceCameraStandIn* CameraStandIn = CameraAnimation.CameraStandIn; + + const FFrameRate InputRate = Player->GetInputRate(); + const FFrameTime CurrentPosition = Player->GetCurrentPosition(); + const float CurrentTime = InputRate.AsSeconds(CurrentPosition); + const float DurationTime = InputRate.AsSeconds(Player->GetDuration()) * Params.PlayRate; + + const float ScaledDeltaTime = DeltaTime * Params.PlayRate; + + const float NewTime = CurrentTime + ScaledDeltaTime; + const FFrameTime NewPosition = CurrentPosition + DeltaTime * Params.PlayRate * InputRate; + + // Advance any easing times. + if (CameraAnimation.bIsEasingIn) + { + CameraAnimation.EaseInCurrentTime += DeltaTime; + } + if (CameraAnimation.bIsEasingOut) + { + CameraAnimation.EaseOutCurrentTime += DeltaTime; + } + + ECameraAnimationResetType ResetType = UGCAnimInfo[Index].ResetType; + bool const bWasEasingOut = UGCAnimInfo[Index].bWasEasingOut; + + // Start easing out if we're nearing the end. + // CameraAnimation may already be easing out if StopCameraAnimation has been called. + if (!Player->GetIsLooping() && !CameraAnimation.bIsEasingOut) + { + const float BlendOutStartTime = DurationTime - Params.EaseOutDuration; + if (NewTime > BlendOutStartTime) + { + CameraAnimation.bIsEasingOut = true; + CameraAnimation.EaseOutCurrentTime = NewTime - BlendOutStartTime; + + if (!bWasEasingOut) + { + // Here animation has just started easing out but hasn't finished yet + OnAnimationEaseOutStarted.ExecuteIfBound(CameraAnimation.Sequence); + } + } + } + + // Check if we're done easing in or out. + bool bIsDoneEasingOut = false; + if (CameraAnimation.bIsEasingIn) + { + if (CameraAnimation.EaseInCurrentTime > Params.EaseInDuration || Params.EaseInDuration == 0.f) + { + CameraAnimation.bIsEasingIn = false; + } + } + if (CameraAnimation.bIsEasingOut) + { + if (CameraAnimation.EaseOutCurrentTime > Params.EaseOutDuration) + { + bIsDoneEasingOut = true; + } + } + + // Figure out the final easing weight. + const float EasingInT = FMath::Clamp((CameraAnimation.EaseInCurrentTime / Params.EaseInDuration), 0.f, 1.f); + const float EasingInWeight = CameraAnimation.bIsEasingIn ? + EvaluateEasing(Params.EaseInType, EasingInT) : 1.f; + + const float EasingOutT = FMath::Clamp((1.f - CameraAnimation.EaseOutCurrentTime / Params.EaseOutDuration), 0.f, 1.f); + const float EasingOutWeight = CameraAnimation.bIsEasingOut ? + EvaluateEasing(Params.EaseOutType, EasingOutT) : 1.f; + + const float TotalEasingWeight = FMath::Min(EasingInWeight, EasingOutWeight); + + // We might be done playing. Normally the player will stop on its own, but there are other situation in which + // the responsibility falls to this code: + // - If the animation is looping and waiting for an explicit Stop() call on us. + // - If there was a Stop() call with bImmediate=false to let an animation blend out. + if (bIsDoneEasingOut || TotalEasingWeight <= 0.f) + { + Player->Stop(); + return; + } + + UMovieSceneEntitySystemLinker* Linker = Player->GetEvaluationTemplate().GetEntitySystemLinker(); + CameraStandIn->Reset(InOutPOV, Linker); + + // Get the "unanimated" properties that need to be treated additively. + const float OriginalFieldOfView = CameraStandIn->FieldOfView; + + // Update the sequence. + Player->Update(NewPosition); + + // Recalculate properties that might be invalidated by other properties having been animated. + CameraStandIn->RecalcDerivedData(); + + // Grab the final animated (animated) values, figure out the delta, apply scale, and feed that into the result. + // Transform is always treated as a local, additive value. The data better be good. + const float Scale = Params.Scale * TotalEasingWeight; + const FTransform AnimatedTransform = CameraStandIn->GetTransform(); + FVector AnimatedLocation = AnimatedTransform.GetLocation() * Scale; + FRotator AnimatedRotation = AnimatedTransform.GetRotation().Rotator() * Scale; + const FCameraAnimationHelperOffset CameraOffset{ AnimatedLocation, AnimatedRotation }; + + FVector OwnerLocation = GetViewTarget()->GetActorLocation(); + + // If using a character, camera should start from the pivot location of the mesh. + { + ACharacter* OwnerCharacter = GetViewTargetAs(); + if (OwnerCharacter && OwnerCharacter->GetMesh()) + { + OwnerLocation = OwnerCharacter->GetMesh()->GetComponentLocation(); + } + } + + // TO DO #GravityCompatibility + FRotator const OwnerRot = FRotator(0.f, GetViewTarget()->GetActorRotation().Yaw, 0.f); + const FMatrix OwnerRotationMatrix = FRotationMatrix(OwnerRot); + + FMinimalViewInfo InPOV = InOutPOV; + + // Blend from current camera location to actor location + InPOV.Location = FMath::Lerp(InOutPOV.Location, OwnerLocation, Scale); + InPOV.Rotation = FMath::Lerp(InOutPOV.Rotation, OwnerRot, Scale); + + FCameraAnimationHelper::ApplyOffset(OwnerRotationMatrix, InPOV, CameraOffset, AnimatedLocation, AnimatedRotation); + + InOutPOV.Location = AnimatedLocation; + InOutPOV.Rotation = AnimatedRotation; + + // Blend back depending on reset type + if (ResetType != ECameraAnimationResetType::BackToStart + && CameraOwner && CameraOwner->GetOwningPlayerController() && CameraAnimation.bIsEasingOut && !bWasEasingOut) + { + FRotator TargetRot = OwnerRot; + switch (ResetType) + { + case ECameraAnimationResetType::ContinueFromEnd: + { + bool const bIsStrafing = UGCCameraManager->IsOwnerStrafing(); + if (!bIsStrafing) + { + // TO DO #GravityCompatibility + TargetRot = FRotator(InOutPOV.Rotation.Pitch, InOutPOV.Rotation.Yaw, 0.f); + } + break; + } + default: + break; + } + CameraOwner->GetOwningPlayerController()->SetControlRotation(TargetRot); + } + + // FieldOfView follows the current camera's value every frame, so we can compute how much the animation is + // changing it. + const float AnimatedFieldOfView = CameraStandIn->FieldOfView; + const float DeltaFieldOfView = AnimatedFieldOfView - OriginalFieldOfView; + InOutPOV.FOV = OriginalFieldOfView + DeltaFieldOfView * Scale; + + // Add the post-process settings. + if (CameraOwner != nullptr && CameraStandIn->PostProcessBlendWeight > 0.f) + { + CameraOwner->AddCachedPPBlend(CameraStandIn->PostProcessSettings, CameraStandIn->PostProcessBlendWeight); + } + + UGCAnimInfo[Index].bWasEasingOut = CameraAnimation.bIsEasingOut; +} + +FVector UUGC_CameraAnimationModifier::GetTraceSafeLocation(FMinimalViewInfo const& InPOV) +{ + AActor* TargetActor = GetViewTarget(); + FVector SafeLocation = TargetActor ? TargetActor->GetActorLocation() : FVector::Zero(); + if (TargetActor && UGCCameraManager) + { + if (USpringArmComponent* SpringArm = UGCCameraManager->GetOwnerSpringArmComponent()) + { + SafeLocation = SpringArm->GetComponentLocation() + SpringArm->TargetOffset; + } + else if (UPrimitiveComponent const* PPActorRootComponent = Cast(TargetActor->GetRootComponent())) + { + // Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming. + // Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible. + // Pick closest point on capsule to our aim line. + FVector ClosestPointOnLineToCapsuleCenter; + FMath::PointDistToLine(SafeLocation, InPOV.Rotation.Vector(), InPOV.Location, ClosestPointOnLineToCapsuleCenter); + + // Adjust Safe distance height to be same as aim line, but within capsule. + float const PushInDistance = UGCCameraAnimationHelper::CollisionProbe.ProbeRadius; + float const MaxHalfHeight = TargetActor->GetSimpleCollisionHalfHeight() - PushInDistance; + SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight); + + float DistanceSqr = 0.f; + PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation); + // Push back inside capsule to avoid initial penetration when doing line checks. + SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance; + } + } + return SafeLocation; +} + +void UUGC_CameraAnimationModifier::UGCTickAnimCollision(FActiveCameraAnimationInfo& CameraAnimation, float DeltaTime, FMinimalViewInfo& InOutPOV, int Index) +{ + if (!UGCAnimInfo[Index].bDoCollisionChecks) + { + return; + } + + check(CameraAnimation.Player); + check(CameraAnimation.CameraStandIn); + const FCameraAnimationParams& Params = CameraAnimation.Params; + const float BlendInTime = FMath::Max(UGCCameraAnimationHelper::CollisionBlendInTime, UE_KINDA_SMALL_NUMBER); + float BlendOutTime = FMath::Max(UGCCameraAnimationHelper::CollisionBlendOutTime, UE_KINDA_SMALL_NUMBER); + if (CameraAnimation.bIsEasingOut) + { + BlendOutTime = FMath::Min(Params.EaseOutDuration - CameraAnimation.EaseOutCurrentTime, UGCCameraAnimationHelper::CollisionBlendOutTime); + BlendOutTime = FMath::Max(BlendOutTime, UE_KINDA_SMALL_NUMBER); + } + + FVector SafeLoc = GetTraceSafeLocation(InOutPOV); + + const FVector CameraLoc = InOutPOV.Location; + const FVector BaseRay = CameraLoc - SafeLoc; + + if (BaseRay.IsNearlyZero()) + { + return; + } + + const FRotationMatrix BaseMatrix(BaseRay.Rotation()); + const FVector Right = BaseMatrix.GetUnitAxis(EAxis::Y); + const FVector Up = BaseMatrix.GetUnitAxis(EAxis::Z); + + float HardBlockedPct = UGCAnimInfo[Index].DistBlockedPct; + float SoftBlockedPct = UGCAnimInfo[Index].DistBlockedPct; + float BlockedThisFrame = 1.f; + + UWorld* World = GetWorld(); + int32 NbrHits = 0; + const AActor* OwningActor = GetViewTarget(); + + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(CameraPen), false, OwningActor); + QueryParams.AddIgnoredActor(OwningActor); + + { + const FPenetrationAvoidanceFeeler& Feeler = UGCCameraAnimationHelper::CollisionProbe; + const FRotator OffsetRot = Feeler.AdjustmentRot; + + FVector RayTarget = BaseRay.RotateAngleAxis(OffsetRot.Yaw, Up).RotateAngleAxis(OffsetRot.Pitch, Right) + SafeLoc; + + FHitResult Hit; + FCollisionShape Shape = FCollisionShape::MakeSphere(Feeler.ProbeRadius); + bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, ECollisionChannel::ECC_Camera, Shape, QueryParams); + + if (bHit && Hit.GetActor()) + { + if (Hit.GetActor()->ActorHasTag(UGCCameraAnimationHelper::CollisionIgnoreActorWithTag)) + { + QueryParams.AddIgnoredActor(Hit.GetActor()); + } + else + { + ++NbrHits; + const float Weight = Hit.GetActor()->IsA() ? Feeler.PawnWeight : Feeler.WorldWeight; + float NewBlockPct = Hit.Time + (1.f - Hit.Time) * (1.f - Weight); + NewBlockPct = (Hit.Location - SafeLoc).Size() / (RayTarget - SafeLoc).Size(); + + BlockedThisFrame = FMath::Min(NewBlockPct, BlockedThisFrame); + HardBlockedPct = BlockedThisFrame; + } + } + } +#if ENABLE_DRAW_DEBUG + if (NbrHits > 0) + { + if (bDebug && GEngine != nullptr) + { + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6 + FString const& DebugText = FString::Printf(TEXT("UGC_CameraAnimationModifier: %d feeler%hs colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#else + FString const& DebugText = FString::Printf(TEXT("UGC_CameraAnimationModifier: %d feeler%s colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#endif + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor(150, 150, 200), DebugText); + } + } +#endif + + if (UGCAnimInfo[Index].DistBlockedPct < BlockedThisFrame) + { + UGCAnimInfo[Index].DistBlockedPct += DeltaTime / BlendOutTime * (BlockedThisFrame - UGCAnimInfo[Index].DistBlockedPct); + } + else if (UGCAnimInfo[Index].DistBlockedPct > HardBlockedPct) + { + UGCAnimInfo[Index].DistBlockedPct = HardBlockedPct; + } + else if (UGCAnimInfo[Index].DistBlockedPct > SoftBlockedPct) + { + UGCAnimInfo[Index].DistBlockedPct -= DeltaTime / BlendInTime * (UGCAnimInfo[Index].DistBlockedPct - SoftBlockedPct); + } + + UGCAnimInfo[Index].DistBlockedPct = FMath::Clamp(UGCAnimInfo[Index].DistBlockedPct, 0.f, 1.f); + InOutPOV.Location = SafeLoc + (CameraLoc - SafeLoc) * UGCAnimInfo[Index].DistBlockedPct; +} + +#if ENABLE_DRAW_DEBUG +void UUGC_CameraAnimationModifier::UGCDebugAnimation(FActiveCameraAnimationInfo& ActiveAnimation, float DeltaTime) +{ + if (bDebug && ActiveAnimation.IsValid() && GEngine) + { + const FFrameRate InputRate = ActiveAnimation.Player->GetInputRate(); +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 + const FFrameTime DurationFrames = ActiveAnimation.Player->GetDuration(); +#elif ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION < 3 + const FFrameNumber DurationFrames = ActiveAnimation.Player->GetDuration(); +#endif + const FFrameTime CurrentPosition = ActiveAnimation.Player->GetCurrentPosition(); + + const float CurrentTime = InputRate.AsSeconds(CurrentPosition); + const float DurationSeconds = InputRate.AsSeconds(DurationFrames); + + const FString LoopString = ActiveAnimation.Params.bLoop ? TEXT(" - Looping") : TEXT(""); + const FString EaseInString = ActiveAnimation.bIsEasingIn ? FString::Printf(TEXT(" - Easing In: %f / %f"), ActiveAnimation.EaseInCurrentTime, ActiveAnimation.Params.EaseInDuration) : TEXT(""); + const FString EaseOutString = ActiveAnimation.bIsEasingOut ? FString::Printf(TEXT(" - Easing Out: %f / %f"), ActiveAnimation.EaseOutCurrentTime, ActiveAnimation.Params.EaseOutDuration) : TEXT(""); + const FString DebugText = FString::Printf(TEXT("UGC_CameraAnimationModifier: %s - PlayRate: %f%s - Duration: %f - Elapsed: %f%s%s"), *GetNameSafe(ActiveAnimation.Sequence), ActiveAnimation.Params.PlayRate, *LoopString, DurationSeconds, CurrentTime, *EaseInString, *EaseOutString); + + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor(49, 61, 255), DebugText); + } +} +#endif \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraCollisionModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraCollisionModifier.cpp new file mode 100644 index 0000000..9971dfa --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraCollisionModifier.cpp @@ -0,0 +1,332 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Modifiers/UGC_CameraCollisionModifier.h" + +#include "Camera/PlayerCameraManager.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "CineCameraActor.h" +#include "CollisionQueryParams.h" +#include "CollisionShape.h" +#include "Components/PrimitiveComponent.h" +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Character.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/CameraBlockingVolume.h" +#include "GameFramework/SpringArmComponent.h" +#include "Runtime/Launch/Resources/Version.h" + +/* + * DEPRECATED. USE UGC_SpringArmComponent INSTEAD. + */ + +namespace CollisionHelpers +{ +#if ENABLE_DRAW_DEBUG + FColor const DebugColor = FColor(150, 150, 200); +#endif +} + +UUGC_CameraCollisionModifier::UUGC_CameraCollisionModifier() +{ + Priority = 134; + bExclusive = true; +} + +bool UUGC_CameraCollisionModifier::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + Super::ModifyCamera(DeltaTime, InOutPOV); + + if (!bPlayDuringCameraAnimations) + { + if (UGCCameraManager && UGCCameraManager->IsPlayingAnyCameraAnimation()) + { + return false; + } + } + + if (SpringArm) + { + // Don't do collision tests is spring arm is taking care of them. + if (SpringArm->bDoCollisionTest && CollisionSettings.bPreventPenetration) + { +#if ENABLE_DRAW_DEBUG + if (GEngine != nullptr) + { + FString const CameraManagerName = GetNameSafe(CameraOwner); + FString const ActorName = GetNameSafe(GetViewTarget()); + FString const SpringName = GetNameSafe(SpringArm); + FString const DebugText = FString::Printf(TEXT("Actor %s has Collision Checks enabled on SpringArm %s but Camera Manager %s has Camera Modifier UGC_CameraCollisionModifier. This is not allowed so the modifier will be aborted.") + , *ActorName, *SpringName, *CameraManagerName); + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Red, DebugText); + } +#endif + return false; + } + } + + // Do collision checks + UpdatePreventPenetration(DeltaTime, InOutPOV); + + return false; +} + +void UUGC_CameraCollisionModifier::UpdatePreventPenetration(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + if (!CollisionSettings.bPreventPenetration) + { + return; + } + + FVector const SafeLocation = GetTraceSafeLocation(InOutPOV); + + // Then aim line to desired camera position + bool const bSingleRayPenetrationCheck = !CollisionSettings.bDoPredictiveAvoidance || !SingleRayOverriders.IsEmpty(); + + if (bSingleRayPenetrationCheck) + { +#if ENABLE_DRAW_DEBUG + if (bDebug && GEngine != nullptr) + { + FString const& DebugText = FString::Printf(TEXT("UGC_CameraCollisionModifier: Single Ray mode enabled.")); + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, CollisionHelpers::DebugColor, DebugText); + } +#endif + } + + if (AActor* TargetActor = GetViewTarget()) + { + PreventCameraPenetration(*TargetActor, SafeLocation, InOutPOV.Location, DeltaTime, AimLineToDesiredPosBlockedPct, bSingleRayPenetrationCheck); + } +} + +FVector UUGC_CameraCollisionModifier::GetTraceSafeLocation(FMinimalViewInfo const& InPOV) +{ + AActor* TargetActor = GetViewTarget(); + FVector SafeLocation = TargetActor ? TargetActor->GetActorLocation() : FVector::Zero(); + if (TargetActor) + { + if (SpringArm) + { + SafeLocation = SpringArm->GetComponentLocation() + SpringArm->TargetOffset; + } + else if (UPrimitiveComponent const* PPActorRootComponent = Cast(TargetActor->GetRootComponent())) + { + // Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming. + // Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible. + // Pick closest point on capsule to our aim line. + FVector ClosestPointOnLineToCapsuleCenter; + FMath::PointDistToLine(SafeLocation, InPOV.Rotation.Vector(), InPOV.Location, ClosestPointOnLineToCapsuleCenter); + + // Adjust Safe distance height to be same as aim line, but within capsule. + float const PushInDistance = CollisionSettings.PenetrationAvoidanceFeelers[0].ProbeRadius; + float const MaxHalfHeight = TargetActor->GetSimpleCollisionHalfHeight() - PushInDistance; + SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight); + + float DistanceSqr = 0.f; + PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation); + // Push back inside capsule to avoid initial penetration when doing line checks. + if (CollisionSettings.PenetrationAvoidanceFeelers.Num() > 0) + { + SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance; + } + } + } + return SafeLocation; +} + +void UUGC_CameraCollisionModifier::PreventCameraPenetration(AActor const& ViewTarget, FVector const& SafeLoc, FVector& OutCameraLoc, float const& DeltaTime, float& OutDistBlockedPct, bool bSingleRayOnly) +{ +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.Reset(); +#endif + + FVector const& CameraLoc = OutCameraLoc; + float HardBlockedPct = OutDistBlockedPct; + float SoftBlockedPct = OutDistBlockedPct; + + FVector BaseRay = CameraLoc - SafeLoc; + FRotationMatrix BaseRayMatrix(BaseRay.Rotation()); + FVector BaseRayLocalUp, BaseRayLocalFwd, BaseRayLocalRight; + + BaseRayMatrix.GetScaledAxes(BaseRayLocalFwd, BaseRayLocalRight, BaseRayLocalUp); + + float DistBlockedPctThisFrame = 1.f; + + int32 const NumRaysToShoot = bSingleRayOnly ? FMath::Min(1, CollisionSettings.PenetrationAvoidanceFeelers.Num()) : CollisionSettings.PenetrationAvoidanceFeelers.Num(); + + CollidingFeelers = TStaticBitArray<128U>(); + + FCollisionQueryParams SphereParams(SCENE_QUERY_STAT(CameraPen), false, nullptr/*PlayerCamera*/); + + SphereParams.AddIgnoredActor(&ViewTarget); + + FCollisionShape SphereShape = FCollisionShape::MakeSphere(0.f); + UWorld* World = GetWorld(); + + int32 NbrHits = 0; + + for (int32 RayIdx = 0; RayIdx < NumRaysToShoot; ++RayIdx) + { + FPenetrationAvoidanceFeeler& Feeler = CollisionSettings.PenetrationAvoidanceFeelers[RayIdx]; + FRotator AdjustmentRot = Feeler.AdjustmentRot; + + if (RayIdx == 0 && Feeler.AdjustmentRot != FRotator::ZeroRotator) + { +#if ENABLE_DRAW_DEBUG + if (GEngine != nullptr) + { + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Red, TEXT("UGC_CameraCollisionModifier: First Penetration Avoidance Feeler should always have an adjustment roation equal to 0,0,0!.")); + } +#endif + AdjustmentRot = FRotator::ZeroRotator; + } + + // calc ray target + FVector RayTarget; + { + // TO DO #GravityCompatibility + FVector RotatedRay = BaseRay.RotateAngleAxis(AdjustmentRot.Yaw, BaseRayLocalUp); + RotatedRay = RotatedRay.RotateAngleAxis(AdjustmentRot.Pitch, BaseRayLocalRight); + RayTarget = SafeLoc + RotatedRay; + } + + // Cast for world and pawn hits separately. this is so we can safely ignore the camera's target pawn. + bool const bForceSmallShape = bSingleRayOnly && !SingleRayOverriders.IsEmpty(); + SphereShape.Sphere.Radius = bForceSmallShape ? 2.f : Feeler.ProbeRadius; + ECollisionChannel TraceChannel = ECC_Camera; + + // Do multi-line check to make sure the hits we throw out aren't masking real hits behind (these are important rays). + // Passing camera as actor so that camerablockingvolumes know when it's the camera doing traces. + FHitResult Hit; + const bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, TraceChannel, SphereShape, SphereParams); +#if ENABLE_DRAW_DEBUG + if (World->TimeSince(LastDrawDebugTime) < 1.f) + { + DrawDebugSphere(World, SafeLoc, SphereShape.Sphere.Radius, 8, FColor::Red); + DrawDebugSphere(World, bHit ? Hit.Location : RayTarget, SphereShape.Sphere.Radius, 8, FColor::Red); + DrawDebugLine(World, SafeLoc, bHit ? Hit.Location : RayTarget, FColor::Red); + } +#endif // ENABLE_DRAW_DEBUG + + const AActor* HitActor = Hit.GetActor(); + + if (bHit && HitActor) + { + bool bIgnoreHit = false; + + if (HitActor->ActorHasTag(CollisionSettings.IgnoreCameraCollisionTag)) + { + bIgnoreHit = true; + SphereParams.AddIgnoredActor(HitActor); + } + + // Ignore CameraBlockingVolume hits that occur in front of the ViewTarget. + if (!bIgnoreHit && HitActor->IsA()) + { + const FVector ViewTargetForwardXY = ViewTarget.GetActorForwardVector().GetSafeNormal2D(); + const FVector ViewTargetLocation = ViewTarget.GetActorLocation(); + const FVector HitOffset = Hit.Location - ViewTargetLocation; + const FVector HitDirectionXY = HitOffset.GetSafeNormal2D(); + const float DotHitDirection = FVector::DotProduct(ViewTargetForwardXY, HitDirectionXY); + if (DotHitDirection > 0.0f) + { + bIgnoreHit = true; + // Ignore this CameraBlockingVolume on the remaining sweeps. + SphereParams.AddIgnoredActor(HitActor); + } + else + { +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); +#endif + } + } + + if (!bIgnoreHit) + { + CollidingFeelers[RayIdx] = true; + ++NbrHits; + + float const Weight = Cast(Hit.GetActor()) ? Feeler.PawnWeight : Feeler.WorldWeight; + float NewBlockPct = Hit.Time; + NewBlockPct += (1.f - NewBlockPct) * (1.f - Weight); + + // Recompute blocked pct taking into account pushout distance. + NewBlockPct = (Hit.Location - SafeLoc).Size() / (RayTarget - SafeLoc).Size(); + DistBlockedPctThisFrame = FMath::Min(NewBlockPct, DistBlockedPctThisFrame); + +#if ENABLE_DRAW_DEBUG + DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); +#endif + } + } + + if (RayIdx == 0) + { + // Don't interpolate toward this one, snap to it. THIS ASSUMES RAY 0 IS THE CENTER/MAIN RAY. + HardBlockedPct = DistBlockedPctThisFrame; + } + else + { + SoftBlockedPct = DistBlockedPctThisFrame; + } + } + +#if ENABLE_DRAW_DEBUG + if (NbrHits > 0) + { + if (bDebug && GEngine != nullptr) + { + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6 + FString const& DebugText = FString::Printf(TEXT("UGC_CameraCollisionModifier: %d feeler%hs colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#else + FString const& DebugText = FString::Printf(TEXT("UGC_CameraCollisionModifier: %d feeler%s colliding."), NbrHits, NbrHits > 1 ? "s" : ""); +#endif + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, CollisionHelpers::DebugColor, DebugText); + } + } +#endif + + if (OutDistBlockedPct < DistBlockedPctThisFrame) + { + // interpolate smoothly out + if (CollisionSettings.PenetrationBlendOutTime > DeltaTime) + { + OutDistBlockedPct = OutDistBlockedPct + DeltaTime / CollisionSettings.PenetrationBlendOutTime * (DistBlockedPctThisFrame - OutDistBlockedPct); + } + else + { + OutDistBlockedPct = DistBlockedPctThisFrame; + } + } + else + { + if (OutDistBlockedPct > HardBlockedPct) + { + OutDistBlockedPct = HardBlockedPct; + } + else if (OutDistBlockedPct > SoftBlockedPct) + { + // interpolate smoothly in + if (CollisionSettings.PenetrationBlendInTime > DeltaTime) + { + OutDistBlockedPct = OutDistBlockedPct - DeltaTime / CollisionSettings.PenetrationBlendInTime * (OutDistBlockedPct - SoftBlockedPct); + } + else + { + OutDistBlockedPct = SoftBlockedPct; + } + } + } + + OutDistBlockedPct = FMath::Clamp(OutDistBlockedPct, 0.f, 1.f); + if (OutDistBlockedPct < (1.f - ZERO_ANIMWEIGHT_THRESH)) + { + OutCameraLoc = SafeLoc + (CameraLoc - SafeLoc) * OutDistBlockedPct; + } +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraDitheringModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraDitheringModifier.cpp new file mode 100644 index 0000000..9212077 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraDitheringModifier.cpp @@ -0,0 +1,421 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + + +#include "Camera/Modifiers/UGC_CameraDitheringModifier.h" + +#include "CollisionQueryParams.h" +#include "CollisionShape.h" +#include "Camera/PlayerCameraManager.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 +#include "Engine/OverlapResult.h" +#endif +#include "GameFramework/SpringArmComponent.h" +#include "GameFramework/Actor.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Components/PrimitiveComponent.h" +#include "Components/MeshComponent.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Pawn.h" +#include "Materials/MaterialInterface.h" +#include "Materials/MaterialLayersFunctions.h" +#include "Materials/MaterialParameterCollection.h" +#include "Materials/MaterialParameterCollectionInstance.h" +#include "Misc/Guid.h" + +namespace DitherHelpers +{ + bool DoAnyComponentsOverlap(AActor* inActor, ECollisionChannel overlapChannel) + { + if (inActor) + { + for (UActorComponent* ActorComponent : inActor->GetComponents()) + { + UPrimitiveComponent* Primitive = Cast(ActorComponent); + if (Primitive && Primitive->IsCollisionEnabled()) + { + if (Primitive->GetCollisionResponseToChannel(overlapChannel) == ECollisionResponse::ECR_Overlap) + { + return true; + } + } + } + } + return false; + } + + int32 FindInactiveDitherState(TArray& DitherStates) + { + for (int32 Index = 0; Index < DitherStates.Num(); ++Index) + { + const FDitheredActorState& DitherState(DitherStates[Index]); + if (!DitherState.IsValid()) + { + return Index; + } + } + + return DitherStates.Emplace(); + } + + bool DitherStatesContain(TArray& DitherStates, AActor* InActor) + { + if (InActor) + { + for (auto const& State : DitherStates) + { + if (State.Actor == InActor) + { + return true; + } + } + } + + return false; + } + + FVector GetTraceSafeLocation(AActor* TargetActor, USpringArmComponent* SpringArm, FMinimalViewInfo const& InPOV) + { + FVector SafeLocation = TargetActor ? TargetActor->GetActorLocation() : FVector::Zero(); + if (TargetActor) + { + // Try to get spring arm component + if (SpringArm) + { + SafeLocation = SpringArm->GetComponentLocation() + SpringArm->TargetOffset; + } + else if (UPrimitiveComponent const* PPActorRootComponent = Cast(TargetActor->GetRootComponent())) + { + // Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming. + // Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible. + // Pick closest point on capsule to our aim line. + FVector ClosestPointOnLineToCapsuleCenter; + FMath::PointDistToLine(SafeLocation, InPOV.Rotation.Vector(), InPOV.Location, ClosestPointOnLineToCapsuleCenter); + + // Adjust Safe distance height to be same as aim line, but within capsule. + float const PushInDistance = 0.f; + float const MaxHalfHeight = TargetActor->GetSimpleCollisionHalfHeight() - PushInDistance; + SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight); + + float DistanceSqr = 0.f; + PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation); + // Push back inside capsule to avoid initial penetration when doing line checks. + SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance; + } + } + return SafeLocation; + } +} + +void FDitheredActorState::StartDithering(AActor* InActor, EDitherType InDitherType) +{ + if (InActor) + { + Actor = InActor; + CollisionTime = 0.f; + bIsDitheringIn = false; + bIsDitheringOut = false; + DitherType = InDitherType; + } +} + +void FDitheredActorState::Invalidate() +{ + Actor = nullptr; + CurrentOpacity = 1.f; + CollisionTime = 0.f; + bIsDitheringIn = false; + bIsDitheringOut = false; + DitherType = EDitherType::None; +} + +void FDitheredActorState::ComputeOpacity(float DeltaTime, float DitherInSpeed, float DitherOutSpeed, float DitherMin) +{ + if (!IsValid()) + { + return; + } + + bool const bCanDither = !bIsDitheringOut && bIsDitheringIn; + CurrentOpacity = FMath::FInterpTo(CurrentOpacity, bCanDither ? DitherMin : 1.f, DeltaTime, bIsDitheringOut ? DitherOutSpeed : DitherInSpeed); +} + +UUGC_CameraDitheringModifier::UUGC_CameraDitheringModifier() +{ + Priority = 255; // You want this modifier to be the last one to be executed so that it always knows the final camera position. +} + +void UUGC_CameraDitheringModifier::ResetDitheredActors_Implementation() +{ + for (auto& DitherState : DitheredActorStates) + { + DitherState.bIsDitheringIn = false; + DitherState.bIsDitheringOut = true; + DitherState.CurrentOpacity = 1.f; + static float constexpr DeltaTime = 1 / 60.f; + ApplyDithering(DeltaTime, DitherState); + DitherState.Invalidate(); + } +} + +bool UUGC_CameraDitheringModifier::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + Super::ModifyCamera(DeltaTime, InOutPOV); + if (CameraOwner && CameraOwner->PCOwner && CameraOwner->PCOwner->GetPawn()) + { + FCollisionQueryParams QueryParams(TEXT("CameraDithering")); + + // Get All overlapped actors and LOS-blocking actors + TArray ActorsToDither; + + if (DitheringSettings.bDitherOverlaps) + { + if (!DitheringSettings.bDitherOwner) + { + QueryParams.AddIgnoredActor(CameraOwner->PCOwner->GetPawn()); + if (AActor* PendingTarget = CameraOwner->PendingViewTarget.Target) + { + QueryParams.AddIgnoredActor(PendingTarget); + } + } + + TArray OutOverlaps; + GetWorld()->OverlapMultiByChannel(OutOverlaps, InOutPOV.Location, FQuat::Identity, DitheringSettings.DitherOverlapChannel, FCollisionShape::MakeSphere(DitheringSettings.SphereCollisionRadius), QueryParams); + +#if ENABLE_DRAW_DEBUG + if (GetWorld()->DebugDrawTraceTag == TEXT("CameraDithering")) + { + DrawDebugSphere(GetWorld(), InOutPOV.Location, DitheringSettings.SphereCollisionRadius, 32, FColor::Red); + } +#endif + + for (auto const& Overlap : OutOverlaps) + { + // Ignore null actors + if (!Overlap.GetActor()) + { + continue; + } + + TArray OverlapActors; + OverlapActors.Add(Overlap.GetActor()); + + if (DitheringSettings.bDitherAttachedComponents) + { + Overlap.GetActor()->GetAttachedActors(OverlapActors, false, true); + } + + for (auto& Actor : OverlapActors) + { + // Ignore null actors or actors with IgnoreDitheringTag + if (!Actor || Actor->ActorHasTag(DitheringSettings.IgnoreDitheringTag)) + { + continue; + } + + bool const bIsCollisionResponseCorrect = DitherHelpers::DoAnyComponentsOverlap(Actor, DitheringSettings.DitherOverlapChannel); + if (!bIsCollisionResponseCorrect) + { + continue; + } + + ActorsToDither.Add(Actor); + + // Start dithering actor if not done already + if (!DitherHelpers::DitherStatesContain(DitheredActorStates, Actor)) + { + QueryParams.AddIgnoredActor(Actor); + int32 Index = DitherHelpers::FindInactiveDitherState(DitheredActorStates); + DitheredActorStates[Index].StartDithering(Actor, EDitherType::OverlappingCamera); + } + } + } + } + + if (DitheringSettings.bDitherLineOfSight) + { + QueryParams.AddIgnoredActor(CameraOwner->PCOwner->GetPawn()); + if (AActor* PendingTarget = CameraOwner->PendingViewTarget.Target) + { + QueryParams.AddIgnoredActor(PendingTarget); + } + + TArray OutOverlaps; + FVector const EndLocation = DitherHelpers::GetTraceSafeLocation(GetViewTarget(), SpringArm, InOutPOV); + FVector const ToTarget = (EndLocation - InOutPOV.Location).GetSafeNormal(); + FVector const StartLocation = InOutPOV.Location + ToTarget * DitheringSettings.SphereCollisionRadius; + FVector const Delta = EndLocation - StartLocation; + FQuat const Rotation = FRotationMatrix::MakeFromX(Delta.GetSafeNormal()).ToQuat(); + FVector const BoxOrigin = Delta * 0.5f + StartLocation; + + FVector const BoxExtent = FVector(Delta.Size() * 0.495f, DitheringSettings.LOSProbeSize, DitheringSettings.LOSProbeSize); + FCollisionShape const Box = FCollisionShape::MakeBox(BoxExtent); + + GetWorld()->OverlapMultiByChannel(OutOverlaps, BoxOrigin, Rotation, DitheringSettings.DitherLOSChannel, Box, QueryParams); + +#if ENABLE_DRAW_DEBUG + if (GetWorld()->DebugDrawTraceTag == TEXT("CameraDithering")) + { + DrawDebugBox(GetWorld(), BoxOrigin, BoxExtent, Rotation, FColor::Red); + } +#endif + + for (auto const& Overlap : OutOverlaps) + { + // Ignore null actors + if (!Overlap.GetActor()) + { + continue; + } + + TArray OverlapActors; + OverlapActors.Add(Overlap.GetActor()); + + if (DitheringSettings.bDitherAttachedComponents) + { + Overlap.GetActor()->GetAttachedActors(OverlapActors, false, true); + } + + for (auto& Actor : OverlapActors) + { + // Ignore null actors or actors with IgnoreDitheringTag + if (!Actor || Actor->ActorHasTag(DitheringSettings.IgnoreDitheringTag)) + { + continue; + } + ActorsToDither.Add(Actor); + + // Start dithering actor if not done already + if (!DitherHelpers::DitherStatesContain(DitheredActorStates, Actor)) + { + int32 Index = DitherHelpers::FindInactiveDitherState(DitheredActorStates); + DitheredActorStates[Index].StartDithering(Actor, EDitherType::BlockingLOS); + } + } + } + } + + // Update dithered state of actors + for (auto& DitherState : DitheredActorStates) + { + if (!DitherState.IsValid()) + { + continue; + } + + // Set the opacity + DitherState.ComputeOpacity(DeltaTime, DitheringSettings.DitherInSpeed, DitheringSettings.DitherOutSpeed, DitheringSettings.MaterialDitherMinimum); + + ApplyDithering(DeltaTime, DitherState); + +#if ENABLE_DRAW_DEBUG + UGCDebugDithering(DitherState, DeltaTime, DitheringSettings.MaterialDitherMinimum); +#endif + + // If the dithered actor isn't overlapped + if (!ActorsToDither.Contains(DitherState.Actor)) + { + // Stop and do not dither in + DitherState.bIsDitheringIn = false; + DitherState.CollisionTime = 0.f; + DitherState.bIsDitheringOut = true; + + // If finished dithering out + if (DitherState.CurrentOpacity >= 1.f) + { + DitherState.Invalidate(); + } + } + // Otherwise if the dithered actor is still overlapped + else + { + // Do not dither out + DitherState.bIsDitheringOut = false; + if (DitherState.DitherType == EDitherType::OverlappingCamera || (DitherState.DitherType == EDitherType::BlockingLOS && DitherState.CollisionTime >= DitheringSettings.CollisionTimeThreshold)) + { + DitherState.bIsDitheringIn = true; + } + DitherState.CollisionTime += DeltaTime; + } + } + } + + return false; +} + +void UUGC_CameraDitheringModifier::ApplyDithering(float DeltaTime, FDitheredActorState& DitherState) +{ + if (!DitherState.IsValid()) + { + return; + } + + if (DitheringSettings.bUpdateMaterialPlayerPosition && CameraOwner && CameraOwner->PCOwner && CameraOwner->PCOwner->GetPawn() && DitheringMPC && GetWorld()) + { + if (!DitheringMPCInstance) + { + DitheringMPCInstance = GetWorld()->GetParameterCollectionInstance(DitheringMPC.Get()); + } + if (DitheringMPCInstance) + { + DitheringMPCInstance->SetVectorParameterValue(DitheringSettings.MaterialPlayerPositionParameterName, CameraOwner->PCOwner->GetPawn()->GetActorLocation()); + } + } + + // Get all mesh components + TArray Meshes; + DitherState.Actor->GetComponents(Meshes, DitheringSettings.bDitherChildActors); + + for (auto& Mesh : Meshes) + { + if (!Mesh) + { + continue; + } + + // Get all materials + const TArray Materials = Mesh->GetMaterials(); + for (auto& Material : Materials) + { + if (!Material) + { + continue; + } + + // Get all float parameters + TArray ScalarParams; + TArray ScalarParamIds; + Material->GetAllScalarParameterInfo(ScalarParams, ScalarParamIds); + + // Set the params that have the same name as MaterialOpacityParameterName + for (auto& ScalarParam : ScalarParams) + { + if (ScalarParam.Name != DitheringSettings.MaterialOpacityParameterName) + { + continue; + } + + Mesh->SetScalarParameterValueOnMaterials(DitheringSettings.MaterialOpacityParameterName, DitherState.CurrentOpacity); + } + } + } +} + +#if ENABLE_DRAW_DEBUG +void UUGC_CameraDitheringModifier::UGCDebugDithering(FDitheredActorState& DitherState, float DeltaTime, float DitherMin) +{ + if (bDebug && DitherState.IsValid() && GEngine) + { + const FString EaseInString = DitherState.bIsDitheringIn ? FString::Printf(TEXT(" - Dithering In")) : TEXT(""); + const FString EaseOutString = DitherState.bIsDitheringOut ? FString::Printf(TEXT(" - Dithering Out")) : TEXT(""); + const FString DebugText = FString::Printf(TEXT("UGC_CameraDitheringModifier: %s - Type: %s - Opacity: %f - Elapsed: %f%s%s") + , *GetNameSafe(DitherState.Actor), *UEnum::GetDisplayValueAsText(DitherState.DitherType).ToString(), DitherState.CurrentOpacity, DitherState.CollisionTime, *EaseInString, *EaseOutString); + + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Silver, DebugText); + } +} +#endif \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraModifier.cpp new file mode 100644 index 0000000..f9f24e4 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraModifier.cpp @@ -0,0 +1,250 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Modifiers/UGC_CameraModifier.h" + +#include "AuroraDevs_UGC.h" +#include "Camera/Modifiers/UGC_CameraAnimationModifier.h" +#include "Camera/PlayerCameraManager.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "GameFramework/Character.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/SpringArmComponent.h" + +void UUGC_CameraModifier::EnableModifier() +{ + Super::EnableModifier(); + OnModifierEnabled(CameraOwner->GetCameraCacheView()); +} + +void UUGC_CameraModifier::DisableModifier(bool bImmediate) +{ + Super::DisableModifier(bImmediate); + + if (bImmediate && bDisabled && !bPendingDisable) + { + Alpha = 0.f; + } + OnModifierDisabled(CameraOwner->GetCameraCacheView(), bImmediate); +} + +void UUGC_CameraModifier::OnModifierEnabled_Implementation(FMinimalViewInfo const& LastPOV) +{ +} + +void UUGC_CameraModifier::OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bImmediate) +{ +} + +void UUGC_CameraModifier::ProcessBoomLengthAndFOV_Implementation(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength) +{ + OutFOV = InFOV; + OutArmLength = InArmLength; +} + +void UUGC_CameraModifier::ProcessBoomOffsets_Implementation(float DeltaTime, FVector InSocketOffset, FVector InTargetOffset, FVector ViewLocation, FRotator ViewRotation, FVector& OutSocketOffset, FVector& OutTargetOffset) +{ + OutSocketOffset = InSocketOffset; + OutTargetOffset = InTargetOffset; +} + +void UUGC_CameraModifier::OnAnyLevelSequenceStarted_Implementation() +{ +} + +void UUGC_CameraModifier::OnAnyLevelSequenceEnded_Implementation() +{ +} + +void UUGC_CameraModifier::OnSetViewTarget_Implementation(bool bImmediate, bool bNewTargetIsOwner) +{ +} + +void UUGC_CameraModifier::PostUpdate_Implementation(float DeltaTime, FVector ViewLocation, FRotator ViewRotation) +{ +} + +bool UUGC_CameraModifier::IsDebugEnabled() const +{ + return bDebug; +} + +void UUGC_CameraModifier::ToggleDebug(bool const bEnabled) +{ + bDebug = bEnabled; +} + +bool UUGC_CameraModifier::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) +{ + return Super::ModifyCamera(DeltaTime, InOutPOV); +} + +void UUGC_CameraModifier::ModifyCamera(float DeltaTime, FVector ViewLocation, FRotator ViewRotation, float FOV, FVector& OutViewLocation, FRotator& OutViewRotation, float& OutFOV) +{ + Super::ModifyCamera(DeltaTime, ViewLocation, ViewRotation, FOV, OutViewLocation, OutViewRotation, OutFOV); + + OutViewLocation = ViewLocation; + OutViewRotation = ViewRotation; + OutFOV = FOV; + + UpdateOwnerReferences(); + + if (!UGCCameraManager) + { + return; + } + + if (!bPlayDuringCameraAnimations) + { + if (UGCCameraManager->IsPlayingAnyCameraAnimation()) + { + return; + } + } + + if (OwnerController && OwnerPawn) + { + UpdateInternalVariables(DeltaTime); + if (SpringArm) + { + ProcessBoomLengthAndFOV(DeltaTime, FOV, CurrentArmLength, ViewLocation, ViewRotation, OutFOV, SpringArm->TargetArmLength); + ProcessBoomOffsets(DeltaTime, CurrentSocketOffset, CurrentTargetOffset, ViewLocation, ViewRotation, SpringArm->SocketOffset, SpringArm->TargetOffset); + } + else + { + UGC_LOG_ONCE(InvalidSpringArm, Error, TEXT("%s uses UGC but doesn't have a valid Spring Arm Component."), *GetNameSafe(OwnerPawn)); + } + PostUpdate(DeltaTime, ViewLocation, ViewRotation); + } +} + +bool UUGC_CameraModifier::ProcessViewRotation(AActor* ViewTarget, float DeltaTime, FRotator& OutViewRotation, FRotator& OutDeltaRot) +{ + bool bResult = Super::ProcessViewRotation(ViewTarget, DeltaTime, OutViewRotation, OutDeltaRot); + + if (!bPlayDuringCameraAnimations) + { + if (UGCCameraManager && UGCCameraManager->IsPlayingAnyCameraAnimation()) + { + return bResult; + } + } + + if (ViewTarget && CameraOwner && CameraOwner->GetOwningPlayerController()) + { + // TO DO #GravityCompatibility + FVector const CameraLocation = CameraOwner->GetCameraLocation(); + FRotator const ControlRotation = CameraOwner->GetOwningPlayerController()->GetControlRotation(); + FRotator const OwnerRotation = ViewTarget ? ViewTarget->GetActorRotation() : FRotator::ZeroRotator; + FRotator InLocalControlRotation = ControlRotation - OwnerRotation; + InLocalControlRotation.Normalize(); + bResult = ProcessControlRotation(ViewTarget, DeltaTime, CameraLocation, OutViewRotation, InLocalControlRotation, OutDeltaRot, OutDeltaRot); + } + + return bResult; +} + +void UUGC_CameraModifier::UpdateOwnerReferences() +{ + UGCCameraManager = Cast(CameraOwner); + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + if (!UGCCameraManager) + { + return; + } + + OwnerController = UGCCameraManager->GetOwningPlayerController(); + OwnerCharacter = UGCCameraManager->OwnerCharacter; + OwnerPawn = UGCCameraManager->OwnerPawn; + if (OwnerPawn && OwnerPawn->IsLocallyControlled()) + { + SpringArm = UGCCameraManager->CameraArm; + MovementComponent = UGCCameraManager->MovementComponent; + } +} + +void UUGC_CameraModifier::UpdateInternalVariables(float DeltaTime) +{ + if (!UGCCameraManager) + { + return; + } + + bHasMovementInput = UGCCameraManager->bHasMovementInput; + PreviousMovementInput = MovementInput; + MovementInput = UGCCameraManager->MovementInput; + TimeSinceMovementInput = UGCCameraManager->TimeSinceMovementInput; + bHasRotationInput = UGCCameraManager->bHasRotationInput; + RotationInput = UGCCameraManager->RotationInput; + TimeSinceRotationInput = UGCCameraManager->TimeSinceRotationInput; + + if (SpringArm) + { + CurrentSocketOffset = SpringArm->SocketOffset; + CurrentTargetOffset = SpringArm->TargetOffset; + CurrentArmLength = SpringArm->TargetArmLength; + } +} + +bool UUGC_CameraModifier::ProcessTurnRate_Implementation(float DeltaTime, FRotator InLocalControlRotation, float InPitchTurnRate, float InYawTurnRate, float& OutPitchTurnRate, float& OutYawTurnRate) +{ + return false; +} + +bool UUGC_CameraModifier::ProcessControlRotation_Implementation(AActor* ViewTarget, float DeltaTime, FVector InViewLocation, FRotator InViewRotation, FRotator InLocalControlRotation, FRotator InDeltaRot, FRotator& OutDeltaRot) +{ + return false; +} + +FVector UUGC_CameraModifier::GetOwnerVelocity() const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->GetOwnerVelocity(); +} + +bool UUGC_CameraModifier::IsOwnerFalling() const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->IsOwnerFalling(); +} + +bool UUGC_CameraModifier::IsOwnerStrafing() const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->IsOwnerStrafing(); +} + +bool UUGC_CameraModifier::IsOwnerMovingOnGround() const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->IsOwnerMovingOnGround(); +} + +void UUGC_CameraModifier::ComputeOwnerFloorDistance(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, float& OutFloorDistance) const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->ComputeOwnerFloorDist(SweepDistance, CapsuleRadius, bOutFloorExists, OutFloorDistance); +} + +void UUGC_CameraModifier::ComputeOwnerFloorNormal(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, FVector& OutFloorNormal) const +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->ComputeOwnerFloorNormal(SweepDistance, CapsuleRadius, bOutFloorExists, OutFloorNormal); +} + +void UUGC_CameraModifier::ComputeOwnerSlopeAngle(float& OutSlopePitchDegrees, float& OutSlopeRollDegrees) +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->ComputeOwnerSlopeAngle(OutSlopePitchDegrees, OutSlopeRollDegrees); +} + +float UUGC_CameraModifier::ComputeOwnerLookAndMovementDot() +{ + ensureMsgf(UGCCameraManager, TEXT("Please use UGC Camera Modifiers only with a player camera manager inheriting from UGC_PlayerCameraManager.")); + return UGCCameraManager->ComputeOwnerLookAndMovementDot(); +} + +void UUGC_CameraAddOnModifier::SetSettings_Implementation(class UUGC_CameraAddOnModifierSettings* InSettings) +{ + Settings = InSettings; +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.cpp new file mode 100644 index 0000000..d6d107b --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.cpp @@ -0,0 +1,114 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.h" + +#include "Curves/CurveFloat.h" +#include "Engine/Engine.h" + +UUGC_FOVAnimNotifyCameraModifier::UUGC_FOVAnimNotifyCameraModifier() + : Super() +{ + bPlayDuringCameraAnimations = true; + RequestHelper.Init(TEXT("FOV"), [this]() -> bool { return bDebug; }); +} + +void UUGC_FOVAnimNotifyCameraModifier::ProcessBoomLengthAndFOV_Implementation(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength) +{ + Super::ProcessBoomLengthAndFOV_Implementation(DeltaTime, InFOV, InArmLength, ViewLocation, ViewRotation, OutFOV, OutArmLength); + RequestHelper.ProcessValue(DeltaTime, InFOV, ViewLocation, ViewRotation, OutFOV); +} + +void UUGC_FOVAnimNotifyCameraModifier::OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate) +{ + Super::OnModifierDisabled_Implementation(LastPOV, bWasImmediate); + RequestHelper.OnModifierDisabled(LastPOV, bWasImmediate); +} + +void UUGC_FOVAnimNotifyCameraModifier::PushFOVAnimNotifyRequest(FGuid RequestId, float TargetFOV, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve) +{ + RequestHelper.PushValueRequest(RequestId, TargetFOV, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); +} + +void UUGC_FOVAnimNotifyCameraModifier::PopFOVAnimNotifyRequest(FGuid RequestId) +{ + RequestHelper.PopValueRequest(RequestId); +} + +UUGC_ArmOffsetAnimNotifyCameraModifier::UUGC_ArmOffsetAnimNotifyCameraModifier() + : Super() +{ + bPlayDuringCameraAnimations = false; + SocketOffsetRequestHelper.Init(TEXT("SocketOffset"), [this]() -> bool { return bDebug; }); + TargetOffsetRequestHelper.Init(TEXT("TargetOffset"), [this]() -> bool { return bDebug; }); + +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + SocketOffsetRequestHelper.PropertyColor = FColor(252, 195, 53, 255); + TargetOffsetRequestHelper.PropertyColor = FColor(255, 212, 105, 255); +#endif +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::ProcessBoomOffsets_Implementation(float DeltaTime, FVector InSocketOffset, FVector InTargetOffset, FVector ViewLocation, FRotator ViewRotation, FVector& OutSocketOffset, FVector& OutTargetOffset) +{ + Super::ProcessBoomOffsets_Implementation(DeltaTime, InSocketOffset, InTargetOffset, ViewLocation, ViewRotation, OutSocketOffset, OutTargetOffset); + SocketOffsetRequestHelper.ProcessValue(DeltaTime, InSocketOffset, ViewLocation, ViewRotation, OutSocketOffset); + TargetOffsetRequestHelper.ProcessValue(DeltaTime, InTargetOffset, ViewLocation, ViewRotation, OutTargetOffset); +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate) +{ + Super::OnModifierDisabled_Implementation(LastPOV, bWasImmediate); + SocketOffsetRequestHelper.OnModifierDisabled(LastPOV, bWasImmediate); + TargetOffsetRequestHelper.OnModifierDisabled(LastPOV, bWasImmediate); +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::PushArmSocketOffsetAnimNotifyRequest(FGuid RequestId, FVector TargetOffset, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve) +{ + SocketOffsetRequestHelper.PushValueRequest(RequestId, TargetOffset, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::PopArmSocketOffsetAnimNotifyRequest(FGuid RequestId) +{ + SocketOffsetRequestHelper.PopValueRequest(RequestId); +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::PushArmTargetOffsetAnimNotifyRequest(FGuid RequestId, FVector TargetOffset, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve) +{ + TargetOffsetRequestHelper.PushValueRequest(RequestId, TargetOffset, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); +} + +void UUGC_ArmOffsetAnimNotifyCameraModifier::PopArmTargetOffsetAnimNotifyRequest(FGuid RequestId) +{ + TargetOffsetRequestHelper.PopValueRequest(RequestId); +} + +UUGC_ArmLengthAnimNotifyCameraModifier::UUGC_ArmLengthAnimNotifyCameraModifier() + : Super() +{ + bPlayDuringCameraAnimations = false; + RequestHelper.Init(TEXT("ArmLength"), [this]() -> bool { return bDebug; }); +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + RequestHelper.PropertyColor = FColor(55, 219, 33, 255); +#endif +} + +void UUGC_ArmLengthAnimNotifyCameraModifier::ProcessBoomLengthAndFOV_Implementation(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength) +{ + Super::ProcessBoomLengthAndFOV_Implementation(DeltaTime, InFOV, InArmLength, ViewLocation, ViewRotation, OutFOV, OutArmLength); + RequestHelper.ProcessValue(DeltaTime, InArmLength, ViewLocation, ViewRotation, OutArmLength); +} + +void UUGC_ArmLengthAnimNotifyCameraModifier::OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate) +{ + Super::OnModifierDisabled_Implementation(LastPOV, bWasImmediate); + RequestHelper.OnModifierDisabled(LastPOV, bWasImmediate); +} + +void UUGC_ArmLengthAnimNotifyCameraModifier::PushArmLengthAnimNotifyRequest(FGuid RequestId, float TargetLength, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve) +{ + RequestHelper.PushValueRequest(RequestId, TargetLength, TotalDuration, BlendInDuration, BlendInCurve, BlendOutDuration, BlendOutCurve); +} + +void UUGC_ArmLengthAnimNotifyCameraModifier::PopArmLengthAnimNotifyRequest(FGuid RequestId) +{ + RequestHelper.PopValueRequest(RequestId); +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.cpp new file mode 100644 index 0000000..4ae33f0 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.cpp @@ -0,0 +1,100 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h" +#include "Camera/PlayerCameraManager.h" + +namespace PlayCameraAnimCallbackProxyHelper +{ + FCameraAnimationHandle const UGCInvalid(MAX_int16, 0); +} + +UUGC_PlayCameraAnimCallbackProxy::UUGC_PlayCameraAnimCallbackProxy(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bInterruptedCalledBeforeBlendingOut(false) +{ +} + +FUGCCameraAnimationParams::operator FCameraAnimationParams() const +{ + FCameraAnimationParams Params; + Params.PlayRate = PlayRate; + Params.EaseInDuration = EaseInDuration; + Params.EaseOutDuration = EaseOutDuration; + Params.EaseInType = EaseInType; + Params.EaseOutType = EaseOutType; + Params.bLoop = false; + + return Params; +} + +UUGC_PlayCameraAnimCallbackProxy* UUGC_PlayCameraAnimCallbackProxy::CreateProxyObjectForPlayCameraAnim(APlayerCameraManager* InPlayerCameraManager, TSubclassOf ModifierClass, UCameraAnimationSequence* CameraSequence, FUGCCameraAnimationParams Params, FCameraAnimationHandle& Handle, bool bInterruptOthers, bool bDoCollisionChecks) +{ + UUGC_PlayCameraAnimCallbackProxy* Proxy = NewObject(); + Proxy->SetFlags(RF_StrongRefOnFrame); + Proxy->PlayCameraAnimation(InPlayerCameraManager, ModifierClass, CameraSequence, Params, Handle, bInterruptOthers, bDoCollisionChecks); + return Proxy; +} + +UUGC_PlayCameraAnimCallbackProxy* UUGC_PlayCameraAnimCallbackProxy::CreateProxyObjectForPlayCameraAnimForModifier(UUGC_CameraAnimationModifier* CameraAnimationModifier, UCameraAnimationSequence* CameraSequence, FUGCCameraAnimationParams Params, FCameraAnimationHandle& Handle, bool bInterruptOthers, bool bDoCollisionChecks) +{ + UUGC_PlayCameraAnimCallbackProxy* Proxy = NewObject(); + Proxy->SetFlags(RF_StrongRefOnFrame); + Proxy->PlayCameraAnimation(CameraAnimationModifier, CameraSequence, Params, Handle, bInterruptOthers, bDoCollisionChecks); + return Proxy; +} + +void UUGC_PlayCameraAnimCallbackProxy::PlayCameraAnimation(UUGC_CameraAnimationModifier* CameraAnimModifier, UCameraAnimationSequence* CameraSequence, FUGCCameraAnimationParams Params, FCameraAnimationHandle& Handle, bool bInterruptOthers, bool bDoCollisionChecks) +{ + Handle = PlayCameraAnimCallbackProxyHelper::UGCInvalid; + bool bPlayedSuccessfully = false; + + if (CameraAnimModifier) + { + Handle = CameraAnimModifier->PlaySingleCameraAnimation(CameraSequence, static_cast(Params), Params.ResetType, bInterruptOthers, bDoCollisionChecks); + bPlayedSuccessfully = Handle.IsValid(); + + if (bPlayedSuccessfully) + { + CameraAnimationModifierPtr = CameraAnimModifier; + + CameraAnimationEasingOutDelegate.BindUObject(this, &UUGC_PlayCameraAnimCallbackProxy::OnCameraAnimationEasingOut); + CameraAnimationModifierPtr->CameraAnimation_SetEasingOutDelegate(CameraAnimationEasingOutDelegate, Handle); + + CameraAnimationEndedDelegate.BindUObject(this, &UUGC_PlayCameraAnimCallbackProxy::OnCameraAnimationEnded); + CameraAnimationModifierPtr->CameraAnimation_SetEndedDelegate(CameraAnimationEndedDelegate, Handle); + } + } + + if (!bPlayedSuccessfully) + { + OnInterrupted.Broadcast(); + } +} + +void UUGC_PlayCameraAnimCallbackProxy::PlayCameraAnimation(APlayerCameraManager* InPlayerCameraManager, TSubclassOf ModifierClass, UCameraAnimationSequence* CameraSequence, FUGCCameraAnimationParams Params, FCameraAnimationHandle& Handle, bool bInterruptOthers, bool bDoCollisionChecks) +{ + if (InPlayerCameraManager) + { + if (UUGC_CameraAnimationModifier* CameraAnimModifier = Cast(InPlayerCameraManager->FindCameraModifierByClass(ModifierClass))) + { + PlayCameraAnimation(CameraAnimModifier, CameraSequence, Params, Handle, bInterruptOthers, bDoCollisionChecks); + } + } +} + +void UUGC_PlayCameraAnimCallbackProxy::OnCameraAnimationEasingOut(UCameraAnimationSequence* CameraAnimation) +{ + OnEaseOut.Broadcast(); +} + +void UUGC_PlayCameraAnimCallbackProxy::OnCameraAnimationEnded(UCameraAnimationSequence* CameraAnimation, bool bInterrupted) +{ + if (!bInterrupted) + { + OnCompleted.Broadcast(); + } + else if (!bInterruptedCalledBeforeBlendingOut) + { + OnInterrupted.Broadcast(); + } +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/UGC_PlayerCameraManager.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/UGC_PlayerCameraManager.cpp new file mode 100644 index 0000000..92490bb --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Camera/UGC_PlayerCameraManager.cpp @@ -0,0 +1,791 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Camera/UGC_PlayerCameraManager.h" + +#include "Camera/CameraComponent.h" +#include "Camera/Data/UGC_CameraData.h" +#include "Camera/Modifiers/UGC_CameraAnimationModifier.h" +#include "Camera/Modifiers/UGC_CameraModifier.h" +#include "Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h" +#include "CameraAnimationSequence.h" +#include "Components/CapsuleComponent.h" +#include "DrawDebugHelpers.h" +#include "Engine/Canvas.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/SpringArmComponent.h" +#include "Kismet/GameplayStatics.h" +#include "Kismet/KismetMathLibrary.h" +#include "LevelSequenceActor.h" +#include "LevelSequencePlayer.h" +#include "Pawn/UGC_PawnInterface.h" + +TAutoConsoleVariable GShowCameraManagerModifiersCVar( + TEXT("ShowCameraModifiersDebug"), + false, + TEXT("Show information about the currently active camera modifiers and their priorities.")); + +namespace PlayerCameraHelpers +{ + UCameraComponent* GetSpringArmChildCamera(USpringArmComponent* SpringArm) + { + UCameraComponent* Camera = nullptr; + if (SpringArm) + { + for (int32 i = 0; i < SpringArm->GetNumChildrenComponents(); ++i) + { + USceneComponent* Child = SpringArm->GetChildComponent(i); + + if (UCameraComponent* PotentialCamera = Cast(Child)) + { + Camera = PotentialCamera; + break; // found it + } + } + } + return Camera; + } +} + +AUGC_PlayerCameraManager::AUGC_PlayerCameraManager() +{ + PrimaryActorTick.bCanEverTick = true; +} + +void AUGC_PlayerCameraManager::InitializeFor(APlayerController* PC) +{ + Super::InitializeFor(PC); + RefreshLevelSequences(); // TO DO, this might not work for open worlds if level sequences are loaded in runtime. +} + +void AUGC_PlayerCameraManager::PrePossess(APawn* NewPawn, UUGC_CameraDataAssetBase* NewCameraDA, bool bBlendSpringArmProperties, bool bMatchCameraRotation) +{ + if (!NewPawn) + { + return; + } + + USpringArmComponent* NewSpringArm = NewPawn->FindComponentByClass(); + if (!NewSpringArm || !CameraArm) + { + return; + } + + if (bBlendSpringArmProperties) + { + const bool bSkipChecks = !NewCameraDA || !GetCurrentCameraDataAsset(); + if (bSkipChecks || ((NewCameraDA->ArmLengthSettings.MinArmLength != GetCurrentCameraDataAsset()->ArmLengthSettings.MinArmLength + || NewCameraDA->ArmLengthSettings.MaxArmLength != GetCurrentCameraDataAsset()->ArmLengthSettings.MaxArmLength) + && NewCameraDA->ArmLengthSettings.ArmRangeBlendTime > 0.f)) + { + NewSpringArm->TargetArmLength = CameraArm->TargetArmLength; + } + + if (bSkipChecks || (NewCameraDA->ArmOffsetSettings.ArmSocketOffset != GetCurrentCameraDataAsset()->ArmOffsetSettings.ArmSocketOffset + && NewCameraDA->ArmOffsetSettings.ArmSocketOffsetBlendTime > 0.f)) + { + NewSpringArm->SocketOffset = CameraArm->SocketOffset; + } + + if (bSkipChecks || (NewCameraDA->ArmOffsetSettings.ArmTargetOffset != GetCurrentCameraDataAsset()->ArmOffsetSettings.ArmTargetOffset + && NewCameraDA->ArmOffsetSettings.ArmTargetOffsetBlendTime > 0.f)) + { + NewSpringArm->TargetOffset = CameraArm->TargetOffset; + } + + if (bSkipChecks || ((NewCameraDA->FOVSettings.MinFOV != GetCurrentCameraDataAsset()->FOVSettings.MinFOV + || NewCameraDA->FOVSettings.MaxFOV != GetCurrentCameraDataAsset()->FOVSettings.MaxFOV) + && NewCameraDA->FOVSettings.FOVRangeBlendTime > 0.f)) + { + if (UCameraComponent* NewCamera = PlayerCameraHelpers::GetSpringArmChildCamera(NewSpringArm)) + { + NewCamera->SetFieldOfView(ViewTarget.POV.FOV); + } + } + } + else if (NewCameraDA) + { + const float PitchRatio = FMath::GetMappedRangeValueClamped( + FVector2D(ViewPitchMin, ViewPitchMax), FVector2D(-1.f, 1.f), + bMatchCameraRotation ? static_cast(ViewTarget.POV.Rotation.Pitch) : 0.f); + NewSpringArm->TargetArmLength = NewCameraDA->PitchToArmAndFOVCurveSettings.PitchToArmLengthCurve ? + FMath::GetMappedRangeValueClamped(FVector2D(-1.f, 1.f), FVector2D(NewCameraDA->ArmLengthSettings.MinArmLength, NewCameraDA->ArmLengthSettings.MaxArmLength), NewCameraDA->PitchToArmAndFOVCurveSettings.PitchToArmLengthCurve->GetFloatValue(PitchRatio)) + : NewCameraDA->ArmLengthSettings.MinArmLength; + NewSpringArm->SocketOffset = NewCameraDA->ArmOffsetSettings.ArmSocketOffset; + NewSpringArm->TargetOffset = NewCameraDA->ArmOffsetSettings.ArmTargetOffset; + if (UCameraComponent* NewCamera = PlayerCameraHelpers::GetSpringArmChildCamera(NewSpringArm)) + { + NewCamera->SetFieldOfView(NewCameraDA->PitchToArmAndFOVCurveSettings.PitchToFOVCurve ? + FMath::GetMappedRangeValueClamped(FVector2D(-1.f, 1.f), FVector2D(NewCameraDA->FOVSettings.MinFOV, NewCameraDA->FOVSettings.MaxFOV), NewCameraDA->PitchToArmAndFOVCurveSettings.PitchToFOVCurve->GetFloatValue(PitchRatio)) + : NewCameraDA->FOVSettings.MinFOV); + } + } + + if (bMatchCameraRotation) + { + PendingPossessPayload.bMatchCameraRotation = true; + PendingPossessPayload.PendingControlRotation = GetOwningPlayerController()->GetControlRotation(); + } + PendingPossessPayload.PendingCameraDA = NewCameraDA; + PendingPossessPayload.bBlendCameraProperties = bBlendSpringArmProperties; +} + +void AUGC_PlayerCameraManager::PostPossess(bool bReplaceCurrentCameraDA) +{ + if (PendingPossessPayload.PendingCameraDA) + { + UUGC_CameraDataAssetBase* CurrentHead = GetCurrentCameraDataAsset(); + PushCameraData_Internal(PendingPossessPayload.PendingCameraDA, PendingPossessPayload.bBlendCameraProperties); + if (bReplaceCurrentCameraDA && CurrentHead) + { + PopCameraData(CurrentHead); + } + } + if (PendingPossessPayload.bMatchCameraRotation) + { + GetOwningPlayerController()->SetControlRotation(PendingPossessPayload.PendingControlRotation); + } + PendingPossessPayload = AUGC_PlayerCameraManager::PossessPayload(); +} + +void AUGC_PlayerCameraManager::RefreshLevelSequences() +{ + // This resets the array and gets all actors of class. + QUICK_SCOPE_CYCLE_COUNTER(AUGC_PlayerCameraManager_RefreshLevelSequences); + LevelSequences.Reset(); + + for (TActorIterator It(GetWorld()); It; ++It) + { + ALevelSequenceActor* LevelSequence = *It; + LevelSequences.Add(LevelSequence); + + LevelSequence->GetSequencePlayer()->OnPlay.AddDynamic(this, &AUGC_PlayerCameraManager::OnLevelSequenceStarted); + LevelSequence->GetSequencePlayer()->OnPlayReverse.AddDynamic(this, &AUGC_PlayerCameraManager::OnLevelSequenceStarted); + LevelSequence->GetSequencePlayer()->OnStop.AddDynamic(this, &AUGC_PlayerCameraManager::OnLevelSequenceEnded); + LevelSequence->GetSequencePlayer()->OnPause.AddDynamic(this, &AUGC_PlayerCameraManager::OnLevelSequencePaused); + } +} + +void AUGC_PlayerCameraManager::OnLevelSequenceStarted() +{ + if (NbrActivePausedLevelSequences > 0) --NbrActivePausedLevelSequences; + ++NbrActiveLevelSequences; + DoForEachUGCModifier(&UUGC_CameraModifier::OnAnyLevelSequenceStarted); +} + +void AUGC_PlayerCameraManager::OnLevelSequencePaused() +{ + ++NbrActivePausedLevelSequences; + --NbrActiveLevelSequences; + ensure(NbrActiveLevelSequences >= 0); +} + +void AUGC_PlayerCameraManager::OnLevelSequenceEnded() +{ + --NbrActiveLevelSequences; + ensure(NbrActiveLevelSequences >= 0); + DoForEachUGCModifier(&UUGC_CameraModifier::OnAnyLevelSequenceEnded); +} + +void AUGC_PlayerCameraManager::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + UpdateInternalVariables(DeltaTime); +} + +bool AUGC_PlayerCameraManager::IsPlayingAnyCameraAnimation() const +{ + if (UUGC_CameraAnimationModifier const* CameraAnimModifier = FindCameraModifierOfType()) + { + return CameraAnimModifier && CameraAnimModifier->IsAnyCameraAnimationSequence(); + } + return false; +} + +void AUGC_PlayerCameraManager::PlayCameraAnimation(UCameraAnimationSequence* CameraSequence, FUGCCameraAnimationParams const& Params, bool bInterruptOthers, bool bDoCollisionChecks) +{ + if (UUGC_CameraAnimationModifier* CameraAnimModifier = FindCameraModifierOfType()) + { + CameraAnimModifier->PlaySingleCameraAnimation(CameraSequence, static_cast(Params), Params.ResetType, bInterruptOthers, bDoCollisionChecks); + } +} + +void AUGC_PlayerCameraManager::SetViewTarget(AActor* NewViewTarget, FViewTargetTransitionParams TransitionParams) +{ + auto OldPendingTarget = PendingViewTarget.Target; + auto OldTarget = ViewTarget.Target; + + Super::SetViewTarget(NewViewTarget, TransitionParams); + + if (!OwnerPawn) + { + return; + } + + bool const bAssignedNewTarget = ViewTarget.Target != OldTarget; + bool const bBlendingToNewTarget = PendingViewTarget.Target != OldPendingTarget; + if (bAssignedNewTarget || bBlendingToNewTarget) + { + bool const bWasImmediate = bAssignedNewTarget && !bBlendingToNewTarget; + bool bNewTargetIsOwner = false; + + // If old character has been unpossessed, then our new target is the owner! + // Or, if the view target is the controller, we are doing seamless travel which does not unpossess the pawn. + if (bWasImmediate && (OwnerPawn->GetController() == nullptr || OwnerPawn->GetController() == ViewTarget.Target)) + { + bNewTargetIsOwner = true; + } + else + { + bNewTargetIsOwner = bWasImmediate ? ViewTarget.Target == OwnerPawn : PendingViewTarget.Target == OwnerPawn; + } + + DoForEachUGCModifier([bNewTargetIsOwner, bWasImmediate](UUGC_CameraModifier* UGCModifier) + { + UGCModifier->OnSetViewTarget(bWasImmediate, bNewTargetIsOwner); + }); + } +} + +UCameraModifier* AUGC_PlayerCameraManager::FindCameraModifierOfClass(TSubclassOf ModifierClass, bool bIncludeInherited) +{ + for (UCameraModifier* Mod : ModifierList) + { + if (bIncludeInherited) + { + if (Mod->GetClass()->IsChildOf(ModifierClass)) + { + return Mod; + } + } + else + { + if (Mod->GetClass() == ModifierClass) + { + return Mod; + } + } + } + + return nullptr; +} + +UCameraModifier const* AUGC_PlayerCameraManager::FindCameraModifierOfClass(TSubclassOf ModifierClass, bool bIncludeInherited) const +{ + for (UCameraModifier* Mod : ModifierList) + { + if (bIncludeInherited) + { + if (Mod->GetClass()->IsChildOf(ModifierClass)) + { + return Mod; + } + } + else + { + if (Mod->GetClass() == ModifierClass) + { + return Mod; + } + } + } + + return nullptr; +} + +void AUGC_PlayerCameraManager::ToggleUGCCameraModifiers(bool const bEnabled, bool const bImmediate) +{ + DoForEachUGCModifier([bEnabled, bImmediate](UUGC_CameraModifier* UGCModifier) + { + if (bEnabled) + { + UGCModifier->EnableModifier(); + } + else + { + UGCModifier->DisableModifier(bImmediate); + } + }); +} + +void AUGC_PlayerCameraManager::ToggleCameraModifiers(bool const bEnabled, bool const bImmediate) +{ + for (int32 ModifierIdx = 0; ModifierIdx < ModifierList.Num(); ModifierIdx++) + { + if (ModifierList[ModifierIdx] != nullptr) + { + if (bEnabled) + { + ModifierList[ModifierIdx]->EnableModifier(); + } + else + { + ModifierList[ModifierIdx]->DisableModifier(bImmediate); + } + } + } +} + +void AUGC_PlayerCameraManager::ToggleAllUGCModifiersDebug(bool const bEnabled) +{ + DoForEachUGCModifier([bEnabled](UUGC_CameraModifier* UGCModifier) + { + if (!UGCModifier->IsDisabled()) + { + UGCModifier->bDebug = bEnabled; + } + }); +} + +void AUGC_PlayerCameraManager::ToggleAllModifiersDebug(bool const bEnabled) +{ + for (int32 ModifierIdx = 0; ModifierIdx < ModifierList.Num(); ModifierIdx++) + { + if (ModifierList[ModifierIdx] != nullptr && !ModifierList[ModifierIdx]->IsDisabled()) + { + ModifierList[ModifierIdx]->bDebug = bEnabled; + } + } +} + +void AUGC_PlayerCameraManager::PushCameraData_Internal(UUGC_CameraDataAssetBase* CameraDA, bool bBlendCameraProperties) +{ + CameraDataStack.Push(CameraDA); + OnCameraDataStackChanged(CameraDA, bBlendCameraProperties); +} + +void AUGC_PlayerCameraManager::PushCameraData(UUGC_CameraDataAssetBase* CameraDA) +{ + static constexpr bool bBlendCameraProperties = true; + PushCameraData_Internal(CameraDA, bBlendCameraProperties); +} + +void AUGC_PlayerCameraManager::PopCameraDataHead() +{ + CameraDataStack.Pop(); + OnCameraDataStackChanged(CameraDataStack.IsEmpty() ? nullptr : CameraDataStack[CameraDataStack.Num() - 1]); +} + +void AUGC_PlayerCameraManager::PopCameraData(UUGC_CameraDataAssetBase* CameraDA) +{ + if (CameraDataStack.IsEmpty()) + { + return; + } + + if (GetCurrentCameraDataAsset() == CameraDA) + { + PopCameraDataHead(); + } + + CameraDataStack.Remove(CameraDA); +} + +void AUGC_PlayerCameraManager::OnCameraDataStackChanged_Implementation(UUGC_CameraDataAssetBase* CameraDA, bool bBlendSpringArmProperties) +{ +} + +void AUGC_PlayerCameraManager::ProcessViewRotation(float DeltaTime, FRotator& OutViewRotation, FRotator& OutDeltaRot) +{ + Super::ProcessViewRotation(DeltaTime, OutViewRotation, OutDeltaRot); + if (PCOwner && ViewTarget.Target) + { + FRotator const ControlRotation = PCOwner->GetControlRotation(); + FRotator const OwnerRotation = ViewTarget.Target->GetActorRotation(); + FRotator InLocalControlRotation = ControlRotation - OwnerRotation; + InLocalControlRotation.Normalize(); + + float OutPitchTurnRate = PitchTurnRate; + float OutYawTurnRate = YawTurnRate; + + // TO DO #GravityCompatibility + ProcessTurnRate(DeltaTime, InLocalControlRotation, OutPitchTurnRate, OutYawTurnRate); + + PitchTurnRate = FMath::Clamp(OutPitchTurnRate, 0.f, 1.f); + YawTurnRate = FMath::Clamp(OutYawTurnRate, 0.f, 1.f); + } +} + +void AUGC_PlayerCameraManager::ProcessTurnRate(float DeltaTime, FRotator InLocalControlRotation, float& OutPitchTurnRate, float& OutYawTurnRate) +{ + DoForEachUGCModifierWithBreak([&](UUGC_CameraModifier* UGCModifier) -> bool + { + if (!UGCModifier->IsDisabled()) + { + if (!UGCModifier->CanPlayDuringCameraAnimation()) + { + if (IsPlayingAnyCameraAnimation()) + { + return false; + } + } + + // TO DO #GravityCompatibility + return UGCModifier->ProcessTurnRate(DeltaTime, InLocalControlRotation, PitchTurnRate, YawTurnRate, OutPitchTurnRate, OutYawTurnRate); + } + return false; // Don't break + }); +} + +void AUGC_PlayerCameraManager::UpdateInternalVariables_Implementation(float DeltaTime) +{ + AspectRatio = GetCameraCacheView().AspectRatio; + HorizontalFOV = GetFOVAngle(); + ensureAlways(!FMath::IsNearlyZero(AspectRatio)); + VerticalFOV = FMath::RadiansToDegrees(2.f * FMath::Atan(FMath::Tan(FMath::DegreesToRadians(HorizontalFOV) * 0.5f) / AspectRatio)); + + if (PCOwner && PCOwner->GetPawn()) + { + APawn* NewOwnerPawn = PCOwner->GetPawn(); + if (!OwnerPawn || NewOwnerPawn != OwnerPawn) + { + // Either initialising reference, or we have possessed a new character. + OwnerPawn = NewOwnerPawn; + if (OwnerCharacter = PCOwner->GetPawn(), + OwnerCharacter != nullptr) + { + MovementComponent = OwnerCharacter->GetCharacterMovement(); + } + CameraArm = OwnerPawn->FindComponentByClass(); + OriginalArmLength = CameraArm ? CameraArm->TargetArmLength : 0.f; + + } + + if (OwnerPawn) + { + MovementInput = GetMovementControlInput(); + bHasMovementInput = !MovementInput.IsZero(); + TimeSinceMovementInput = bHasMovementInput ? 0.f : TimeSinceMovementInput + DeltaTime; + + RotationInput = GetRotationInput(); + bHasRotationInput = !RotationInput.IsZero(); + TimeSinceRotationInput = bHasRotationInput ? 0.f : TimeSinceRotationInput + DeltaTime; + } + } +} + +FRotator AUGC_PlayerCameraManager::GetRotationInput_Implementation() const +{ + FRotator RotInput = FRotator::ZeroRotator; + + if (OwnerPawn && OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnInterface::StaticClass())) + { + RotInput = IUGC_PawnInterface::Execute_GetRotationInput(OwnerPawn); + } + return RotInput; +} + +FVector AUGC_PlayerCameraManager::GetMovementControlInput_Implementation() const +{ + FVector MovInput = FVector::ZeroVector; + + if (OwnerPawn && OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnInterface::StaticClass())) + { + MovInput = IUGC_PawnInterface::Execute_GetMovementInput(OwnerPawn); + } + return MovInput; +} + +// Limit the view yaw in local space instead of world space. +void AUGC_PlayerCameraManager::LimitViewYaw(FRotator& ViewRotation, float InViewYawMin, float InViewYawMax) +{ + // TO DO #GravityCompatibility + if (PCOwner && PCOwner->GetPawn()) + { + FRotator ActorRotation = PCOwner->GetPawn()->GetActorRotation(); + ViewRotation.Yaw = FMath::ClampAngle(ViewRotation.Yaw, ActorRotation.Yaw + InViewYawMin, ActorRotation.Yaw + InViewYawMax); + ViewRotation.Yaw = FRotator::ClampAxis(ViewRotation.Yaw); + } +} + +void AUGC_PlayerCameraManager::DrawRealDebugCamera(float Duration, FLinearColor CameraColor, float Thickness) const +{ +#if ENABLE_DRAW_DEBUG + ::DrawDebugCamera(GetWorld(), ViewTarget.POV.Location, ViewTarget.POV.Rotation, ViewTarget.POV.FOV, 1.0f, CameraColor.ToFColor(true), false, Duration); +#endif +} + +/** Draw a debug camera shape. */ +void AUGC_PlayerCameraManager::DrawGameDebugCamera(float Duration, bool bDrawCamera, FLinearColor CameraColor, bool bDrawSpringArm, FLinearColor SpringArmColor, float Thickness) const +{ +#if ENABLE_DRAW_DEBUG + + if (bDrawCamera && CameraArm) + { + int32 const NbrComponents = CameraArm->GetNumChildrenComponents(); + for (int32 i = 0; i < NbrComponents; ++i) + { + if (USceneComponent* ChildComp = CameraArm->GetChildComponent(i)) + { + if (UCameraComponent* CameraComp = Cast(ChildComp)) + { + ::DrawDebugCamera(GetWorld(), CameraComp->GetComponentLocation(), CameraComp->GetComponentRotation(), ViewTarget.POV.FOV, 1.0f, CameraColor.ToFColor(true), false, Duration); + + if (bDrawSpringArm) + { + DrawDebugSpringArm(CameraComp->GetComponentLocation(), Duration, SpringArmColor, Thickness); + } + break; + + } + + } + } + } +#endif +} + +void AUGC_PlayerCameraManager::DrawDebugSpringArm(FVector const& CameraLocation, float Duration, FLinearColor SpringArmColor, float Thickness) const +{ +#if ENABLE_DRAW_DEBUG + if (CameraArm) + { + FVector const SafeLocation = CameraArm->GetComponentLocation() + CameraArm->TargetOffset; + ::DrawDebugLine(GetWorld(), CameraLocation, SafeLocation, SpringArmColor.ToFColor(true), false, Duration, 0, Thickness); + } +#endif +} + +void AUGC_PlayerCameraManager::DoForEachUGCModifier(TFunction const& Function) +{ + if (Function) + { + for (int32 ModifierIdx = 0; ModifierIdx < UGCModifiersList.Num(); ++ModifierIdx) + { + ensure(UGCModifiersList[ModifierIdx]); + + if (UGCModifiersList[ModifierIdx]) + { + Function(UGCModifiersList[ModifierIdx]); + } + } + } +} + +void AUGC_PlayerCameraManager::DoForEachUGCModifierWithBreak(TFunction const& Function) +{ + if (Function) + { + for (int32 ModifierIdx = 0; ModifierIdx < UGCModifiersList.Num(); ++ModifierIdx) + { + ensure(UGCModifiersList[ModifierIdx]); + + if (UGCModifiersList[ModifierIdx]) + { + if (Function(UGCModifiersList[ModifierIdx])) + { + break; + } + } + } + } +} + +void AUGC_PlayerCameraManager::DisplayDebug(class UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) +{ + Super::DisplayDebug(Canvas, DebugDisplay, YL, YPos); + const bool bShowModifierList = GShowCameraManagerModifiersCVar.GetValueOnGameThread(); + if (bShowModifierList) + { + for (int32 ModifierIdx = 0; ModifierIdx < ModifierList.Num(); ++ModifierIdx) + { + if (ModifierList[ModifierIdx] != nullptr) + { + Canvas->SetDrawColor(FColor::White); + FString DebugString = FString::Printf(TEXT("UGC Modifier %d: %s - Priority %d"), ModifierIdx, *ModifierList[ModifierIdx]->GetName(), ModifierList[ModifierIdx]->Priority); + Canvas->DrawText(GEngine->GetSmallFont(), DebugString, YL, YPos); + YPos += YL; + } + } + } +} + +UCameraModifier* AUGC_PlayerCameraManager::AddNewCameraModifier(TSubclassOf ModifierClass) +{ + UCameraModifier* AddedModifier = Super::AddNewCameraModifier(ModifierClass); + if (AddedModifier) + { + if (UUGC_CameraModifier* UGCModifier = Cast(AddedModifier)) + { + UGCModifiersList.Add(UGCModifier); + if (UUGC_CameraAddOnModifier* UGCAddOnModifier = Cast(AddedModifier)) + { + UGCAddOnModifiersList.Add(UGCAddOnModifier); + } + } + } + return AddedModifier; +} + +bool AUGC_PlayerCameraManager::RemoveCameraModifier(UCameraModifier* ModifierToRemove) +{ + if (ModifierToRemove) + { + if (UUGC_CameraModifier* UGCModifierToRemove = Cast(ModifierToRemove)) + { + // Loop through each modifier in camera + for (int32 ModifierIdx = 0; ModifierIdx < UGCModifiersList.Num(); ++ModifierIdx) + { + // If we found ourselves, remove ourselves from the list and return + if (UGCModifiersList[ModifierIdx] == UGCModifierToRemove) + { + UGCModifiersList.RemoveAt(ModifierIdx, 1); + break; + } + } + + if (UUGC_CameraAddOnModifier* UGCAddOnModifier = Cast(ModifierToRemove)) + { + // Loop through each modifier in camera + for (int32 ModifierIdx = 0; ModifierIdx < UGCAddOnModifiersList.Num(); ++ModifierIdx) + { + // If we found ourselves, remove ourselves from the list and return + if (UGCAddOnModifiersList[ModifierIdx] == UGCModifierToRemove) + { + UGCAddOnModifiersList.RemoveAt(ModifierIdx, 1); + break; + } + } + } + } + } + return Super::RemoveCameraModifier(ModifierToRemove); +} + +FVector AUGC_PlayerCameraManager::GetOwnerVelocity() const +{ + FVector Velocity = FVector::ZeroVector; + if (MovementComponent) + { + Velocity = MovementComponent->Velocity; + } + else if (OwnerPawn && OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnMovementInterface::StaticClass())) + { + Velocity = IUGC_PawnMovementInterface::Execute_GetOwnerVelocity(OwnerPawn); + } + return Velocity; +} + +bool AUGC_PlayerCameraManager::IsOwnerFalling() const +{ + bool bIsFalling = false; + if (MovementComponent) + { + bIsFalling = MovementComponent->IsFalling(); + } + else if (OwnerPawn && OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnMovementInterface::StaticClass())) + { + bIsFalling = IUGC_PawnMovementInterface::Execute_IsOwnerFalling(OwnerPawn); + } + return bIsFalling; +} + +bool AUGC_PlayerCameraManager::IsOwnerStrafing() const +{ + bool bIsStrafing = false; + if (MovementComponent && OwnerPawn) + { + bIsStrafing = OwnerPawn->bUseControllerRotationYaw || (MovementComponent->bUseControllerDesiredRotation && !MovementComponent->bOrientRotationToMovement); + } + else if (OwnerPawn) + { + if (OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnMovementInterface::StaticClass())) + { + bIsStrafing = IUGC_PawnMovementInterface::Execute_IsOwnerStrafing(OwnerPawn); + } + else + { + bIsStrafing = OwnerPawn->bUseControllerRotationYaw; + } + } + return bIsStrafing; +} + +bool AUGC_PlayerCameraManager::IsOwnerMovingOnGround() const +{ + bool bIsMovingOnGround = false; + if (MovementComponent) + { + bIsMovingOnGround = MovementComponent->IsMovingOnGround(); + } + else if (OwnerPawn && OwnerPawn->GetClass()->ImplementsInterface(UUGC_PawnMovementInterface::StaticClass())) + { + bIsMovingOnGround = IUGC_PawnMovementInterface::Execute_IsOwnerMovingOnGround(OwnerPawn); + } + return bIsMovingOnGround; +} + +void AUGC_PlayerCameraManager::ComputeOwnerFloorDist(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, float& OutFloorDistance) const +{ + if (MovementComponent && OwnerCharacter) + { + CapsuleRadius = FMath::Max(CapsuleRadius, OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleRadius()); + FFindFloorResult OutFloorResult; + MovementComponent->ComputeFloorDist(OwnerPawn->GetActorLocation(), SweepDistance, SweepDistance, OutFloorResult, CapsuleRadius); + bOutFloorExists = OutFloorResult.bBlockingHit; + OutFloorDistance = bOutFloorExists ? OutFloorResult.FloorDist : 0.f; + } + else + { + FHitResult OutHit; + bOutFloorExists = GetWorld()->SweepSingleByChannel(OutHit + , OwnerPawn->GetActorLocation() + , OwnerPawn->GetActorLocation() - SweepDistance * FVector::UpVector + , FQuat::Identity + , ECollisionChannel::ECC_Visibility + , FCollisionShape::MakeSphere(CapsuleRadius)); + + OutFloorDistance = bOutFloorExists ? OutHit.Distance : 0.f; + } +} + +void AUGC_PlayerCameraManager::ComputeOwnerFloorNormal(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, FVector& OutFloorNormal) const +{ + if (MovementComponent && OwnerCharacter) + { + bOutFloorExists = MovementComponent->CurrentFloor.IsWalkableFloor(); + OutFloorNormal = MovementComponent->CurrentFloor.HitResult.ImpactNormal; + } + else + { + FHitResult OutHit; + bOutFloorExists = GetWorld()->SweepSingleByChannel(OutHit + , OwnerPawn->GetActorLocation() + , OwnerPawn->GetActorLocation() - SweepDistance * FVector::UpVector + , FQuat::Identity + , ECollisionChannel::ECC_Visibility + , FCollisionShape::MakeSphere(CapsuleRadius)); + + bOutFloorExists = OutHit.bBlockingHit; + OutFloorNormal = bOutFloorExists ? OutHit.ImpactNormal : FVector::ZeroVector; + } +} + +void AUGC_PlayerCameraManager::ComputeOwnerSlopeAngle(float& OutSlopePitchDegrees, float& OutSlopeRollDegrees) +{ + bool bOutFloorExists = false; + FVector OutFloorNormal = FVector::ZeroVector; + ComputeOwnerFloorNormal(96.f, 64.f, bOutFloorExists, OutFloorNormal); + UKismetMathLibrary::GetSlopeDegreeAngles(OwnerPawn->GetActorRightVector(), OutFloorNormal, OwnerPawn->GetActorUpVector(), OutSlopePitchDegrees, OutSlopeRollDegrees); +} + +float AUGC_PlayerCameraManager::ComputeOwnerLookAndMovementDot() +{ + if (IsOwnerStrafing()) + { + return 1.f; + } + + FVector const Velocity = GetOwnerVelocity(); + if (Velocity.IsNearlyZero()) + { + return 0.f; + } + + float const Dot = Velocity | OwnerPawn->GetControlRotation().Vector(); + return Dot; +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_CameraTurnRateModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_CameraTurnRateModifier.cpp new file mode 100644 index 0000000..b3a3c62 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_CameraTurnRateModifier.cpp @@ -0,0 +1,49 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "Input/UGC_CameraTurnRateModifier.h" +#include "EnhancedInputSubsystems.h" +#include "Camera/UGC_PlayerCameraManager.h" +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" +#include "GameFramework/PlayerController.h" + +FInputActionValue UUGC_CameraTurnRateModifier::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime) +{ + EInputActionValueType ValueType = CurrentValue.GetValueType(); + if (ValueType == EInputActionValueType::Boolean) + { + return CurrentValue; + } + + if (!PlayerCameraManager) + { + if (!PlayerInput->GetOuterAPlayerController() || !PlayerInput->GetOuterAPlayerController()->PlayerCameraManager) + { +#if ENABLE_DRAW_DEBUG + // Debugging + if (GEngine) + { + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Red, FString::Printf(TEXT("UGC_CameraSlowDownInputModifier: Could not find Player Camera Manager to use Camera Slow Down constraint."))); + } +#endif + return CurrentValue; + } + + AUGC_PlayerCameraManager* PCManager = Cast(PlayerInput->GetOuterAPlayerController()->PlayerCameraManager); + if (!PCManager) + { +#if ENABLE_DRAW_DEBUG + // Debugging + if (GEngine) + { + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Red, FString::Printf(TEXT("UGC_CameraSlowDownInputModifier: Player's Camera Manager does not inherit from UGC_PlayerCameraManager. Angle Constraint cannot be used."))); + } +#endif + return CurrentValue; + } + // Here we know that it's not null + PlayerCameraManager = PCManager; + } + + return CurrentValue.Get() * PlayerCameraManager->GetCameraTurnRate(); +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_InputAccelerationModifier.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_InputAccelerationModifier.cpp new file mode 100644 index 0000000..3f5d606 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Input/UGC_InputAccelerationModifier.cpp @@ -0,0 +1,48 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + + +#include "Input/UGC_InputAccelerationModifier.h" + +#include "Curves/CurveFloat.h" +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" + +FInputActionValue UUGC_InputAccelerationModifier::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime) +{ + EInputActionValueType ValueType = CurrentValue.GetValueType(); + if (ValueType == EInputActionValueType::Boolean) + { + return CurrentValue; + } + + if (CurrentValue.Get() == FVector::ZeroVector || !AccelerationCurve) + { + Timer = 0.f; + return CurrentValue; + } + +#if ENABLE_DRAW_DEBUG + // Debugging + if (GEngine) + { + bool bValidCurve = true; + FVector2D TimeRange; + AccelerationCurve->GetTimeRange(TimeRange.X, TimeRange.Y); + + if (TimeRange.X != 0.f || TimeRange.Y != 1.f) + { + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, FColor::Red, FString::Printf(TEXT("UGC_InputAccelerationModifier: Given Curve's time range (X Axis) is not normalized i.e., between 0 and 1! Found: %s"), *TimeRange.ToString())); + bValidCurve = false; + } + + if (!bValidCurve) + { + return CurrentValue; + } + } +#endif + + Timer += DeltaTime; + float const ClampedTime = FMath::Clamp(AccelerationTime > 0.f ? Timer / AccelerationTime : 0.f, 0.f, 1.f); + return CurrentValue.Get() * AccelerationCurve->GetFloatValue(ClampedTime); +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Private/Pawn/UGC_PawnInterface.cpp b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Pawn/UGC_PawnInterface.cpp new file mode 100644 index 0000000..d7d1aa2 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Private/Pawn/UGC_PawnInterface.cpp @@ -0,0 +1,6 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + + +#include "Pawn/UGC_PawnInterface.h" + +// Add default functionality here for any IUGC_PawnInterface functions that are not pure virtual. diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.h new file mode 100644 index 0000000..59ba729 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/AnimNotifyState/UGC_CameraPropertiesRequestAnimNotifyState.h @@ -0,0 +1,168 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimNotifies/AnimNotifyState.h" +#include "Curves/CurveFloat.h" +#include "Misc/Guid.h" +#include "UGC_CameraPropertiesRequestAnimNotifyState.generated.h" + +/** + * Anim Notify State to send requests to the FOVAnimNotifyModifier to change the FOV during animations. + */ +UCLASS() +class AURORADEVS_UGC_API UUGC_FOVRequestAnimNotifyState : public UAnimNotifyState +{ + GENERATED_BODY() + +protected: + virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override; + virtual FString GetNotifyName_Implementation() const override; + virtual FLinearColor GetEditorColor() override; + +protected: + /** Horizontal FOV to set. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float TargetFOV = 90.f; + + /** Time needed for the FOV value to ease into the TargetValue. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float BlendInDuration = 0.5f; + + /** Controls the blending in. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + TObjectPtr BlendInCurve = nullptr; + + /** Time needed for the FOV value to ease out from the TargetValue into the normal gameplay value. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float BlendOutDuration = 0.5f; + + /** Controls the blending Out. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + TObjectPtr BlendOutCurve = nullptr; + +#if WITH_EDITORONLY_DATA + /** Whether the name of the notify should include the request Id. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + bool bShowRequestIdInName = false; +#endif + +protected: + FGuid RequestId = FGuid::NewGuid(); +}; + +/** + * Anim Notify State to send requests to the ArmOffsetAnimNotifyModifier to change the Arm Offsets during animations. + */ +UCLASS() +class AURORADEVS_UGC_API UUGC_ArmOffsetRequestAnimNotifyState : public UAnimNotifyState +{ + GENERATED_BODY() + +protected: + virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override; + virtual FString GetNotifyName_Implementation() const override; + virtual FLinearColor GetEditorColor() override; + +protected: + /** Whether to change socket offset. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset") + bool bModifySocketOffset = false; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset", meta = (EditCondition="bModifySocketOffset")) + FVector TargetSocketOffset = FVector::Zero(); + + /** Time needed for the Socket Offset value to ease into the TargetValue. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset", meta = (EditCondition = "bModifySocketOffset")) + float SocketOffsetBlendInDuration = 0.5f; + + /** Controls the blending in. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset", meta = (EditCondition = "bModifySocketOffset")) + TObjectPtr SocketOffsetBlendInCurve = nullptr; + + /** Time needed for the Socket Offset value to ease out from the TargetValue into the normal gameplay value. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset", meta = (EditCondition = "bModifySocketOffset")) + float SocketOffsetBlendOutDuration = 0.5f; + + /** Controls the blending Out. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|SocketOffset", meta = (EditCondition = "bModifySocketOffset")) + TObjectPtr SocketOffsetBlendOutCurve = nullptr; + + /** Whether to change socket offset. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset") + bool bModifyTargetOffset = false; + + /** Target Offset to set. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset", meta = (EditCondition = "bModifyTargetOffset")) + FVector TargetTargetOffset = FVector::Zero(); + + /** Time needed for the Target Offset value to ease into the TargetValue. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset", meta = (EditCondition = "bModifyTargetOffset")) + float TargetOffsetBlendInDuration = 0.5f; + + /** Controls the blending in. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset", meta = (EditCondition = "bModifyTargetOffset")) + TObjectPtr TargetOffsetBlendInCurve = nullptr; + + /** Time needed for the Target Offset value to ease out from the TargetValue into the normal gameplay value. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset", meta = (EditCondition = "bModifyTargetOffset")) + float TargetOffsetBlendOutDuration = 0.5f; + + /** Controls the blending Out. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|TargetOffset", meta = (EditCondition = "bModifyTargetOffset")) + TObjectPtr TargetOffsetBlendOutCurve = nullptr; + +#if WITH_EDITORONLY_DATA + /** Whether the name of the notify should include the request Id. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + bool bShowRequestIdInName = false; +#endif + +protected: + FGuid RequestId = FGuid::NewGuid(); +}; + +/** + * Anim Notify State to send requests to the ArmLengthAnimNotifyModifier to change the Arm Length during animations. + */ +UCLASS() +class AURORADEVS_UGC_API UUGC_ArmLengthRequestAnimNotifyState : public UAnimNotifyState +{ + GENERATED_BODY() + +protected: + virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override; + virtual FString GetNotifyName_Implementation() const override; + virtual FLinearColor GetEditorColor() override; + +protected: + /** ArmLength to set. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float TargetArmLength = 0.f; + + /** Time needed for the ArmLength value to ease into the TargetValue. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float BlendInDuration = 0.5f; + + /** Controls the blending in. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + TObjectPtr BlendInCurve = nullptr; + + /** Time needed for the ArmLength value to ease out from the TargetValue into the normal gameplay value. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + float BlendOutDuration = 0.5f; + + /** Controls the blending Out. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermit if unsure. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + TObjectPtr BlendOutCurve = nullptr; + +#if WITH_EDITORONLY_DATA + /** Whether the name of the notify should include the request Id. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings") + bool bShowRequestIdInName = false; +#endif + +protected: + FGuid RequestId = FGuid::NewGuid(); +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/AuroraDevs_UGC.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/AuroraDevs_UGC.h new file mode 100644 index 0000000..d98dd1f --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/AuroraDevs_UGC.h @@ -0,0 +1,30 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +#ifndef AURORA_DEVS_UGC +#define AURORA_DEVS_UGC +#endif + +DECLARE_LOG_CATEGORY_EXTERN(AuroraUGC, Log, All); +#define UGC_LOG(Verbosity, Format, ...) UE_LOG(AuroraUGC, Verbosity, Format, ##__VA_ARGS__) +#define UGC_LOG_ONCE(LogId, Verbosity, Format, ...)\ +do\ +{\ + static bool bLogged##LogId##Already = false; \ + if (!bLogged##LogId##Already)\ + {\ + UE_LOG(AuroraUGC, Verbosity, Format, ##__VA_ARGS__); \ + bLogged##LogId##Already = true; \ + } \ +} while (0) + +class FAuroraDevs_UGCModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Components/UGC_SpringArmComponentBase.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Components/UGC_SpringArmComponentBase.h new file mode 100644 index 0000000..274ee58 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Components/UGC_SpringArmComponentBase.h @@ -0,0 +1,53 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/SpringArmComponent.h" +#include "Camera/Data/UGC_CameraData.h" +#include "Misc/CoreMiscDefines.h" +#include "UGC_SpringArmComponentBase.generated.h" + +/** + * Custom SpringArm component with enhanced collision handling. + */ +UCLASS(Blueprintable, Abstract, ClassGroup = "UGC Camera", Category = "UGC|Components", meta = (BlueprintSpawnableComponent)) +class AURORADEVS_UGC_API UUGC_SpringArmComponentBase : public USpringArmComponent +{ + GENERATED_BODY() + +protected: + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + virtual void UpdateDesiredArmLocation(bool bDoTrace, bool bDoLocationLag, bool bDoRotationLag, float DeltaTime) override; + virtual FVector BlendLocations(const FVector& DesiredArmLocation, const FVector& TraceHitLocation, bool bHitSomething, float DeltaTime) override; + bool IsPlayerControlled() const; + +protected: + /** Camera collision settings including feelers */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = CameraCollision, meta = (EditCondition = "bDoCollisionTest")) + FCameraCollisionSettings CameraCollisionSettings; + + /* *EXPERIMENTAL* Might cause bugs. + * Whether we want the framing to stay the same during collisions. This is useful for games where you need to aim (bow, gun; etc.) since it + * allows the center of the screen to not shift during collision. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = CameraCollision) + bool bMaintainFramingDuringCollisions = false; + + /* Whether to draw debug messages regarding the spring arm collision.*/ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CameraCollision") + bool bPrintCollisionDebug = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CameraCollision", meta = (EditCondition = "bPrintCollisionDebug")) + bool bPrintHitActors = false; + +protected: + /** Runtime interpolated distance percentage (0 = fully blocked, 1 = clear) */ + float DistBlockedPct = 1.f; + + // Debug-only: Track which actors were hit by feeler rays (not needed in shipping) +#if WITH_EDITORONLY_DATA + UPROPERTY(VisibleInstanceOnly, Transient, Category = "CameraCollision|Debug") + TArray HitActors; +#endif +}; diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Data/UGC_CameraData.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Data/UGC_CameraData.h new file mode 100644 index 0000000..06adf8c --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Data/UGC_CameraData.h @@ -0,0 +1,639 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Curves/CurveFloat.h" +#include "Engine/DataAsset.h" +#include "Engine/EngineTypes.h" +#include "UGC_CameraData.generated.h" + +/* + * The settings of the camera yaw follow. This is useful for player who don't like to control the camera too much. + * The YawFollowModifier in blueprint will adjust the yaw to face the movement direction (Used by AAA games like Hogwarts Legacy, Witcher, etc). + */ +USTRUCT(BlueprintType) +struct FCameraYawFollowSettings +{ + GENERATED_BODY() + + // Whether the yaw should follow the character, when they are moving. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Yaw Follow") + bool bEnableYawMovementFollow = true; + + // Whether the yaw threshold timer should be reset when the character stop moving. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Yaw Follow") + bool bResetThresholdTimerWhenNoMovement = false; + + // The speed at which the camera rotates its yaw in the movement direction of the character. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Yaw Follow") + float YawFollowSpeed = 50.f; + + /* The minimum time the player shouldn't rotate the camera manually before the yaw follow kicks in. + * The timer can be reset when the character stops moving by enabling `bResetThresholdTimerWhenNoMovement`. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Yaw Follow") + float YawFollowTimeThreshold = 2.f; + + // Threshold yaw angle above which we trigger the yaw follow modifier. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Yaw Follow") + float YawFollowAngleThreshold = 10.f; +}; + +/* + * The settings of the camera pitch follow. This is useful for player who don't like to control the camera too much. + * The PitchFollowModifier in blueprint will adjust the pitch to face slopes, falling and reset the pitch if it's left untouched for long enough. + * (Used by AAA games like Red Dead Redemption, Hogwarts Legacy, Witcher, etc) + */ +USTRUCT(BlueprintType) +struct FCameraPitchFollowSettings +{ + GENERATED_BODY() + + /** Whether the pitch should reset to the resting pitch when the character is moving. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Pitch Follow") + bool bEnablePitchMovementFollow = true; + + /** The speed at which the camera rotates its pitch to follow when falling/moving. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Pitch Follow") + float PitchFollowSpeed = 10.f; + + /** The minimum time the player shouldn't rotate the camera manually before the pitch follow kicks in. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Pitch Follow") + float PitchFollowTimeThreshold = 2.f; + + /** Whether the pitch threshold timer should be reset when the character stop moving. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Pitch Follow") + bool bResetThresholdTimerWhenNoMovement = false; + + /** Threshold pitch angle above which we trigger the pitch follow modifier to reset the pitch to RestingPitch. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement Follow") + float PitchFollowAngleThreshold = 5.f; + + /** The pitch to go to when the the character is moving without controlling the camera's pitch is messed up. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement Follow") + float RestingPitch = -10.f; + + /** Should the camera look down when the character is falling for at least TimeThresholdWhenFalling in seconds and the floor is at least MinDistanceFromGround centimeters away ? */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Falling") + bool bTriggerWhenFalling = true; + + /** The minimun duration of time the character should fall before the modifier is triggered. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Falling", meta = (EditCondition = "bTriggerWhenFalling")) + float PitchFollowTimeThresholdWhenFalling = 0.5f; + + /** The minimum distance we should be above the ground to trigger the pitch follow when falling. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Falling", meta = (EditCondition = "bTriggerWhenFalling")) + float MinDistanceFromGroundToTriggerWhenFalling = 500.f; + + /** A multiplier to apply to the follow speed of the camera when we're feeling. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Falling", meta = (EditCondition = "bTriggerWhenFalling")) + float SpeedMultiplierWhenFalling = 6.f; + + /** Should the camera pitch look toward the inclination of the slope the character is walking on? */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Slopes") + bool bTriggerOnSlopes = true; + + /** The minimum slope pitch inclination angle so that the pitch follow modifier is triggered (so that it's not triggered for small bumps). */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Slopes", meta = (EditCondition = "bTriggerOnSlopes")) + float SlopeMinIncline = 25.f; + + /** The weight of the pitch of the camera when the modifier is trying to follow a slope. + * 1 means the camera pitch will match the slope angle exactly, 0.5 means half the angle; etc. + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Slopes", meta = (MultiLine = "true", EditCondition = "bTriggerOnSlopes", UIMin = "0.1", ClampMin = "0.1", UIMax = "1", ClampMax = "1")) + float SlopeFollowWeight = 1.f; +}; + +/* + * The settings of the camera Arm Offset. + */ +USTRUCT(BlueprintType) +struct FCameraArmOffsetSettings +{ + GENERATED_BODY() + + /** Offset at the end of the spring arm. Use this instead of the relative-space *rotation* so that the UE camera system works as expected. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true")) + FVector ArmSocketOffset = FVector(0.f, 40.f, 0.f); + + /** How long does it take to blend to the current ArmSocketOffset. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float ArmSocketOffsetBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true")) + TObjectPtr ArmSocketOffsetBlendCurve; + + /** Offset at start of spring, applied in world space. Use this if you want a world-space offset from the parent component instead of the usual relative-space location. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true")) + FVector ArmTargetOffset = FVector(0.f, 0.f, 58.f); + + /** How long does it take to blend to the current ArmTargetOffset. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float ArmTargetOffsetBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Offset Modifier", meta = (MultiLine = "true")) + TObjectPtr ArmTargetOffsetBlendCurve; +}; + +/* + * This is used by the PitchToArmLengthAndFOV Camera Modifier in blueprint. + * This makes the Arm Length and FOV change when the character is looking up/down (Used by many AAA games like all GTA games, Red Dead Redemption Assassin's Creed, Hogwarts Legacy, etc.). + */ +USTRUCT(BlueprintType) +struct FCameraPitchToArmAndFOVCurveSettings +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Pitch to Arm and FOV Modifier", meta = (MultiLine = "true")) + bool Enabled = true; + + /** Curve with X and Y between -1.0 and 1.0. This maps the LocalMinPitch (X=-1.0) and LocalMaxPitch (X=1.0) to the MinArmLength and MaxArmLength. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Pitch to Arm and FOV Modifier", meta = (MultiLine = "true")) + TObjectPtr PitchToArmLengthCurve; + + /** Curve with X and Y between -1.0 and 1.0. This maps the LocalMinPitch (X=-1.0) and LocalMaxPitch (X=1.0) to the MinFOV and MaxFOV. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Pitch to Arm and FOV Modifier", meta = (MultiLine = "true")) + TObjectPtr PitchToFOVCurve; +}; + +/* +* The settings of the camera Arm Length. Defines the range of the Arm Length and how to blend from one range to this one. This is used by the PitchToArmLengthAndFOV Camera Modifier in blueprint. +* This makes the Arm Length change when the character is looking up/down (Used by many AAA games like all GTA games, Red Dead Redemption Assassin's Creed, Hogwarts Legacy, etc.). +*/ +USTRUCT(BlueprintType) +struct FCameraArmLengthSettings +{ + GENERATED_BODY() + + /** The minimum arm length value. The length of the arm will change depending on the current pitch of the camera and the PitchToArmLengthCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Length", meta = (MultiLine = "true")) + float MinArmLength = 100.f; + + /** The maximum arm length value. The length of the arm will change depending on the current pitch of the camera and the PitchToArmLengthCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Length", meta = (MultiLine = "true")) + float MaxArmLength = 250.f; + + /** How long does it take to blend to the current Arm Length range. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Length", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float ArmRangeBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm Length", meta = (MultiLine = "true")) + TObjectPtr ArmRangeBlendCurve; +}; + +/* + * The settings of the camera FOV. Defines the range of the FOV and how to blend from one range to this one. This is used by the PitchToArmLengthAndFOV Camera Modifier in blueprint. + * This makes the FOV change when the character is looking up/down (Used by many AAA games like GTA IV). + */ +USTRUCT(BlueprintType) +struct FCameraFOVSettings +{ + GENERATED_BODY() + + /** The minimum FOV value. The FOV of the camera will change depending on the current pitch of the camera and the PitchToFOVCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera FOV", meta = (MultiLine = "true")) + float MinFOV = 90.f; + + /** The maximum FOV value. The FOV of the camera will change depending on the current pitch of the camera and the PitchToFOVCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera FOV", meta = (MultiLine = "true")) + float MaxFOV = 125.f; + + /** A tolerance in degrees above which the Angle Constraints modifier will start decelerating the camera. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera FOV", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float FOVRangeBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera FOV", meta = (MultiLine = "true")) + TObjectPtr FOVRangeBlendCurve; +}; + +/* + * The settings of the camera yaw contraints. This is useful to limit how much the character can look left/right. + * The looking input action needs to have a UGC_CameraTurnRateModifier for the camera to decelerate when close to the constraints. + */ +USTRUCT(BlueprintType) +struct FCameraYawConstraintSettings +{ + GENERATED_BODY() + + /** Whether the yaw should be constrained to LocalMinYaw and LocalMaxYaw. If the yaw was already out of the new range, it will blend into range using the BlendTime and BlendCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + bool bConstrainYaw = false; + + /** How close in degrees to the YawMin or YawMax should the camera start decelerating? */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float YawConstraintTolerance = 10.f; + + /** How much can the character look right (angle in degrees). */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + float LocalMinYaw = 0.f; + + /** How much can the character look left (angle in degrees). */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + float LocalMaxYaw = 359.998993f; + + /** How fast should we blend from one Yaw constraint range to this one. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float YawConstraintsBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + TObjectPtr YawConstraintsBlendCurve; +}; + +/* + * The settings of the camera pitch contraints. This is useful to limit how much the character can look up/down. AAA games usually do not allow the entire 180 degrees range. + * The looking input action needs to have a UGC_CameraTurnRateModifier for the camera to decelerate when close to the constraints. + */ +USTRUCT(BlueprintType) +struct FCameraPitchConstraintSettings +{ + GENERATED_BODY() + + /** Whether the pitch should be constrained to LocalMinPitch and LocalMaxPitch. If the yaw was already out of the new range, it will blend into range using the BlendTime and BlendCurve. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + bool bConstrainPitch = false; + + /** How close in degrees to the PitchMin or PitchMax should the camera start decelerating? */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float PitchConstraintTolerance = 10.f; + + // How much can the character look down (angle in degrees). + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + float LocalMinPitch = -89.99f; + + // How much can the character look up (angle in degrees). + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + float LocalMaxPitch = 89.99f; + + /** How fast should we blend from one Pitch constraint range to this one. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true", UIMin = "0", ClampMin = "0")) + float PitchConstraintsBlendTime = 0.5f; + + /** Controls the acceleration/deceleration of the blend. The curve has to be normalized (going from 0 to 1). Leave empty or use Hermite if unsure. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints", meta = (MultiLine = "true")) + TObjectPtr PitchConstraintsBlendCurve; +}; + +/* + * The settings of the focus camera. This is used for hard-lock in games. + * Has a function which retrieves the target we want the camera to look at. (Uses the Strategy Design Pattern) + */ +USTRUCT(BlueprintType) +struct FUGCCameraFocusSettings +{ + GENERATED_BODY() + + /** Whether the focus camera is enabled. If it is, this will use the Focus Target Method to get the target's location.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + bool bEnabled = false; + + /** How fast the camera will focus the target.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + float InterpSpeed = 10.f; + + /** An offset in degrees applied on the pitch and yaw. This is useful if you want the focused location to always be on the left/right or looked down/up.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + FRotator RotationOffset = FRotator::ZeroRotator; + + /** Whether the camera input should be ignored during focus.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + bool bIgnoreCameraInput = false; + + + /** Whether we should rotatet only the yaw angle of the camera or the pitch as well. + * You can set this to true and combine it with the Rotation Offset if you want the cam to stay at a specific pitch. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + bool bRotateYawOnly = false; + + /** Whether we should stop focusing if the Line of Sight is occluded.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + bool bStopIfBlockedLOS = false; + + /** How long should the line of sight be occluded before we stop focusing trying to focus target.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true", EditCondition = "bStopIfBlockedLOS")) + float BlockedLOSTimeThreshold = 3.f; + + /** The distance from the target below which we will stop focusing the target. If this is too low, the camera might start rotating crazily around the target.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + float MinDistanceFromTarget = 100.f; + + /** The distance from the target above which we will stop focusing the target.*/ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", meta = (MultiLine = "true")) + float MaxDistanceFromTarget = 1000.f; + + /** Function which retrieves the target we want the camera to look at. (Uses the Strategy Design Pattern) */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Focus", Instanced, meta = (MultiLine = "true")) + TObjectPtr FocusTargetMethod; +}; + +/* + * The settings of a camera modifier which needs to retrieve a list of actors. + * Has a function which retrieves the all actors relevant for some camera modifier/other object with settings inheriting from this class. (Uses the Strategy Design Pattern) + */ +USTRUCT(BlueprintType) +struct FUGCCameraSettingsWithGetActorsMethod +{ + GENERATED_BODY() + + /** Function which retrieves all relevant actors. (Uses the Strategy Design Pattern) */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Targets", Instanced, meta = (MultiLine = "true")) + TObjectPtr GetActorsMethod; +}; + +/* + * The settings of a camera modifier or other object which needs to retrieve one actor location. + * Has a function which an actor and its location using the instanced method. (Uses the Strategy Design Pattern) + */ +USTRUCT(BlueprintType) +struct FUGCCameraSettingsWithGetActorLocationMethod +{ + GENERATED_BODY() + + /** Function which retrieves the relevant actor and their position. (Uses the Strategy Design Pattern) */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Target", Instanced, meta = (MultiLine = "true")) + TObjectPtr GetActorLocationMethod; +}; + +/* + * Dithering settings used to dither/hide/fade objects colliding with the camera either occluding the line of sight to the player or overlapping the camera directly. + */ +USTRUCT(BlueprintType) +struct FCameraDitheringSettings +{ + GENERATED_BODY() + + // The name of the scalar material parameter to blend in/out. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering") + FName MaterialOpacityParameterName = "Opacity"; + + // Whether the material has a vector parameter which should be updated to the player location. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering") + bool bUpdateMaterialPlayerPosition = false; + + // The name of the vector parameter to set to the player position inside the Material Parameter Collection. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering", meta = (EditCondition = "bUpdateMaterialPlayerPosition")) + FName MaterialPlayerPositionParameterName = "PlayerLocation"; + + // The minimum value of the material's Opacity when dithered. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering", meta = (UIMin = 0.f, UIMax = 1.f, ClampMin = 0.f, ClampMax = 1.f)) + float MaterialDitherMinimum = 0.1f; + + // Controls the speed of the blend when starting to dither an object. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering", meta = (UIMin = 0.f, ClampMin = 0.f)) + float DitherInSpeed = 10.f; + + // Controls the speed of the blend when finishing to dither an object. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering", meta = (UIMin = 0.f, ClampMin = 0.f)) + float DitherOutSpeed = 10.f; + + // Actors with this tag will not be dithered even if they overlap DitherLOSChannel and DitherOverlapChannel. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering") + FName IgnoreDitheringTag = "UGC_IgnoreDithering"; + + // Whether we should dither the child actors attached to the obstacle. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions") + bool bDitherChildActors = true; + + // Whether we should dither the components attached to the obstacle. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions") + bool bDitherAttachedComponents = true; + + // Whether we should dither objects that block the LINE OF SIGHT of the camera. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Line Of Sight") + bool bDitherLineOfSight = false; + + // The collision channel to use when checking for what the LINE OF SIGHT of the camera is colliding with. The other objects have to Overlap this channel in order to be dithered! + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Line Of Sight", meta = (EditCondition = "bDitherLineOfSight")) + TEnumAsByte DitherLOSChannel = ECC_Camera; + + // The width of the probe when testing if any object is blocking the LINE OF SIGHT from the player to the camera. The other objects have to Overlap this channel in order to be dithered! + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Line Of Sight", meta = (UIMin = 0.f, ClampMin = 0.f, EditCondition = "bDitherLineOfSight")) + float LOSProbeSize = 5.f; + + // The minimum amount of time the camera LINE OF SIGHT has to collide with an object for it to be dithered. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Line Of Sight", meta = (UIMin = 0.f, ClampMin = 0.f, EditCondition = "bDitherLineOfSight")) + float CollisionTimeThreshold = 0.f; + + // Whether we should dither objects that OVERLAP the actual camera component. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Overlaps") + bool bDitherOverlaps = true; + + // Whether we should dither the owner character when the actual camera overlaps with them. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Overlaps", meta = (EditCondition = "bDitherOverlaps")) + bool bDitherOwner = true; + + // The collision channel to use when checking for what the actual camera is overlapping with. The other objects have to Overlap this channel in order to be dithered! + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Overlaps", meta = (EditCondition = "bDitherOverlaps")) + TEnumAsByte DitherOverlapChannel = ECC_Camera; + + // The radius around the camera where we check for collisions. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC Camera Dithering|Collisions|Overlaps", meta = (UIMin = 0.f, ClampMin = 0.f, EditCondition = "bDitherOverlaps")) + float SphereCollisionRadius = 15.f; +}; + +/** + * Struct defining a feeler ray used for camera penetration avoidance. The feeler uses sphere sweeps. + */ +USTRUCT(BlueprintType) +struct FPenetrationAvoidanceFeeler +{ + GENERATED_BODY() +public: + FPenetrationAvoidanceFeeler(); + + FPenetrationAvoidanceFeeler(const FRotator& InAdjustmentRot, const float& InWorldWeight, const float& InPawnWeight, const float& InExtent); + + // FRotator describing deviance from main ray. + UPROPERTY(EditAnywhere, Category = PenetrationAvoidanceFeeler) + FRotator AdjustmentRot; + + // How much this feeler affects the final position if it hits the world. + UPROPERTY(EditAnywhere, Category = PenetrationAvoidanceFeeler, meta = (UIMin = 0.f, UIMax = 1.f, ClampMin = 0.f, ClampMax = 1.f)) + float WorldWeight = 0.f; + + // How much this feeler affects the final position if it hits a APawn (setting to 0 will not attempt to collide with pawns at all). + UPROPERTY(EditAnywhere, Category = PenetrationAvoidanceFeeler, meta = (UIMin = 0.f, UIMax = 1.f, ClampMin = 0.f, ClampMax = 1.f)) + float PawnWeight = 0.f; + + // The radius of this feeler probe. + UPROPERTY(EditAnywhere, Category = PenetrationAvoidanceFeeler) + float ProbeRadius = 5.f; +}; + +/** + * Camera collision settings which define how the camera avoidance uses collision feelers or whiskers to avoid camera clipping through obstacles. + */ +USTRUCT(BlueprintType) +struct FCameraCollisionSettings +{ + GENERATED_BODY() +public: + FCameraCollisionSettings(); + + /* The time the camera takes to go to the safe location after a collision has been detected. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision") + float PenetrationBlendInTime = 0.05f; + + /* The time the camera takes to go back to its normal position after the collision has finished. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision") + float PenetrationBlendOutTime = 0.5f; + + // If true, does collision checks to keep the camera out of the world. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision") + bool bPreventPenetration = true; + + // If true, try to detect nearby walls and move the camera in anticipation. Helps prevent popping. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision", meta = (EditCondition = "bPreventPenetration")) + bool bDoPredictiveAvoidance = true; + + /** + * These are the feeler rays that are used to find where to place the camera. + * Index: 0 : This is the normal feeler we use to prevent collisions. + * Index: 1+ : These feelers are used if you bDoPredictiveAvoidance=true, to scan for potential impacts if the player + * were to rotate towards that direction and primitively collide the camera so that it pulls in before + * impacting the occluder. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision") + TArray PenetrationAvoidanceFeelers; + + // Actors with this tag will ignore camera collisions. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collision") + FName IgnoreCameraCollisionTag = "UGC_IgnoreCameraCollision"; +}; + +/** + * Camera settings for the spring arm component. + */ +USTRUCT(BlueprintType) +struct FCameraArmLagSettings +{ + GENERATED_BODY() +public: + + /** + * If true, these settings will be used and the settings authored directly in the spring arm will be ignored. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag") + bool bOverrideSpringArmComponentSettings = false; + + /** + * If true, camera lags behind target position to smooth its movement. + * @see CameraLagSpeed + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag", meta = (editcondition = "bOverrideSpringArmComponentSettings")) + bool bEnableCameraLag = true; + + /** + * If true, camera lags behind target rotation to smooth its movement. + * @see CameraRotationLagSpeed + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag", meta = (editcondition = "bOverrideSpringArmComponentSettings")) + bool bEnableCameraRotationLag = false; + + /** If bEnableCameraLag is true, controls how quickly camera reaches target position. Low values are slower (more lag), high values are faster (less lag), while zero is instant (no lag). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag", meta = (editcondition = "bEnableCameraLag && bOverrideSpringArmComponentSettings", ClampMin = "0.0", ClampMax = "1000.0", UIMin = "0.0", UIMax = "1000.0")) + float CameraLagSpeed = 8.f; + + /** If bEnableCameraRotationLag is true, controls how quickly camera reaches target position. Low values are slower (more lag), high values are faster (less lag), while zero is instant (no lag). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag", meta = (editcondition = "bEnableCameraRotationLag && bOverrideSpringArmComponentSettings", ClampMin = "0.0", ClampMax = "1000.0", UIMin = "0.0", UIMax = "1000.0")) + float CameraRotationLagSpeed = 10.f; + + /** Max distance the camera target may lag behind the current location. If set to zero, no max distance is enforced. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lag", meta = (editcondition = "bEnableCameraLag && bOverrideSpringArmComponentSettings", ClampMin = "0.0", UIMin = "0.0")) + float CameraLagMaxDistance = 0.f; +}; + +/** + * Generic data class which holds settings for an associated Camera Add-On Modifier. + */ +UCLASS(abstract, Category = "UGC|Add On|Settings", EditInlineNew, Blueprintable) +class AURORADEVS_UGC_API UUGC_CameraAddOnModifierSettings : public UObject +{ + GENERATED_BODY() +public: +}; + +/** Camera Data Asset holding all of the camera settings. */ +UCLASS(Blueprintable, BlueprintType) +class UUGC_CameraDataAssetBase : public UPrimaryDataAsset +{ + GENERATED_BODY() +public: + /** + * The settings of the camera Arm Length. Defines the range of the Arm Length and how to blend from one range to this one. This is used by the PitchToArmLengthAndFOV Camera Modifier in blueprint. + * This makes the Arm Length change when the character is looking up/down (Used by many AAA games like all GTA games, Red Dead Redemption Assassin's Creed, Hogwarts Legacy, etc.). + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm") + FCameraArmLengthSettings ArmLengthSettings; + + /** + * The settings of the camera FOV. Defines the range of the FOV and how to blend from one range to this one. This is used by the PitchToArmLengthAndFOV Camera Modifier in blueprint. + * This makes the FOV change when the character is looking up/down (Used by many AAA games like GTA IV). + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera FOV") + FCameraFOVSettings FOVSettings; + + /** Camera collision settings which define how the camera avoidance uses collision feelers or whiskers to avoid camera clipping through obstacles. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "CameraCollisions") + FCameraCollisionSettings CollisionSettings; + + /** Dithering settings used to dither/hide/fade objects colliding with the camera either occluding the line of sight to the player or overlapping the camera directly. */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Dithering") + FCameraDitheringSettings DitheringSettings; + + /** + * The settings of the camera pitch follow. This is useful for player who don't like to control the camera too much. + * The PitchFollowModifier in blueprint will adjust the pitch to face slopes, falling and reset the pitch if it's left untouched for long enough. + * (Used by AAA games like Red Dead Redemption, Hogwarts Legacy, Witcher, etc) + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Movement Follow|Pitch") + FCameraPitchFollowSettings PitchFollowSettings; + + /** + * The settings of the camera yaw follow. This is useful for player who don't like to control the camera too much. + * The YawFollowModifier in blueprint will adjust the yaw to face the movement direction (Used by AAA games like Hogwarts Legacy, Witcher, etc). + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Movement Follow|Yaw") + FCameraYawFollowSettings YawFollowSettings; + + /** The settings of the camera Spring Arm Offset. Offset at the end of the spring arm. Use this instead of the relative world location so that the UE camera system works as exptected */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Arm") + FCameraArmOffsetSettings ArmOffsetSettings; + + /** + * The settings of the camera pitch contraints. This is useful to limit how much the character can look up/down. AAA games usually do not allow the entire 180 degrees range. + * The looking input action needs to have a UGC_CameraSlowDownInputModifier for the camera to decelerate when close to the constraints. + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints") + FCameraPitchConstraintSettings PitchConstraints; + + /** + * The settings of the camera yaw contraints. This is useful to limit how much the character can look left/right. + * The looking input action needs to have a UGC_CameraSlowDownInputModifier for the camera to decelerate when close to the constraints. + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Camera Angle Constraints") + FCameraYawConstraintSettings YawConstraints; + + /** + * The settings of the focus camera. This is used for hard-lock in games. + * Has a function which retrieves the target we want the camera to look at. (Uses the Strategy Design Pattern) + */ + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "CameraFocus") + FUGCCameraFocusSettings FocusSettings; + + /** This makes the Arm Length and FOV change when the character is looking up/down (Used by many AAA games like all GTA games, Red Dead Redemption Assassin's Creed, Hogwarts Legacy, etc.). */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "PitchToArmLengthAndFOVCurv") + FCameraPitchToArmAndFOVCurveSettings PitchToArmAndFOVCurveSettings; + + /** Arm lag settings of the camera. Need to be enabled by checking `bOverrideSpringArmComponentSettings`, otherwise the settings of the component are used instead. **/ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Camera Arm") + FCameraArmLagSettings ArmLagSettings; + + UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Add Ons", Instanced, meta = (MultiLine = "true")) + TArray> AddOnsSettings; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IFocusTargetMethod.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IFocusTargetMethod.h new file mode 100644 index 0000000..9498111 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IFocusTargetMethod.h @@ -0,0 +1,32 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "UObject/WeakObjectPtrTemplates.h" + +#include "UGC_IFocusTargetMethod.generated.h" + +/** + * Function which retrieves the target we want the camera to look at. (Uses the Strategy Design Pattern) + */ +UCLASS(abstract, Category = "UGC|Methods", EditInlineNew, Blueprintable) +class AURORADEVS_UGC_API UUGC_IFocusTargetMethod : public UObject +{ + GENERATED_BODY() +public: + /* + * Get the location of the target we want the camera to look at. + * @param Owner The owner of the camera. + * @param OwnerLocation The world location of the owner of camera. + * @param ViewPointLocation Camera's location. + * @param ViewPointRotation Camera's rotation. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Methods") + AActor* GetTargetLocation(class AActor* InOwner, FVector OwnerLocation, FVector ViewPointLocation, FRotator ViewPointRotation, FVector& OutTargetLocation); + +private: + /** Getter for the cached world pointer, will return null if the actor is not actually spawned in a level */ + virtual UWorld* GetWorld() const override; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IGetActorsMethod.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IGetActorsMethod.h new file mode 100644 index 0000000..38df72c --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Methods/UGC_IGetActorsMethod.h @@ -0,0 +1,32 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "UObject/WeakObjectPtrTemplates.h" + +#include "UGC_IGetActorsMethod.generated.h" + +/** + * Function which retrieves a vector of actors. (Uses the Strategy Design Pattern) + */ +UCLASS(abstract, Category = "UGC|Methods", EditInlineNew, Blueprintable) +class AURORADEVS_UGC_API UUGC_IGetActorsMethod : public UObject +{ + GENERATED_BODY() +public: + /* + * Get the all actors relevant for this method. + * @param Owner The owner of the camera. + * @param OwnerLocation The world location of the owner of camera. + * @param ViewPointLocation Camera's location. + * @param ViewPointRotation Camera's rotation. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Methods") + void GetActors(class AActor* InOwner, FVector OwnerLocation, FVector ViewPointLocation, FRotator ViewPointRotation, TArray& OutActors); + +private: + /** Getter for the cached world pointer, will return null if the actor is not actually spawned in a level */ + virtual UWorld* GetWorld() const override; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraAnimationModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraAnimationModifier.h new file mode 100644 index 0000000..c0aa322 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraAnimationModifier.h @@ -0,0 +1,131 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CameraAnimationCameraModifier.h" +#include "DrawDebugHelpers.h" +#include "UGC_CameraAnimationModifier.generated.h" + +/** + * Delegate for when a UGC Camera Animation is completed, whether the animation has been interrupted or finished. + * + * bInterrupted = true if it was not property finished + */ +DECLARE_DELEGATE_TwoParams(FOnCameraAnimationEnded, class UCameraAnimationSequence*, bool /*bInterrupted*/) + +/** + * Delegate for when a UGC Camera Animation started easing out, whether the animation has actually been interrupted or not. + * + * bInterrupted = true if it was not property finished + */ +DECLARE_DELEGATE_OneParam(FOnCameraAnimationEaseOutStarted, class UCameraAnimationSequence*) + +UENUM() +enum class ECameraAnimationResetType : uint8 +{ + BackToStart UMETA(ToolTip = "The camera will go back to the position it started from."), + ResetToZero UMETA(ToolTip = "The camera's orientation will be reset to zero. This is usually the back of the character. If UseControllerRotationYaw is true, this is forcefully used."), + ContinueFromEnd UMETA(ToolTip = "The camera will blend out from the last position of the animation.") +}; + +USTRUCT() +struct FUGCActiveAnimationInfo +{ + GENERATED_USTRUCT_BODY() + + ECameraAnimationResetType ResetType = ECameraAnimationResetType::ResetToZero; + /** Runtime interpolated distance percentage (0 = fully blocked, 1 = clear) */ + float DistBlockedPct = 1.f; + bool bDoCollisionChecks = false; + bool bWasEasingOut = false; +}; + +/** + * Gameplay Camera Animation Modifier which plays in the correct transform space in rgeards to the owning player. + */ +UCLASS(abstract, ClassGroup = "UGC Camera Modifiers") +class AURORADEVS_UGC_API UUGC_CameraAnimationModifier : public UCameraAnimationCameraModifier +{ + GENERATED_BODY() + +public: + // UCameraModifier interface + virtual bool ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) override; + + void CameraAnimation_SetEasingOutDelegate(FOnCameraAnimationEaseOutStarted& InOnAnimationEaseOutStarted, FCameraAnimationHandle AnimationHandle); + void CameraAnimation_SetEndedDelegate(FOnCameraAnimationEnded& InOnAnimationEnded, FCameraAnimationHandle AnimationHandle); + + FCameraAnimationHandle PlaySingleCameraAnimation(UCameraAnimationSequence* Sequence, FCameraAnimationParams Params, ECameraAnimationResetType ResetType, bool bInterruptOthers, bool bDoCollisionChecks); + + /** + * Stops the given camera animation sequence. If nullptr, will stop whatever is currently active. + * @param Sequence The camera sequence animation. + * @param bImmediate True to stop it right now and ignore blend out, false to let it blend out as indicated. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Camera Animation") + void StopCameraAnimationSequence(UCameraAnimationSequence* Sequence, bool bImmediate = false); + + /** + * Get the current camera animation playing on this modifier. + * @return The current camera animation playing. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Camera Animations") + void GetCurrentCameraAnimations(TArray& OutAnimations) const; + + /** + * Returns whether the given camera animation is playing on this modifier. + * @param Sequence The Camera Animation Sequence. + * @return Whether the corresponding camera animation is playing or not. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Camera Animations") + bool IsCameraAnimationSequenceActive(UCameraAnimationSequence* Sequence) const; + + /** + * Returns whether any camera animation is playing on this modifier. + * @param Sequence The Camera Animation Sequence. + * @return Whether any camera animation is playing or not. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Camera Animations") + bool IsAnyCameraAnimationSequence() const; + +protected: + void UGCDeactivateCameraAnimation(FActiveCameraAnimationInfo& ActiveAnimation); + + // UCameraAnimationCameraModifier interface + virtual void UGCTickActiveAnimation(float DeltaTime, FMinimalViewInfo& InOutPOV); + + // UCameraAnimationCameraModifier interface + virtual void UGCTickAnimation(FActiveCameraAnimationInfo& CameraAnimation, float DeltaTime, FMinimalViewInfo& InOutPOV, int Index); + virtual void UGCTickAnimCollision(FActiveCameraAnimationInfo& CameraAnimation, float DeltaTime, FMinimalViewInfo& InOutPOV, int Index); + + FVector GetTraceSafeLocation(FMinimalViewInfo const& InPOV); + + template + T* GetViewTargetAs() const + { + return Cast(GetViewTarget()); + } + +private: +#if ENABLE_DRAW_DEBUG + void UGCDebugAnimation(FActiveCameraAnimationInfo& ActiveAnimation, float DeltaTime); +#endif + +private: + UPROPERTY(Transient) + TObjectPtr UGCCameraManager = nullptr; + UPROPERTY(Transient) + TObjectPtr CollisionModifier = nullptr; + + /** How should the camera behave after the current animation is over. */ + UPROPERTY(Transient) + TArray UGCAnimInfo; + + UPROPERTY(Transient) + int LastIndex = 0; + + // Delegates + FOnCameraAnimationEnded OnAnimationEnded; + FOnCameraAnimationEaseOutStarted OnAnimationEaseOutStarted; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraCollisionModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraCollisionModifier.h new file mode 100644 index 0000000..e457a4b --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraCollisionModifier.h @@ -0,0 +1,63 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +#include "UGC_CameraModifier.h" +#include "Camera/Data/UGC_CameraData.h" +#include "Camera/CameraTypes.h" +#include "DrawDebugHelpers.h" +#include "Containers/StaticBitArray.h" +#include "UGC_CameraCollisionModifier.generated.h" + +/** + * DEPRECATED. USE UGC_SpringArmComponent INSTEAD. + * Camera Modifier which does camera avoidance using predictive collision feelers. + */ +UCLASS(abstract, ClassGroup = "UGC Camera Modifiers", meta = (Deprecated = 5.5)) +class AURORADEVS_UGC_API UUGC_CameraCollisionModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() + + UUGC_CameraCollisionModifier(); + +public: + // Force collision modifier to use a single ray by another modifier. Do not use this if you're not familiar with it. + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Collision") + void AddSingleRayOverrider(UCameraModifier const* OverridingModifier) { if (OverridingModifier) SingleRayOverriders.AddUnique(OverridingModifier); } + + // Remove single ray modifier override. Do not use this if you're not familiar with it. + UFUNCTION(BlueprintCallable, Category = "UGC|Modifiers|Collision") + void RemoveSingleRayOverrider(UCameraModifier const* OverridingModifier) { if (OverridingModifier) SingleRayOverriders.Remove(OverridingModifier); } + +protected: + virtual bool ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) override; + + void UpdatePreventPenetration(float DeltaTime, FMinimalViewInfo& InOutPOV); + + void PreventCameraPenetration(class AActor const& ViewTarget, FVector const& SafeLoc, FVector& OutCameraLoc, float const& DeltaTime, float& OutDistBlockedPct, bool bSingleRayOnly); + + FVector GetTraceSafeLocation(FMinimalViewInfo const& POV); + + void ResetSingleRayOverriders() { SingleRayOverriders.Reset(); } + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC|Modifiers|Collision") + FCameraCollisionSettings CollisionSettings; + + // If you don't want the camera to start close to the character and smoothly pan out once your character is spawned, default-initialize this variable to 1.f. + UPROPERTY(Transient) + float AimLineToDesiredPosBlockedPct = 1.f; + + UPROPERTY(Transient) + TArray> DebugActorsHitDuringCameraPenetration; + +protected: + TArray SingleRayOverriders; + TStaticBitArray<128u> CollidingFeelers; + +#if ENABLE_DRAW_DEBUG + mutable float LastDrawDebugTime = -MAX_FLT; +#endif +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraDitheringModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraDitheringModifier.h new file mode 100644 index 0000000..f7bf0a8 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraDitheringModifier.h @@ -0,0 +1,93 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Camera/Modifiers/UGC_CameraModifier.h" +#include "Camera/Data/UGC_CameraData.h" +#include "DrawDebugHelpers.h" +#include "UGC_CameraDitheringModifier.generated.h" + +UENUM() +enum class EDitherType : uint8 +{ + None, + BlockingLOS, + OverlappingCamera +}; + +USTRUCT(BlueprintType) +struct FDitheredActorState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + TObjectPtr Actor; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + float CurrentOpacity = 1.f; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + float CollisionTime = 0.f; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + bool bIsDitheringIn = false; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + bool bIsDitheringOut = false; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Modifiers|Camera Dithering") + EDitherType DitherType = EDitherType::None; + + bool IsValid() const { return Actor != nullptr && DitherType != EDitherType::None; } + + void StartDithering(AActor* InActor, EDitherType InDitherType); + + void Invalidate(); + + friend bool operator==(FDitheredActorState const& lhs, FDitheredActorState const& rhs) + { + return lhs.Actor == rhs.Actor; + } + + void ComputeOpacity(float DeltaTime, float DitherInTime, float DitherOutTime, float DitherMin); +}; + +/** + * UGC Camera Modifier used to dither objects colliding with the camera + */ +UCLASS() +class AURORADEVS_UGC_API UUGC_CameraDitheringModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() + +public: + UUGC_CameraDitheringModifier(); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Modifiers|Dithering") + void ResetDitheredActors(); + +protected: + virtual bool ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) override; + + virtual void ApplyDithering(float DeltaTime, FDitheredActorState& DitherState); + +private: +#if ENABLE_DRAW_DEBUG + void UGCDebugDithering(FDitheredActorState& DitherState, float DeltaTime, float DitherMin); +#endif + +protected: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC|Modifiers|Dithering") + FCameraDitheringSettings DitheringSettings; + + /** Material Parameter Collection for everything dithering-related */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UGC|Modifiers|Dithering") + TSoftObjectPtr DitheringMPC; + + UPROPERTY(Transient) + TSoftObjectPtr DitheringMPCInstance; + + UPROPERTY(Transient) + TArray DitheredActorStates; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraModifier.h new file mode 100644 index 0000000..74aeb1d --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraModifier.h @@ -0,0 +1,233 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Camera/CameraModifier.h" +#include "UGC_CameraModifier.generated.h" + +/** + * Base Camera Modifier Class + */ +UCLASS(abstract, ClassGroup = "UGC|Camera Modifiers") +class AURORADEVS_UGC_API UUGC_CameraModifier : public UCameraModifier +{ + GENERATED_BODY() + +public: + virtual void EnableModifier() override; + virtual void DisableModifier(bool bImmediate) override; + bool GetDebugEnabled() const { return bDebug; } + + /** + * Function called once this modifier gets enabled. + * @param LastPOV - the last view POV of the camera. + */ + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void OnModifierEnabled(FMinimalViewInfo const& LastPOV); + + /** + * Function called once this modifier gets disabled. + * @param bWasImmediate - true if modifier was disabled without a blend out. + * @param LastPOV - the last view POV of the camera. + */ + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void OnModifierDisabled(FMinimalViewInfo const& LastPOV, bool bWasImmediate); + + /** + * Called to give modifiers a chance to adjust view rotation updates before they are applied. + * + * Default just returns ViewRotation unchanged + * @param ViewTarget - Current view target. + * @param InLocalControlRotation - The difference between the actor rotation and the control rotation. + * @param DeltaTime - Frame time in seconds. + * @param InViewLocation - In. The view location of the camera. + * @param InViewRotation - In. The view rotation of the camera. + * @param InDeltaRot - In/out. How much the rotation changed this frame. + * @param OutDeltaRot - Out. How much the control rotation should change this frame. + * @return Return true to prevent subsequent (lower priority) modifiers to further adjust rotation, false otherwise. + */ + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + bool ProcessControlRotation(AActor* ViewTarget, float DeltaTime, FVector InViewLocation, FRotator InViewRotation, FRotator InLocalControlRotation, FRotator InDeltaRot, FRotator& OutDeltaRot); + + /** + * Called to give modifiers a chance to adjust arm length and FOV before they are applied. + * + * @param DeltaTime - Frame time in seconds. + * @param InFOV - The Current FOV of the camera. + * @param InArmLength - The Current Arm Length of the camera. + * @param ViewLocation - The view location of the camera. + * @param ViewRotation - The view rotation of the camera. + * @param OutFOV - The New FOV of the camera. + * @param OutArmLength - The New Arm Length of the camera. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void ProcessBoomLengthAndFOV(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength); + + /** + * Called to give modifiers a chance to adjust arm offsets before they are applied. + * + * @param DeltaTime - Frame time in seconds. + * @param InSocketOffset - The Current Socket Offset of the camera. + * @param InTargetOffset - The Current Target Offset of the camera. + * @param ViewLocation - The view location of the camera. + * @param ViewRotation - The view rotation of the camera. + * @param OutSocketOffset - New Socket Offset of the camera. + * @param OutTargetOffset - New Target Offset of the camera. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void ProcessBoomOffsets(float DeltaTime, FVector InSocketOffset, FVector InTargetOffset, FVector ViewLocation, FRotator ViewRotation, FVector& OutSocketOffset, FVector& OutTargetOffset); + + /** + * Called to give modifiers a chance to adjust miscelaneous stuff at the end of the update order. + * + * @param DeltaTime - Frame time in seconds. + * @param ViewLocation - The view location of the camera. + * @param ViewRotation - The view rotation of the camera. + */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void PostUpdate(float DeltaTime, FVector ViewLocation, FRotator ViewRotation); + + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void OnAnyLevelSequenceStarted(); + + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void OnAnyLevelSequenceEnded(); + + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + void OnSetViewTarget(bool bImmediate, bool bNewTargetIsOwner); + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier") + bool IsDebugEnabled() const; + + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Modifier") + void ToggleDebug(bool const bEnabled); + + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Modifier") + void SetAlpha(float InAlpha) { Alpha = InAlpha; } + + /** + * Called to give modifiers a chance to adjust both the yaw turn rate and pitch turn rate. However the input for looking needs to have UGC_CameraTurnRateModifier. + * + * @param DeltaTime - Frame time in seconds. + * @param InLocalControlRotation - The difference between the actor rotation and the control rotation. + * @param OutPitchTurnRate - Out. New value of the pitch turn rate (between 0 and 1). + * @param OutYawTurnRate - Out. New value of the yaw turn rate (between 0 and 1). + * @return Return true to prevent subsequent (lower priority) modifiers to further adjust rotation, false otherwise. + */ + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Modifier") + bool ProcessTurnRate(float DeltaTime, FRotator InLocalControlRotation, float InPitchTurnRate, float InYawTurnRate, float& OutPitchTurnRate, float& OutYawTurnRate); + + bool CanPlayDuringCameraAnimation() const { return bPlayDuringCameraAnimations; } + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + FVector GetOwnerVelocity() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + bool IsOwnerFalling() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + bool IsOwnerStrafing() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + bool IsOwnerMovingOnGround() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + void ComputeOwnerFloorDistance(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, float& OutFloorDistance) const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + void ComputeOwnerFloorNormal(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, FVector& OutFloorNormal) const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + void ComputeOwnerSlopeAngle(float& OutSlopePitchDegrees, float& OutSlopeRollDegrees); + + /* + * Returns value betwen 1 (the character is looking where they're moving) or -1 (looking in the opposite direction they're moving). + * Will return 0 if the character isn't moving. + */ + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + float ComputeOwnerLookAndMovementDot(); + +protected: + virtual bool ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV) override; + virtual void ModifyCamera(float DeltaTime, FVector ViewLocation, FRotator ViewRotation, float FOV, FVector& OutViewLocation, FRotator& OutViewRotation, float& OutFOV) override; + virtual bool ProcessViewRotation(AActor* ViewTarget, float DeltaTime, FRotator& OutViewRotation, FRotator& OutDeltaRot) override; + + template + T* GetViewTargetAs() const { return Cast(GetViewTarget()); } + + void UpdateOwnerReferences(); + + void UpdateInternalVariables(float DeltaTime); + +protected: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Modifier Settings") + bool bPlayDuringCameraAnimations = false; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr UGCCameraManager = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr OwnerController = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr OwnerCharacter = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr OwnerPawn = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr SpringArm = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + TObjectPtr MovementComponent = nullptr; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + FVector CurrentSocketOffset; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + FVector CurrentTargetOffset; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + float CurrentArmLength; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + bool bHasMovementInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + FVector PreviousMovementInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + FVector MovementInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + float TimeSinceMovementInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + bool bHasRotationInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + FRotator RotationInput; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Modifier|Internal") + float TimeSinceRotationInput; +}; + +/** + * Base Camera Modifier Class for Add-on modifiers + */ +UCLASS(abstract, ClassGroup = "UGC|Add On|Camera Modifiers") +class AURORADEVS_UGC_API UUGC_CameraAddOnModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() +public: + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "UGC|Add On|Camera Modifiers") + void SetSettings(class UUGC_CameraAddOnModifierSettings* InSettings); + + // Add-On Settings class associated to this add-on modifier. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings") + TSubclassOf SettingsClass; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Settings") + TObjectPtr Settings; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.h new file mode 100644 index 0000000..e78768d --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertiesAnimNotifyModifiers.h @@ -0,0 +1,86 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Camera/Modifiers/UGC_CameraModifier.h" +#include "Misc/Build.h" +#include "Misc/Guid.h" +#include "UGC_CameraPropertyRequestStackHelper.h" +#include "UGC_CameraPropertiesAnimNotifyModifiers.generated.h" + +/** + * Camera Modifier in charge of handling FOV change requests from Anim Notifies. + */ +UCLASS(abstract, ClassGroup = "UGC Camera Modifiers") +class AURORADEVS_UGC_API UUGC_FOVAnimNotifyCameraModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() + +public: + UUGC_FOVAnimNotifyCameraModifier(); + +protected: + void PushFOVAnimNotifyRequest(FGuid RequestId, float TargetFOV, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve); + void PopFOVAnimNotifyRequest(FGuid RequestId); + +protected: + void ProcessBoomLengthAndFOV_Implementation(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength) override; + void OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate); + +protected: + friend class UUGC_FOVRequestAnimNotifyState; + UGC_CameraPropertyRequestStackHelper RequestHelper; +}; + +/** + * Camera Modifier in charge of handling Arm Offset changes requests from Anim Notifies. + */ +UCLASS(abstract, ClassGroup = "UGC Camera Modifiers") +class AURORADEVS_UGC_API UUGC_ArmOffsetAnimNotifyCameraModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() + +public: + UUGC_ArmOffsetAnimNotifyCameraModifier(); + +protected: + void PushArmSocketOffsetAnimNotifyRequest(FGuid RequestId, FVector TargetOffset, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve); + void PopArmSocketOffsetAnimNotifyRequest(FGuid RequestId); + + void PushArmTargetOffsetAnimNotifyRequest(FGuid RequestId, FVector TargetOffset, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve); + void PopArmTargetOffsetAnimNotifyRequest(FGuid RequestId); + +protected: + void ProcessBoomOffsets_Implementation(float DeltaTime, FVector InSocketOffset, FVector InTargetOffset, FVector ViewLocation, FRotator ViewRotation, FVector& OutSocketOffset, FVector& OutTargetOffset) override; + void OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate); + +protected: + friend class UUGC_ArmOffsetRequestAnimNotifyState; + UGC_CameraPropertyRequestStackHelper SocketOffsetRequestHelper; + UGC_CameraPropertyRequestStackHelper TargetOffsetRequestHelper; +}; + +/** + * Camera Modifier in charge of handling Arm Length changes requests from Anim Notifies. + */ +UCLASS(abstract, ClassGroup = "UGC Camera Modifiers") +class AURORADEVS_UGC_API UUGC_ArmLengthAnimNotifyCameraModifier : public UUGC_CameraModifier +{ + GENERATED_BODY() + +public: + UUGC_ArmLengthAnimNotifyCameraModifier(); + +protected: + void PushArmLengthAnimNotifyRequest(FGuid RequestId, float TargetLength, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve); + void PopArmLengthAnimNotifyRequest(FGuid RequestId); + +protected: + void ProcessBoomLengthAndFOV_Implementation(float DeltaTime, float InFOV, float InArmLength, FVector ViewLocation, FRotator ViewRotation, float& OutFOV, float& OutArmLength) override; + void OnModifierDisabled_Implementation(FMinimalViewInfo const& LastPOV, bool bWasImmediate); + +protected: + friend class UUGC_ArmLengthRequestAnimNotifyState; + UGC_CameraPropertyRequestStackHelper RequestHelper; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.h new file mode 100644 index 0000000..c7554d6 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.h @@ -0,0 +1,72 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Build.h" +#include "Misc/Guid.h" +#include "Camera/CameraTypes.h" + +class UUGC_CameraModifier; + +template +struct UGC_CameraPropertyValueRequest +{ +public: + bool IsValid() const { return RequestId.IsValid(); } + void Invalidate(); + +public: + FGuid RequestId = FGuid(); + + ValueType TargetValue = {}; + float TotalDuration = 0.f; + float BlendInDuration = 0.f; + float BlendOutDuration = 0.f; + float CurrentTime = 0.f; + float BlendInCurrentTime = 0.f; + float BlendOutCurrentTime = 0.f; + + class UCurveFloat* BlendInCurve = nullptr; + class UCurveFloat* BlendOutCurve = nullptr; + + bool bBlendingIn = false; + bool bBlendingOut = false; +}; + +/** + * Camera Modifier Helper in charge of handling value change requests from Anim Notifies. + */ +template +class AURORADEVS_UGC_API UGC_CameraPropertyRequestStackHelper final +{ +public: + void Init(const FName& PropertyName, const TFunction& DebugGetterFunction); + +public: + void PushValueRequest(FGuid RequestId, ValueType TargetValue, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve); + void PopValueRequest(FGuid RequestId); + void ProcessValue(float DeltaTime, ValueType InValue, FVector ViewLocation, FRotator ViewRotation, ValueType& OutValue); + void OnModifierDisabled(FMinimalViewInfo const& LastPOV, bool bWasImmediate); + +protected: + void InvalidateRequest(int32 RequestIndex); + int32 FindInactiveRequest(); + +protected: + friend class UUGC_FOVRequestAnimNotifyState; + TArray> Requests; + TFunction DebugGetterFunction; + + FName PropertyName = TEXT(""); + +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) +public: + FLinearColor PropertyColor = FColor::Red; + +protected: + uint32 NumberActive = 0; +#endif +}; + +#include "UGC_CameraPropertyRequestStackHelper.inl" \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.inl b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.inl new file mode 100644 index 0000000..7594e03 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_CameraPropertyRequestStackHelper.inl @@ -0,0 +1,211 @@ +#pragma once +#include "Curves/CurveFloat.h" +#include "Engine/Engine.h" +#include "Containers/UnrealString.h" +#include "Templates/IsArithmetic.h" +#include "Templates/Decay.h" + +template +void UGC_CameraPropertyValueRequest::Invalidate() +{ + *this = UGC_CameraPropertyValueRequest(); +} + +template +void UGC_CameraPropertyRequestStackHelper::Init(const FName& InPropertyName, const TFunction& BoolDebugGetterFunction) +{ + PropertyName = InPropertyName; + DebugGetterFunction = BoolDebugGetterFunction; +} + +template +void UGC_CameraPropertyRequestStackHelper::PushValueRequest(FGuid RequestId, ValueType TargetValue, float TotalDuration, float BlendInDuration, UCurveFloat* BlendInCurve, float BlendOutDuration, UCurveFloat* BlendOutCurve) +{ + if (!RequestId.IsValid() || TotalDuration <= 0.f) + { + UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__ ": Wrong request.")); + return; + } + + UGC_CameraPropertyValueRequest Request; + Request.RequestId = RequestId; + Request.CurrentTime = 0.f; + Request.TargetValue = TargetValue; + Request.TotalDuration = TotalDuration; + + Request.bBlendingIn = BlendInDuration > 0.f; + Request.BlendInDuration = BlendInDuration; + Request.BlendInCurve = BlendInCurve; + Request.BlendInCurrentTime = 0.f; + + Request.bBlendingOut = false; + Request.BlendOutDuration = BlendOutDuration; + Request.BlendOutCurve = BlendOutCurve; + Request.BlendOutCurrentTime = 0.f; + + const int32 NewIndex = FindInactiveRequest(); + check(NewIndex < MAX_uint16); + Requests[NewIndex] = MoveTemp(Request); + +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + NumberActive++; +#endif +} + +template +void UGC_CameraPropertyRequestStackHelper::PopValueRequest(FGuid RequestId) +{ + int32 FoundIndex = -1; + for (int32 Index = 0; Index < Requests.Num(); ++Index) + { + if (Requests[Index].RequestId == RequestId) + { + FoundIndex = Index; + break; + } + } + + if (FoundIndex >= 0) + { + InvalidateRequest(FoundIndex); + } +} + +template +int32 UGC_CameraPropertyRequestStackHelper::FindInactiveRequest() +{ + for (int32 Index = 0; Index < Requests.Num(); ++Index) + { + const UGC_CameraPropertyValueRequest& Request(Requests[Index]); + if (!Request.IsValid()) + { + return Index; + } + } + + return Requests.Emplace(); +} + +template +void UGC_CameraPropertyRequestStackHelper::InvalidateRequest(int32 RequestIndex) +{ + if (RequestIndex >= 0 && RequestIndex < Requests.Num()) + { + Requests[RequestIndex].Invalidate(); +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + --NumberActive; +#endif + } +} + +template +void UGC_CameraPropertyRequestStackHelper::ProcessValue(float DeltaTime, ValueType InValue, FVector ViewLocation, FRotator ViewRotation, ValueType& OutValue) +{ + ValueType FinalValue = InValue; + for (int32 Index = 0; Index < Requests.Num(); ++Index) + { + UGC_CameraPropertyValueRequest& Request = Requests[Index]; + if (!Request.IsValid()) + { + continue; + } + + Request.CurrentTime += DeltaTime; + + // Advance any easing times. + if (Request.bBlendingIn) + { + Request.BlendInCurrentTime += DeltaTime; + } + if (Request.bBlendingOut) + { + Request.BlendOutCurrentTime += DeltaTime; + } + + // Start easing out if we're nearing the end. + if (!Request.bBlendingOut) + { + const float BlendOutStartTime = Request.TotalDuration - Request.BlendOutDuration; + if (Request.CurrentTime > BlendOutStartTime) + { + Request.bBlendingOut = true; + Request.BlendOutCurrentTime = Request.CurrentTime - BlendOutStartTime; + } + } + + // Check if we're done easing in or out. + bool bIsDoneEasingOut = false; + if (Request.bBlendingIn) + { + if (Request.BlendInCurrentTime > Request.BlendInDuration || Request.BlendInDuration == 0.f) + { + Request.bBlendingIn = false; + } + } + if (Request.bBlendingOut) + { + if (Request.BlendOutCurrentTime > Request.BlendOutDuration) + { + bIsDoneEasingOut = true; + } + } + + // Figure out the final easing weight. + const float EasingInT = FMath::Clamp((Request.BlendInCurrentTime / Request.BlendInDuration), 0.f, 1.f); + const float EasingInWeight = Request.bBlendingIn ? + (Request.BlendInCurve ? Request.BlendInCurve->GetFloatValue(EasingInT) : EasingInT) : 1.f; + + const float EasingOutT = FMath::Clamp((1.f - Request.BlendOutCurrentTime / Request.BlendOutDuration), 0.f, 1.f); + const float EasingOutWeight = Request.bBlendingOut ? + (Request.BlendOutCurve ? Request.BlendOutCurve->GetFloatValue(EasingOutT) : EasingOutT) : 1.f; + + const float TotalEasingWeight = FMath::Min(EasingInWeight, EasingOutWeight); + + // We might be done playing or there was a DisableModifier() call with bImmediate=false to let a value request blend out. + if (bIsDoneEasingOut || TotalEasingWeight <= 0.f) + { + InvalidateRequest(Index); + continue; + } + + // Using FinalValue so that having multiple value requests at the same time doesn't cause jumps in values. + FinalValue = FMath::Lerp(FinalValue, Request.TargetValue, TotalEasingWeight); + +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) + if (DebugGetterFunction && DebugGetterFunction() && GEngine) + { + const float TextAlpha = (TotalEasingWeight * static_cast(Index + 1) / static_cast(NumberActive)); + const FLinearColor TextColor = FMath::Lerp(FLinearColor::White, PropertyColor, TextAlpha); + + FString ValueStr = ""; + if constexpr (TIsArithmetic::Value) ValueStr = LexToSanitizedString(FinalValue); + else ValueStr = FinalValue.ToCompactString(); + + GEngine->AddOnScreenDebugMessage(-1, DeltaTime, TextColor.ToFColor(true), + FString::Printf(TEXT("UUGC_%sAnimNotifyRequest: Notify %d - %s"), *PropertyName.ToString(), Index, *ValueStr)); + } +#endif + } + OutValue = FinalValue; +} + +template +void UGC_CameraPropertyRequestStackHelper::OnModifierDisabled(FMinimalViewInfo const& LastPOV, bool bWasImmediate) +{ + for (int32 Index = 0; Index < Requests.Num(); ++Index) + { + if (!Requests[Index].IsValid()) + { + continue; + } + + if (bWasImmediate) + { + InvalidateRequest(Index); + } + else if (!Requests[Index].bBlendingOut) + { + Requests[Index].bBlendingOut = true; + } + } +} diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h new file mode 100644 index 0000000..27b983b --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h @@ -0,0 +1,114 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "UObject/ObjectMacros.h" +#include "UObject/Object.h" +#include "UObject/ScriptMacros.h" +#include "UGC_CameraAnimationModifier.h" +#include "UGC_PlayCameraAnimCallbackProxy.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCameraAnimationPlayDelegate); + +/** Parameter struct for adding new camera animations to UGCCameraAnimationCameraModifier */ +USTRUCT(BlueprintType) +struct FUGCCameraAnimationParams +{ + GENERATED_BODY() + + /** Time scale for playing the new camera animation */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + float PlayRate = 1.f; + + /** Ease-in function type */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + ECameraAnimationEasingType EaseInType = ECameraAnimationEasingType::Linear; + /** Ease-in duration in seconds */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + float EaseInDuration = 0.f; + + /** Ease-out function type */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + ECameraAnimationEasingType EaseOutType = ECameraAnimationEasingType::Linear; + /** Ease-out duration in seconds */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + float EaseOutDuration = 0.f; + + /** How should the camera behave after the animation is over. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera Animation") + ECameraAnimationResetType ResetType = ECameraAnimationResetType::ResetToZero; + + explicit operator FCameraAnimationParams() const; +}; + +UCLASS() +class AURORADEVS_UGC_API UUGC_PlayCameraAnimCallbackProxy : public UObject +{ + GENERATED_UCLASS_BODY() + + // Called when Camera Animation finished playing and wasn't interrupted + UPROPERTY(BlueprintAssignable) + FOnCameraAnimationPlayDelegate OnCompleted; + + // Called when Camera Animation starts blending out and is not interrupted + UPROPERTY(BlueprintAssignable) + FOnCameraAnimationPlayDelegate OnEaseOut; + + // Called when Camera Animation has been interrupted (or failed to play) + UPROPERTY(BlueprintAssignable) + FOnCameraAnimationPlayDelegate OnInterrupted; + + // Called to perform the query internally + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true")) + static UUGC_PlayCameraAnimCallbackProxy* CreateProxyObjectForPlayCameraAnim( + class APlayerCameraManager* InPlayerCameraManager, + TSubclassOf ModifierClass, + class UCameraAnimationSequence* CameraSequence, + FUGCCameraAnimationParams Params, + struct FCameraAnimationHandle& Handle, + bool bInterruptOthers, + bool bDoCollisionChecks); + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true")) + static UUGC_PlayCameraAnimCallbackProxy* CreateProxyObjectForPlayCameraAnimForModifier( + class UUGC_CameraAnimationModifier* CameraAnimationModifier, + class UCameraAnimationSequence* CameraSequence, + FUGCCameraAnimationParams Params, + struct FCameraAnimationHandle& Handle, + bool bInterruptOthers, + bool bDoCollisionChecks); + +protected: + UFUNCTION() + void OnCameraAnimationEasingOut(UCameraAnimationSequence* CameraAnimation); + + UFUNCTION() + void OnCameraAnimationEnded(UCameraAnimationSequence* CameraAnimation, bool bInterrupted); + +private: + TWeakObjectPtr CameraAnimationModifierPtr; + + bool bInterruptedCalledBeforeBlendingOut = false; + + FOnCameraAnimationEaseOutStarted CameraAnimationEasingOutDelegate; + FOnCameraAnimationEnded CameraAnimationEndedDelegate; + + void PlayCameraAnimation( + class APlayerCameraManager* InPlayerCameraManager, + TSubclassOf ModifierClass, + class UCameraAnimationSequence* CameraSequence, + FUGCCameraAnimationParams Params, + struct FCameraAnimationHandle& Handle, + bool bInterruptOthers, + bool bDoCollisionChecks); + + void PlayCameraAnimation( + class UUGC_CameraAnimationModifier* CameraAnimationModifier, + class UCameraAnimationSequence* CameraSequence, + FUGCCameraAnimationParams Params, + struct FCameraAnimationHandle& Handle, + bool bInterruptOthers, + bool bDoCollisionChecks); +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/UGC_PlayerCameraManager.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/UGC_PlayerCameraManager.h new file mode 100644 index 0000000..e2e056a --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Camera/UGC_PlayerCameraManager.h @@ -0,0 +1,326 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Camera/PlayerCameraManager.h" +#include "UGC_PlayerCameraManager.generated.h" + +/** + * + */ +UCLASS() +class AURORADEVS_UGC_API AUGC_PlayerCameraManager : public APlayerCameraManager +{ + GENERATED_BODY() +public: + AUGC_PlayerCameraManager(); + + virtual void InitializeFor(class APlayerController* PC) override; + + /* + * This returns the real camera view, this isn't necessarily the view of the camera attached to your character. + * For example, during camera animations, the camera used is different than the normal gameplay camera. + */ + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager") + void GetRealCameraView(FVector& OutViewLocation, FRotator& OutViewRotation) { OutViewLocation = ViewTarget.POV.Location; OutViewRotation = ViewTarget.POV.Rotation; } + + /* + * Prepare the possession of a new pawn. Doesn't actually possess the pawn. + * APlayerController::Possess needs to be called directly after this function. + * @param NewPawn The new pawn that we intend on possessing. + * @param NewCameraDA The camera data asset that will be pushed after possessing. + *Requires call to PostPosses after the APlayerController::Possess has been called!* + * @param bBlendSpringArmProperties Whether the new pawn's spring arm should prepare to blend from the previous spring arm. + *This doesn't work with SetViewTargetWithBlend!* + *Requires call to PostPosses after the APlayerController::Possess has been called!* + * @param bMatchCameraRotation Whether the new pawn should have the same camera spring arm rotation (aka Control Rotation) or not. + *This doesn't work with SetViewTargetWithBlend!* + *Requires call to PostPosses after the APlayerController::Possess has been called!* + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PrePossess(class APawn* NewPawn, class UUGC_CameraDataAssetBase* NewCameraDA, bool bBlendSpringArmProperties = false, bool bMatchCameraRotation = false); + + /* + * Finush the possession of a new pawn. APlayerController::Possess needs to have been called directly before this function. + * @param bReplaceCurrentCameraDA Whether to push the Camera DA that was passed in the PrePossess function (if any) should + replace the current camera DA or be pushed on top of it. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PostPossess(bool bReplaceCurrentCameraDA = true); + + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void RefreshLevelSequences(); + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager") + bool IsPlayingAnyLevelSequence() const { return NbrActiveLevelSequences > 0 || NbrActivePausedLevelSequences > 0; } + + // Plays a single new camera animation sequence. Any subsequent calls while this animation runs will interrupt the current animation. + // This variation can be used in contexts where async nodes aren't allowd (e.g., AnimNotifies). + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PlayCameraAnimation(class UCameraAnimationSequence* CameraSequence, struct FUGCCameraAnimationParams const& Params, bool bInterruptOthers, bool bDoCollisionChecks); + + /** + * Enables/Disables all camera modifiers ONLY if they inherit from UGC Camera Modifier. + * @param bEnabled - true to enable all UGC camera modifiers, false to disable. + * @param bImmediate - If bEnabled is false: true to disable immediately with no blend out, false (default) to allow blend out + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void ToggleUGCCameraModifiers(bool const bEnabled, bool const bImmediate = true); + + /** + * Enables/Disables all camera modifiers, regardless of whether they inherit from UGC Camera Modifier. + * @param bEnabled - true to enable all camera modifiers, false to disable. + * @param bImmediate - If bEnabled is false: true to disable immediately with no blend out, false (default) to allow blend out + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void ToggleCameraModifiers(bool const bEnabled, bool const bImmediate = true); + + /** + * Enables/Disables all debugging of camera modifiers ONLY if they inherit from UGC Camera Modifier. + * @param bEnabled - true to enable all debugging of UGC camera modifiers, false to disable. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void ToggleAllUGCModifiersDebug(bool const bEnabled); + /** + * + * Enables/Disables all debugging of camera modifiers regardless of whether they inherit from UGC Camera Modifier. + * @param bEnabled - true to enable all debugging of all camera modifiers, false to disable. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void ToggleAllModifiersDebug(bool const bEnabled); + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager") + class USpringArmComponent* GetOwnerSpringArmComponent() const { return CameraArm.Get(); } + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager") + class UUGC_CameraDataAssetBase* GetCurrentCameraDataAsset() const { return CameraDataStack.IsEmpty() ? nullptr : CameraDataStack[CameraDataStack.Num() - 1]; } + + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PushCameraData(class UUGC_CameraDataAssetBase* CameraDA); + + // Pops the most recent Camera DA. + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PopCameraDataHead(); + + // Pops the given Camera DA. If it isn't in stack, it returns false. + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void PopCameraData(class UUGC_CameraDataAssetBase* CameraDA); + + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Manager") + void OnCameraDataStackChanged(class UUGC_CameraDataAssetBase* CameraDA, bool bBlendCameraProperties = true); + + /** Draw a debug camera shape at the real camera's location, which can be different from the character's attached camera. + * For example, camera animations use a different camera then the character's attached camera. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager|Debug") + void DrawRealDebugCamera(float Duration, FLinearColor CameraColor = FLinearColor::Red, float Thickness = 1.f) const; + + /** Draw a debug camera shape at the character's attached camera's location, which can be different from the real camera. + * For example, camera animations use a different camera then the character's attached camera. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager|Debug") + void DrawGameDebugCamera(float Duration, bool bDrawCamera = true, FLinearColor CameraColor = FLinearColor::Blue, bool bDrawSpringArm = true, FLinearColor SpringArmColor = FLinearColor::Blue, float Thickness = 1.f) const; + + /* Draw Spring Arm. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager|Debug") + void DrawDebugSpringArm(FVector const& CameraLocation, float Duration, FLinearColor SpringArmColor = FLinearColor::Blue, float Thickness = 1.f) const; + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "UGC|Camera Manager") + bool IsPlayingAnyCameraAnimation() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + FVector GetOwnerVelocity() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + void ComputeOwnerFloorDist(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, float& OutFloorDistance) const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + bool IsOwnerFalling() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + bool IsOwnerStrafing() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + bool IsOwnerMovingOnGround() const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Manager|Movement") + void ComputeOwnerFloorNormal(float SweepDistance, float CapsuleRadius, bool& bOutFloorExists, FVector& OutFloorNormal) const; + + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + void ComputeOwnerSlopeAngle(float& OutSlopePitchDegrees, float& OutSlopeRollDegrees); + + /* + * Returns value betwen 1 (the character is looking where they're moving) or -1 (looking in the opposite direction they're moving). + * Will return 0 if the character isn't moving. + */ + UFUNCTION(BlueprintPure, Category = "UGC|Camera Modifier|Movement") + float ComputeOwnerLookAndMovementDot(); + + /* + * Only use this when your pawn has multiple spring arm components and you need to use a different one. + * The spring arm is automatically reset to the first found spring arm component on the pawn when we initialize + * the camera manager or when we possess a new pawn. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager") + void SetSpringArmComponent(class USpringArmComponent* Arm) { if (Arm) CameraArm = Arm; } + + /** + * Returns camera modifier for this camera of the given class, if it exists. + * Looks for inherited classes too. If there are multiple modifiers which fit, the first one is returned. + */ + UFUNCTION(BlueprintCallable, Category = "UGC|Camera Manager", meta = (DeterminesOutputType = "ModifierClass")) + UCameraModifier* FindCameraModifierOfClass(TSubclassOf ModifierClass, bool bIncludeInherited); + + FVector GetCameraTurnRate() const { return FVector(YawTurnRate, PitchTurnRate, 0.f); } + + UCameraModifier const* FindCameraModifierOfClass(TSubclassOf ModifierClass, bool bIncludeInherited) const; + + template + T* FindCameraModifierOfType() + { + bool constexpr bIncludeInherited = true; + UCameraModifier* Modifier = FindCameraModifierOfClass(T::StaticClass(), bIncludeInherited); + return Modifier != nullptr ? Cast(Modifier) : nullptr; + } + + template + T const* FindCameraModifierOfType() const + { + bool constexpr bIncludeInherited = true; + UCameraModifier const* Modifier = FindCameraModifierOfClass(T::StaticClass(), bIncludeInherited); + return Modifier != nullptr ? Cast(Modifier) : nullptr; + } +protected: + void PushCameraData_Internal(class UUGC_CameraDataAssetBase* CameraDA, bool bBlendCameraProperties); + + void DoForEachUGCModifier(TFunction const& Function); + + // Breaks when the function returns true. + void DoForEachUGCModifierWithBreak(TFunction const& Function); + + virtual void DisplayDebug(class UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) override; + virtual UCameraModifier* AddNewCameraModifier(TSubclassOf ModifierClass) override; + virtual bool RemoveCameraModifier(UCameraModifier* ModifierToRemove) override; + + virtual void SetViewTarget(AActor* NewViewTarget, FViewTargetTransitionParams TransitionParams) override; + + /** + * Called to give PlayerCameraManager a chance to adjust view rotation updates before they are applied. + * e.g. The base implementation enforces view rotation limits using LimitViewPitch, et al. + * @param DeltaTime - Frame time in seconds. + * @param OutViewRotation - In/out. The view rotation to modify. + * @param OutDeltaRot - In/out. How much the rotation changed this frame. + */ + virtual void ProcessViewRotation(float DeltaTime, FRotator& OutViewRotation, FRotator& OutDeltaRot); + + /** + * Called to give PlayerCameraManager a chance to adjust both the yaw turn rate and pitch turn rate. + * + * @param DeltaTime - Frame time in seconds. + * @param InLocalControlRotation - The difference between the actor rotation and the control rotation. + * @param OutPitchTurnRate - Out. New value of the pitch turn rate (between 0 and 1). + * @param OutYawTurnRate - Out. New value of the yaw turn rate (between 0 and 1). + * @return Return true to prevent subsequent (lower priority) modifiers to further adjust rotation, false otherwise. + */ + virtual void ProcessTurnRate(float DeltaTime, FRotator InLocalControlRotation, float& OutPitchTurnRate, float& OutYawTurnRate); + + virtual void Tick(float DeltaTime) override; + + virtual void LimitViewYaw(FRotator& ViewRotation, float InViewYawMin, float InViewYawMax) override; + + // This updates the internal variables of the UGC Player Camera Manager. Make sure to call the parent function if you override this in BP. + UFUNCTION(BlueprintNativeEvent, Category = "UGC|Camera Manager|Internal") + void UpdateInternalVariables(float DeltaTime); + + // Usually uses the UGC Pawn Interface to fetch the rotation input of the camera (Mouse or Right Thumbstick). Override this if you want to provide your own way of getting the camera rotation input. + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Camera Manager|Internal") + FRotator GetRotationInput() const; + + // Usually uses the UGC Pawn Interface to fetch the movement input of the character (WASD or Left Thumbstick). Override this if you want to provide your own way of getting the camera rotation input. + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "UGC|Camera Manager|Internal") + FVector GetMovementControlInput() const; + + UFUNCTION() + void OnLevelSequenceStarted(); + + UFUNCTION() + void OnLevelSequencePaused(); + + UFUNCTION() + void OnLevelSequenceEnded(); +protected: + friend class UUGC_CameraModifier; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + TArray CameraDataStack; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Camera Manager|Internal") + TObjectPtr OwnerCharacter; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Camera Manager|Internal") + TObjectPtr OwnerPawn; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Camera Manager|Internal") + TObjectPtr CameraArm; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "UGC|Camera Manager|Internal") + TObjectPtr MovementComponent; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + TArray LevelSequences; + + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "UGC|Camera Manager|Angle Constraints", meta = (UIMin = 0.f, UIMax = 1.f, ClampMin = 0.f, ClampMax = 1.f)) + float PitchTurnRate = 1.f; + + UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "UGC|Camera Manager|Angle Constraints", meta = (UIMin = 0.f, UIMax = 1.f, ClampMin = 0.f, ClampMax = 1.f)) + float YawTurnRate = 1.f; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float AspectRatio = 0.f; + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float VerticalFOV = 0.f; + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float HorizontalFOV = 0.f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + bool bHasMovementInput = false; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + FVector MovementInput = FVector::ZeroVector; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float TimeSinceMovementInput = 0.f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + bool bHasRotationInput = false; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + FRotator RotationInput = FRotator::ZeroRotator; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float TimeSinceRotationInput = 0.f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + float OriginalArmLength = 0.f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + TArray UGCModifiersList; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UGC|Camera Manager|Internal") + TArray UGCAddOnModifiersList; + + UPROPERTY(Transient) + int NbrActiveLevelSequences = 0; + + UPROPERTY(Transient) + int NbrActivePausedLevelSequences = 0; + + struct PossessPayload + { + TObjectPtr PendingCameraDA = nullptr; + FRotator PendingControlRotation = FRotator::ZeroRotator; + bool bBlendCameraProperties = false; + bool bMatchCameraRotation = false; + } PendingPossessPayload; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_CameraTurnRateModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_CameraTurnRateModifier.h new file mode 100644 index 0000000..91daee7 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_CameraTurnRateModifier.h @@ -0,0 +1,21 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "InputModifiers.h" +#include "UGC_CameraTurnRateModifier.generated.h" + +/** + * + */ +UCLASS() +class AURORADEVS_UGC_API UUGC_CameraTurnRateModifier : public UInputModifier +{ + GENERATED_BODY() +public: + virtual FInputActionValue ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime) override; + +private: + TObjectPtr PlayerCameraManager; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_InputAccelerationModifier.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_InputAccelerationModifier.h new file mode 100644 index 0000000..c31aea3 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Input/UGC_InputAccelerationModifier.h @@ -0,0 +1,30 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "InputModifiers.h" +#include "UGC_InputAccelerationModifier.generated.h" + +/** + * Apply an acceleration curve to your input so that it accelerates as time goes by. The given curve should be time-normalized, i.e., between 0 and 1. + */ +UCLASS(ClassGroup = "UGC Input Modifiers") +class AURORADEVS_UGC_API UUGC_InputAccelerationModifier : public UInputModifier +{ + GENERATED_BODY() +public: + virtual FInputActionValue ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime) override; + +public: + // The time it takes to reach full speed. + UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = Settings, Config) + float AccelerationTime = 1.f; + + // Apply an acceleration curve to your input so that it accelerates as time goes by. The given curve should be time-normalized, i.e., between 0 and 1. + UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = Settings, meta = (DisplayThumbnail = "false")) + TObjectPtr AccelerationCurve; + +private: + float Timer = 0.f; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGC/Public/Pawn/UGC_PawnInterface.h b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Pawn/UGC_PawnInterface.h new file mode 100644 index 0000000..55e2c0b --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGC/Public/Pawn/UGC_PawnInterface.h @@ -0,0 +1,60 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "UGC_PawnInterface.generated.h" + +// This class does not need to be modified. +UINTERFACE(MinimalAPI) +class UUGC_PawnInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Pawn Interface used to get the movement/rotation input values. + */ +class AURORADEVS_UGC_API IUGC_PawnInterface +{ + GENERATED_BODY() + +public: + // Get the value of the camera rotation input (usually the right thumbstick or the mouse). + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "UGC|Camera Interface") + FRotator GetRotationInput() const; + + // Get the value of the movement input (usually WASD or the left thumbstick). + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "UGC|Camera Interface") + FVector GetMovementInput() const; +}; + +// This class does not need to be modified. +UINTERFACE(MinimalAPI) +class UUGC_PawnMovementInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * This interface should only be used with player classes NOT using the default Unreal Movement Component or components inheriting from it. + * This interface can be used to query important movement properties (velocity, falling; etc.) from your custom movement components. + */ +class AURORADEVS_UGC_API IUGC_PawnMovementInterface +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "UGC|Movement Interface") + FVector GetOwnerVelocity() const; + + UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "UGC|Movement Interface") + bool IsOwnerFalling() const; + + UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "UGC|Movement Interface") + bool IsOwnerStrafing() const; + + UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "UGC|Movement Interface") + bool IsOwnerMovingOnGround() const; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGCEditor/AuroraDevs_UGCEditor.Build.cs b/Plugins/UGC/Source/AuroraDevs_UGCEditor/AuroraDevs_UGCEditor.Build.cs new file mode 100644 index 0000000..978e43c --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGCEditor/AuroraDevs_UGCEditor.Build.cs @@ -0,0 +1,27 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +using UnrealBuildTool; + +public class AuroraDevs_UGCEditor : ModuleRules +{ + public AuroraDevs_UGCEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + ShadowVariableWarningLevel = WarningLevel.Error; + bUseUnity = false; + IWYUSupport = IWYUSupport.Full; + PrecompileForTargets = PrecompileTargetsType.Any; + + PrivateIncludePaths.Add(ModuleDirectory); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "BlueprintGraph", + "AuroraDevs_UGC" + }); + } +} diff --git a/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/AuroraDevs_UGCEditor.cpp b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/AuroraDevs_UGCEditor.cpp new file mode 100644 index 0000000..b9938db --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/AuroraDevs_UGCEditor.cpp @@ -0,0 +1,18 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "AuroraDevs_UGCEditor.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FAuroraDevs_UGCEditorModule" + +void FAuroraDevs_UGCEditorModule::StartupModule() +{ +} + +void FAuroraDevs_UGCEditorModule::ShutdownModule() +{ +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FAuroraDevs_UGCEditorModule, AuroraDevs_UGCEditor) \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/UGC_EditorNodes.cpp b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/UGC_EditorNodes.cpp new file mode 100644 index 0000000..5fc201c --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Private/UGC_EditorNodes.cpp @@ -0,0 +1,51 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#include "UGC_EditorNodes.h" + +#include "AuroraDevs_UGC/Public/Camera/Modifiers/UGC_PlayCameraAnimCallbackProxy.h" + +UUGC_PlayCameraAnimation::UUGC_PlayCameraAnimation(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UUGC_PlayCameraAnimCallbackProxy, CreateProxyObjectForPlayCameraAnim); + ProxyFactoryClass = UUGC_PlayCameraAnimCallbackProxy::StaticClass(); + ProxyClass = UUGC_PlayCameraAnimCallbackProxy::StaticClass(); +} + +FText UUGC_PlayCameraAnimation::GetTooltipText() const +{ + return FText::AsCultureInvariant("Plays a single new camera animation sequence. Any subsequent calls while this animation runs will interrupt the current animation. This will try to look for the UGC Animation Camera Modifier through the passed in class (exact match only)."); +} + +FText UUGC_PlayCameraAnimation::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return FText::AsCultureInvariant("Play Camera Animation From Modifier Class"); +} + +FText UUGC_PlayCameraAnimation::GetMenuCategory() const +{ + return FText::AsCultureInvariant("UGC Camera Animations"); +} + +UUGC_PlayCameraAnimationWithModifier::UUGC_PlayCameraAnimationWithModifier(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UUGC_PlayCameraAnimCallbackProxy, CreateProxyObjectForPlayCameraAnimForModifier); + ProxyFactoryClass = UUGC_PlayCameraAnimCallbackProxy::StaticClass(); + ProxyClass = UUGC_PlayCameraAnimCallbackProxy::StaticClass(); +} + +FText UUGC_PlayCameraAnimationWithModifier::GetTooltipText() const +{ + return FText::AsCultureInvariant("Plays a single new camera animation sequence. Any subsequent calls while this animation runs will interrupt the current animation."); +} + +FText UUGC_PlayCameraAnimationWithModifier::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return FText::AsCultureInvariant("Play Camera Animation"); +} + +FText UUGC_PlayCameraAnimationWithModifier::GetMenuCategory() const +{ + return FText::AsCultureInvariant("UGC Camera Animations"); +} \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/AuroraDevs_UGCEditor.h b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/AuroraDevs_UGCEditor.h new file mode 100644 index 0000000..c0a6a18 --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/AuroraDevs_UGCEditor.h @@ -0,0 +1,14 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FAuroraDevs_UGCEditorModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; \ No newline at end of file diff --git a/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/UGC_EditorNodes.h b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/UGC_EditorNodes.h new file mode 100644 index 0000000..572bcdc --- /dev/null +++ b/Plugins/UGC/Source/AuroraDevs_UGCEditor/Public/UGC_EditorNodes.h @@ -0,0 +1,28 @@ +// Copyright(c) Aurora Devs 2022-2025. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "K2Node_BaseAsyncTask.h" + +#include "UGC_EditorNodes.generated.h" + +UCLASS(ClassGroup = "UGC Camera Animations") +class UUGC_PlayCameraAnimation : public UK2Node_BaseAsyncTask +{ + GENERATED_UCLASS_BODY() + + virtual FText GetTooltipText() const override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual FText GetMenuCategory() const override; +}; + +UCLASS(ClassGroup = "UGC Camera Animations") +class UUGC_PlayCameraAnimationWithModifier : public UK2Node_BaseAsyncTask +{ + GENERATED_UCLASS_BODY() + + virtual FText GetTooltipText() const override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual FText GetMenuCategory() const override; +}; \ No newline at end of file diff --git a/Source/PHY.Target.cs b/Source/PHY.Target.cs new file mode 100644 index 0000000..ab50a50 --- /dev/null +++ b/Source/PHY.Target.cs @@ -0,0 +1,21 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class PHYTarget : TargetRules +{ + public PHYTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V6; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7; + ExtraModuleNames.Add("PHY"); + RegisterModulesCreatedByRider(); + } + + private void RegisterModulesCreatedByRider() + { + ExtraModuleNames.AddRange(new string[] { "PHYInventory" }); + } +} diff --git a/Source/PHY/PHY.Build.cs b/Source/PHY/PHY.Build.cs new file mode 100644 index 0000000..691fb63 --- /dev/null +++ b/Source/PHY/PHY.Build.cs @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class PHY : ModuleRules +{ + public PHY(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + CppStandard = CppStandardVersion.Cpp20; + + PrivateIncludePaths.AddRange(new string[] + { + System.IO.Path.Combine(ModuleDirectory, "Private"), + }); + + PublicDependencyModuleNames.AddRange(new string[] { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "EnhancedInput" + }); + + PrivateDependencyModuleNames.AddRange(new string[] + { + "GameplayAbilities", + "GameplayTags", + "GameplayTasks", + "GenericInputSystem", + "GenericUISystem", + "GenericInventorySystem", + "GenericMovementSystem", + "Slate", + "SlateCore", + "AuroraDevs_UGC", + "IKRig", + "NetCore", + "UMG", + "CommonUI" + }); + + // 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/PHY/PHY.cpp b/Source/PHY/PHY.cpp new file mode 100644 index 0000000..c0617c4 --- /dev/null +++ b/Source/PHY/PHY.cpp @@ -0,0 +1,6 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "PHY.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, PHY, "PHY" ); diff --git a/Source/PHY/PHY.h b/Source/PHY/PHY.h new file mode 100644 index 0000000..677c8e2 --- /dev/null +++ b/Source/PHY/PHY.h @@ -0,0 +1,6 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + diff --git a/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.cpp b/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.cpp new file mode 100644 index 0000000..7666d77 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.cpp @@ -0,0 +1,246 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +// (If your include paths don't pick up the module Public folder, use the short include below.) +// #include "PHYAttributeSet.h" + +#include "GameplayEffectExtension.h" +#include "Net/UnrealNetwork.h" + +UPHYAttributeSet::UPHYAttributeSet() +{ +} + +void UPHYAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) +{ + Super::PreAttributeChange(Attribute, NewValue); + + // Basic clamps + if (Attribute == GetHealthAttribute()) + { + NewValue = ClampNonNegative(NewValue); + NewValue = FMath::Min(NewValue, GetMaxHealth()); + } + else if (Attribute == GetMaxHealthAttribute()) + { + NewValue = ClampNonNegative(NewValue); + } + else if (Attribute == GetInnerPowerAttribute()) + { + NewValue = ClampNonNegative(NewValue); + NewValue = FMath::Min(NewValue, GetMaxInnerPower()); + } + else if (Attribute == GetMaxInnerPowerAttribute()) + { + NewValue = ClampNonNegative(NewValue); + } + else if (Attribute == GetMoveSpeedAttribute()) + { + // Allow designers to decide exact caps later; keep reasonable defaults. + NewValue = FMath::Clamp(NewValue, 0.f, 2000.f); + } + else if (Attribute == GetCritChanceAttribute() || Attribute == GetDodgeChanceAttribute() || Attribute == GetHitChanceAttribute() + || Attribute == GetParryChanceAttribute() || Attribute == GetCounterChanceAttribute() || Attribute == GetArmorPenetrationAttribute() + || Attribute == GetDamageReductionAttribute() || Attribute == GetLifeStealAttribute()) + { + NewValue = FMath::Clamp(NewValue, 0.f, 1.f); + } + else if (Attribute == GetCritDamageAttribute()) + { + // Crit damage multiplier should be >= 1.0 + NewValue = FMath::Max(1.f, NewValue); + } +} + +void UPHYAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + const FGameplayAttribute& Attribute = Data.EvaluatedData.Attribute; + + if (Attribute == GetHealthAttribute()) + { + SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth())); + } + else if (Attribute == GetMaxHealthAttribute()) + { + // When MaxHealth is reduced, clamp Health to new MaxHealth. + SetMaxHealth(ClampNonNegative(GetMaxHealth())); + SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth())); + } + else if (Attribute == GetInnerPowerAttribute()) + { + SetInnerPower(FMath::Clamp(GetInnerPower(), 0.f, GetMaxInnerPower())); + } + else if (Attribute == GetMaxInnerPowerAttribute()) + { + SetMaxInnerPower(ClampNonNegative(GetMaxInnerPower())); + SetInnerPower(FMath::Clamp(GetInnerPower(), 0.f, GetMaxInnerPower())); + } + else if (Attribute == GetStrengthAttribute()) { SetStrength(ClampNonNegative(GetStrength())); } + else if (Attribute == GetConstitutionAttribute()) { SetConstitution(ClampNonNegative(GetConstitution())); } + else if (Attribute == GetInnerBreathAttribute()) { SetInnerBreath(ClampNonNegative(GetInnerBreath())); } + else if (Attribute == GetAgilityAttribute()) { SetAgility(ClampNonNegative(GetAgility())); } + else if (Attribute == GetPhysicalAttackAttribute()){ SetPhysicalAttack(ClampNonNegative(GetPhysicalAttack())); } + else if (Attribute == GetPhysicalDefenseAttribute()){ SetPhysicalDefense(ClampNonNegative(GetPhysicalDefense())); } + else if (Attribute == GetMoveSpeedAttribute()) { SetMoveSpeed(FMath::Clamp(GetMoveSpeed(), 0.f, 2000.f)); } + else if (Attribute == GetTenacityAttribute()) { SetTenacity(ClampNonNegative(GetTenacity())); } + else if (Attribute == GetCritChanceAttribute()) { SetCritChance(FMath::Clamp(GetCritChance(), 0.f, 1.f)); } + else if (Attribute == GetCritDamageAttribute()) { SetCritDamage(FMath::Max(1.f, GetCritDamage())); } + else if (Attribute == GetDodgeChanceAttribute()) { SetDodgeChance(FMath::Clamp(GetDodgeChance(), 0.f, 1.f)); } + else if (Attribute == GetHitChanceAttribute()) { SetHitChance(FMath::Clamp(GetHitChance(), 0.f, 1.f)); } + else if (Attribute == GetParryChanceAttribute()) { SetParryChance(FMath::Clamp(GetParryChance(), 0.f, 1.f)); } + else if (Attribute == GetCounterChanceAttribute()){ SetCounterChance(FMath::Clamp(GetCounterChance(), 0.f, 1.f)); } + else if (Attribute == GetArmorPenetrationAttribute()) { SetArmorPenetration(FMath::Clamp(GetArmorPenetration(), 0.f, 1.f)); } + else if (Attribute == GetDamageReductionAttribute()) { SetDamageReduction(FMath::Clamp(GetDamageReduction(), 0.f, 1.f)); } + else if (Attribute == GetLifeStealAttribute()) { SetLifeSteal(FMath::Clamp(GetLifeSteal(), 0.f, 1.f)); } + else if (Attribute == GetHealthRegenRateAttribute()) { SetHealthRegenRate(ClampNonNegative(GetHealthRegenRate())); } + else if (Attribute == GetInnerPowerRegenRateAttribute()) { SetInnerPowerRegenRate(ClampNonNegative(GetInnerPowerRegenRate())); } +} + +void UPHYAttributeSet::OnRep_Strength(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, Strength, OldValue); +} + +void UPHYAttributeSet::OnRep_Constitution(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, Constitution, OldValue); +} + +void UPHYAttributeSet::OnRep_InnerBreath(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, InnerBreath, OldValue); +} + +void UPHYAttributeSet::OnRep_Agility(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, Agility, OldValue); +} + +void UPHYAttributeSet::OnRep_Health(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, Health, OldValue); +} + +void UPHYAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, MaxHealth, OldValue); +} + +void UPHYAttributeSet::OnRep_MoveSpeed(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, MoveSpeed, OldValue); +} + +void UPHYAttributeSet::OnRep_PhysicalAttack(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, PhysicalAttack, OldValue); +} + +void UPHYAttributeSet::OnRep_PhysicalDefense(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, PhysicalDefense, OldValue); +} + +void UPHYAttributeSet::OnRep_Tenacity(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, Tenacity, OldValue); +} + +void UPHYAttributeSet::OnRep_CritChance(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, CritChance, OldValue); +} + +void UPHYAttributeSet::OnRep_CritDamage(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, CritDamage, OldValue); +} + +void UPHYAttributeSet::OnRep_DodgeChance(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, DodgeChance, OldValue); +} + +void UPHYAttributeSet::OnRep_HitChance(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, HitChance, OldValue); +} + +void UPHYAttributeSet::OnRep_ParryChance(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, ParryChance, OldValue); +} + +void UPHYAttributeSet::OnRep_CounterChance(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, CounterChance, OldValue); +} + +void UPHYAttributeSet::OnRep_ArmorPenetration(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, ArmorPenetration, OldValue); +} + +void UPHYAttributeSet::OnRep_DamageReduction(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, DamageReduction, OldValue); +} + +void UPHYAttributeSet::OnRep_LifeSteal(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, LifeSteal, OldValue); +} + +void UPHYAttributeSet::OnRep_HealthRegenRate(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, HealthRegenRate, OldValue); +} + +void UPHYAttributeSet::OnRep_InnerPower(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, InnerPower, OldValue); +} + +void UPHYAttributeSet::OnRep_MaxInnerPower(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, MaxInnerPower, OldValue); +} + +void UPHYAttributeSet::OnRep_InnerPowerRegenRate(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UPHYAttributeSet, InnerPowerRegenRate, OldValue); +} + +void UPHYAttributeSet::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, Strength, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, Constitution, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, InnerBreath, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, Agility, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, Health, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, InnerPower, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, MaxInnerPower, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, MoveSpeed, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, PhysicalAttack, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, PhysicalDefense, COND_None, REPNOTIFY_Always); + + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, Tenacity, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, CritChance, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, CritDamage, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, DodgeChance, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, HitChance, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, ParryChance, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, CounterChance, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, ArmorPenetration, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, DamageReduction, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, LifeSteal, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, HealthRegenRate, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UPHYAttributeSet, InnerPowerRegenRate, COND_None, REPNOTIFY_Always); +} diff --git a/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.h b/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.h new file mode 100644 index 0000000..03ed7d8 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Attributes/PHYAttributeSet.h @@ -0,0 +1,163 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "AbilitySystemComponent.h" +#include "PHYAttributeSet.generated.h" + +#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ + GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ + GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ + GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ + GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) + +UCLASS() +class PHY_API UPHYAttributeSet : public UAttributeSet +{ + GENERATED_BODY() + +public: + UPHYAttributeSet(); + + virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Primary", ReplicatedUsing = OnRep_Strength) + FGameplayAttributeData Strength; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, Strength) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Primary", ReplicatedUsing = OnRep_Constitution) + FGameplayAttributeData Constitution; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, Constitution) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Primary", ReplicatedUsing = OnRep_InnerBreath) + FGameplayAttributeData InnerBreath; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, InnerBreath) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Primary", ReplicatedUsing = OnRep_Agility) + FGameplayAttributeData Agility; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, Agility) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Vitals", ReplicatedUsing = OnRep_Health) + FGameplayAttributeData Health; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, Health) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Vitals", ReplicatedUsing = OnRep_MaxHealth) + FGameplayAttributeData MaxHealth; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, MaxHealth) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Derived", ReplicatedUsing = OnRep_MoveSpeed) + FGameplayAttributeData MoveSpeed; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, MoveSpeed) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Derived", ReplicatedUsing = OnRep_PhysicalAttack) + FGameplayAttributeData PhysicalAttack; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, PhysicalAttack) + + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Derived", ReplicatedUsing = OnRep_PhysicalDefense) + FGameplayAttributeData PhysicalDefense; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, PhysicalDefense) + + /** Secondary - 韧性(负面效果持续缩短/抵抗) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_Tenacity) + FGameplayAttributeData Tenacity; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, Tenacity) + + /** Secondary - 暴击率(0-1) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_CritChance) + FGameplayAttributeData CritChance; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, CritChance) + + /** Secondary - 暴击伤害倍率(例如 1.5 表示 +50%) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_CritDamage) + FGameplayAttributeData CritDamage; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, CritDamage) + + /** Secondary - 闪避率(0-1) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_DodgeChance) + FGameplayAttributeData DodgeChance; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, DodgeChance) + + /** Secondary - 命中(可做命中率或命中值) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_HitChance) + FGameplayAttributeData HitChance; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, HitChance) + + /** Secondary - 招架率(0-1) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_ParryChance) + FGameplayAttributeData ParryChance; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, ParryChance) + + /** Secondary - 反击几率(0-1),一般在成功闪避/招架后判定 */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_CounterChance) + FGameplayAttributeData CounterChance; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, CounterChance) + + /** Secondary - 穿甲(百分比 0-1 或值,先按 0-1 做) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_ArmorPenetration) + FGameplayAttributeData ArmorPenetration; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, ArmorPenetration) + + /** Secondary - 免伤(0-1) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_DamageReduction) + FGameplayAttributeData DamageReduction; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, DamageReduction) + + /** Secondary - 吸血(0-1) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_LifeSteal) + FGameplayAttributeData LifeSteal; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, LifeSteal) + + /** Secondary - 生命回复/秒 */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_HealthRegenRate) + FGameplayAttributeData HealthRegenRate; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, HealthRegenRate) + + /** Resource - 内力(当前) */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Resource", ReplicatedUsing = OnRep_InnerPower) + FGameplayAttributeData InnerPower; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, InnerPower) + + /** Resource - 内力上限 */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Resource", ReplicatedUsing = OnRep_MaxInnerPower) + FGameplayAttributeData MaxInnerPower; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, MaxInnerPower) + + /** Secondary - 内力回复/秒 */ + UPROPERTY(BlueprintReadOnly, Category = "Attributes|Secondary", ReplicatedUsing = OnRep_InnerPowerRegenRate) + FGameplayAttributeData InnerPowerRegenRate; + ATTRIBUTE_ACCESSORS(UPHYAttributeSet, InnerPowerRegenRate) + +protected: + UFUNCTION() void OnRep_Strength(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_Constitution(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_InnerBreath(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_Agility(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_Health(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_MaxHealth(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_MoveSpeed(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_PhysicalAttack(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_PhysicalDefense(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_Tenacity(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_CritChance(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_CritDamage(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_DodgeChance(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_HitChance(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_ParryChance(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_CounterChance(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_ArmorPenetration(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_DamageReduction(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_LifeSteal(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_HealthRegenRate(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_InnerPower(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_MaxInnerPower(const FGameplayAttributeData& OldValue); + UFUNCTION() void OnRep_InnerPowerRegenRate(const FGameplayAttributeData& OldValue); + + static float ClampNonNegative(float Value) { return FMath::Max(0.f, Value); } +}; + +#undef ATTRIBUTE_ACCESSORS + diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.cpp b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.cpp new file mode 100644 index 0000000..cf7a277 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.cpp @@ -0,0 +1,191 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +#include "AbilitySystem/MMC/PHY_MMC_MaxHealth.h" +#include "AbilitySystem/MMC/PHY_MMC_MoveSpeed.h" +#include "AbilitySystem/MMC/PHY_MMC_PhysicalAttack.h" +#include "AbilitySystem/MMC/PHY_MMC_PhysicalDefense.h" +#include "AbilitySystem/MMC/PHY_MMC_MaxInnerPower.h" +#include "AbilitySystem/MMC/PHY_MMC_Tenacity.h" +#include "AbilitySystem/MMC/PHY_MMC_CritChance.h" +#include "AbilitySystem/MMC/PHY_MMC_CritDamage.h" +#include "AbilitySystem/MMC/PHY_MMC_DodgeChance.h" +#include "AbilitySystem/MMC/PHY_MMC_HitChance.h" +#include "AbilitySystem/MMC/PHY_MMC_ParryChance.h" +#include "AbilitySystem/MMC/PHY_MMC_CounterChance.h" +#include "AbilitySystem/MMC/PHY_MMC_DamageReduction.h" +#include "AbilitySystem/MMC/PHY_MMC_HealthRegenRate.h" +#include "AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.h" + +UPHYGE_DerivedAttributes::UPHYGE_DerivedAttributes() +{ + DurationPolicy = EGameplayEffectDurationType::Infinite; + + // MaxHealth + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetMaxHealthAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_MaxHealth::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // MoveSpeed + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetMoveSpeedAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_MoveSpeed::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // PhysicalAttack + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetPhysicalAttackAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_PhysicalAttack::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // PhysicalDefense + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetPhysicalDefenseAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_PhysicalDefense::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // MaxInnerPower + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetMaxInnerPowerAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_MaxInnerPower::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // Tenacity + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetTenacityAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_Tenacity::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // CritChance + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetCritChanceAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_CritChance::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // CritDamage + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetCritDamageAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_CritDamage::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // DodgeChance + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetDodgeChanceAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_DodgeChance::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // HitChance + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetHitChanceAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_HitChance::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // ParryChance + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetParryChanceAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_ParryChance::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // CounterChance + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetCounterChanceAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_CounterChance::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // DamageReduction + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetDamageReductionAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_DamageReduction::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // HealthRegenRate + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetHealthRegenRateAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_HealthRegenRate::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } + + // InnerPowerRegenRate + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetInnerPowerRegenRateAttribute(); + Info.ModifierOp = EGameplayModOp::Override; + FCustomCalculationBasedFloat Calc; + Calc.CalculationClassMagnitude = UPHY_MMC_InnerPowerRegenRate::StaticClass(); + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(Calc); + Modifiers.Add(Info); + } +} + diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.h b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.h new file mode 100644 index 0000000..431c252 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_DerivedAttributes.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffect.h" +#include "PHYGE_DerivedAttributes.generated.h" + +/** + * Infinite GE that drives derived attributes via MMCs. + * + * Applied once on spawn/login; re-apply if you need to refresh snapshot-based MMCs. + */ +UCLASS() +class PHY_API UPHYGE_DerivedAttributes : public UGameplayEffect +{ + GENERATED_BODY() + +public: + UPHYGE_DerivedAttributes(); +}; + diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.cpp b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.cpp new file mode 100644 index 0000000..8d9fb12 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/Effects/PHYGE_InitPrimary.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +#include "GameplayTags/InitAttributeTags.h" + +UPHYGE_InitPrimary::UPHYGE_InitPrimary() +{ + DurationPolicy = EGameplayEffectDurationType::Instant; + + // Use SetByCaller so one GE works for every class. + auto AddSetByCaller = [this](FGameplayAttribute Attr, FGameplayTag DataTag) + { + FGameplayModifierInfo Info; + Info.Attribute = Attr; + Info.ModifierOp = EGameplayModOp::Override; + FSetByCallerFloat SetByCaller; + SetByCaller.DataTag = DataTag; + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller); + Modifiers.Add(Info); + }; + + AddSetByCaller(UPHYAttributeSet::GetStrengthAttribute(), InitAttributeTags::Tag__Data_Init_Primary_Strength); + AddSetByCaller(UPHYAttributeSet::GetConstitutionAttribute(), InitAttributeTags::Tag__Data_Init_Primary_Constitution); + AddSetByCaller(UPHYAttributeSet::GetInnerBreathAttribute(), InitAttributeTags::Tag__Data_Init_Primary_InnerBreath); + AddSetByCaller(UPHYAttributeSet::GetAgilityAttribute(), InitAttributeTags::Tag__Data_Init_Primary_Agility); + + // Also initialize current Health to MaxHealth after derived GE runs. We'll set Health in code post-apply. +} diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.h b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.h new file mode 100644 index 0000000..37c1e30 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_InitPrimary.h @@ -0,0 +1,18 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffect.h" +#include "PHYGE_InitPrimary.generated.h" + +/** Instant GE that sets initial primary stats (四维). */ +UCLASS() +class PHY_API UPHYGE_InitPrimary : public UGameplayEffect +{ + GENERATED_BODY() + +public: + UPHYGE_InitPrimary(); +}; + diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.cpp b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.cpp new file mode 100644 index 0000000..8fb17ea --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.cpp @@ -0,0 +1,33 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/Effects/PHYGE_RegenTick.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +#include "GameplayTags/RegenTags.h" + +UPHYGE_RegenTick::UPHYGE_RegenTick() +{ + DurationPolicy = EGameplayEffectDurationType::Instant; + + // Add Health each tick + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetHealthAttribute(); + Info.ModifierOp = EGameplayModOp::Additive; + FSetByCallerFloat SBC; + SBC.DataTag = RegenTags::Tag__Data_Regen_Health; + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(SBC); + Modifiers.Add(Info); + } + + // Add InnerPower each tick + { + FGameplayModifierInfo Info; + Info.Attribute = UPHYAttributeSet::GetInnerPowerAttribute(); + Info.ModifierOp = EGameplayModOp::Additive; + FSetByCallerFloat SBC; + SBC.DataTag = RegenTags::Tag__Data_Regen_InnerPower; + Info.ModifierMagnitude = FGameplayEffectModifierMagnitude(SBC); + Modifiers.Add(Info); + } +} diff --git a/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.h b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.h new file mode 100644 index 0000000..06a11f4 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/Effects/PHYGE_RegenTick.h @@ -0,0 +1,18 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayEffect.h" +#include "PHYGE_RegenTick.generated.h" + +/** Instant GE applied periodically (code-driven) to regen Health and InnerPower. */ +UCLASS() +class PHY_API UPHYGE_RegenTick : public UGameplayEffect +{ + GENERATED_BODY() + +public: + UPHYGE_RegenTick(); +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.cpp new file mode 100644 index 0000000..4956728 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_CounterChance.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_CounterChance::UPHY_MMC_CounterChance() +{ + AgilityDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetAgilityAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(AgilityDef); +} + +float UPHY_MMC_CounterChance::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Agility = 0.f; + GetCapturedAttributeMagnitude(AgilityDef, Spec, Params, Agility); + Agility = FMath::Max(0.f, Agility); + + constexpr float Base = 0.1f; + constexpr float K = 0.001f; + return FMath::Clamp(Base + Agility * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.h new file mode 100644 index 0000000..430fd9b --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CounterChance.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_CounterChance.generated.h" + +/** CounterChance = Base(0.1) + Agility * 0.001 */ +UCLASS() +class PHY_API UPHY_MMC_CounterChance : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_CounterChance(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition AgilityDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.cpp new file mode 100644 index 0000000..ff54926 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_CritChance.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_CritChance::UPHY_MMC_CritChance() +{ + AgilityDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetAgilityAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(AgilityDef); +} + +float UPHY_MMC_CritChance::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Agility = 0.f; + GetCapturedAttributeMagnitude(AgilityDef, Spec, Params, Agility); + Agility = FMath::Max(0.f, Agility); + + constexpr float Base = 0.05f; + constexpr float K = 0.002f; + return FMath::Clamp(Base + Agility * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.h new file mode 100644 index 0000000..e3aa752 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritChance.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_CritChance.generated.h" + +/** CritChance = Base(0.05) + Agility * 0.002 */ +UCLASS() +class PHY_API UPHY_MMC_CritChance : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_CritChance(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition AgilityDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.cpp new file mode 100644 index 0000000..b87e204 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_CritDamage.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_CritDamage::UPHY_MMC_CritDamage() +{ + StrengthDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetStrengthAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(StrengthDef); +} + +float UPHY_MMC_CritDamage::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Strength = 0.f; + GetCapturedAttributeMagnitude(StrengthDef, Spec, Params, Strength); + Strength = FMath::Max(0.f, Strength); + + constexpr float Base = 1.5f; + constexpr float K = 0.005f; + return FMath::Max(1.f, Base + Strength * K); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.h new file mode 100644 index 0000000..cced802 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_CritDamage.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_CritDamage.generated.h" + +/** CritDamage = Base(1.5) + Strength * 0.005 */ +UCLASS() +class PHY_API UPHY_MMC_CritDamage : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_CritDamage(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition StrengthDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.cpp new file mode 100644 index 0000000..3a75763 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_DamageReduction.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_DamageReduction::UPHY_MMC_DamageReduction() +{ + ConstitutionDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetConstitutionAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(ConstitutionDef); +} + +float UPHY_MMC_DamageReduction::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Constitution = 0.f; + GetCapturedAttributeMagnitude(ConstitutionDef, Spec, Params, Constitution); + Constitution = FMath::Max(0.f, Constitution); + + constexpr float Base = 0.f; + constexpr float K = 0.001f; + return FMath::Clamp(Base + Constitution * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.h new file mode 100644 index 0000000..273da39 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DamageReduction.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_DamageReduction.generated.h" + +/** DamageReduction = Base(0) + Constitution * 0.001 */ +UCLASS() +class PHY_API UPHY_MMC_DamageReduction : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_DamageReduction(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition ConstitutionDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.cpp new file mode 100644 index 0000000..702b76a --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_DodgeChance.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_DodgeChance::UPHY_MMC_DodgeChance() +{ + AgilityDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetAgilityAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(AgilityDef); +} + +float UPHY_MMC_DodgeChance::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Agility = 0.f; + GetCapturedAttributeMagnitude(AgilityDef, Spec, Params, Agility); + Agility = FMath::Max(0.f, Agility); + + constexpr float Base = 0.02f; + constexpr float K = 0.002f; + return FMath::Clamp(Base + Agility * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.h new file mode 100644 index 0000000..acb21d3 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_DodgeChance.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_DodgeChance.generated.h" + +/** DodgeChance = Base(0.02) + Agility * 0.002 */ +UCLASS() +class PHY_API UPHY_MMC_DodgeChance : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_DodgeChance(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition AgilityDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.cpp new file mode 100644 index 0000000..5094ad2 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_HealthRegenRate.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_HealthRegenRate::UPHY_MMC_HealthRegenRate() +{ + ConstitutionDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetConstitutionAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(ConstitutionDef); +} + +float UPHY_MMC_HealthRegenRate::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Constitution = 0.f; + GetCapturedAttributeMagnitude(ConstitutionDef, Spec, Params, Constitution); + Constitution = FMath::Max(0.f, Constitution); + + constexpr float K = 0.1f; + return Constitution * K; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.h new file mode 100644 index 0000000..e76bb13 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HealthRegenRate.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_HealthRegenRate.generated.h" + +/** HealthRegenRate = Constitution * 0.1 */ +UCLASS() +class PHY_API UPHY_MMC_HealthRegenRate : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_HealthRegenRate(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition ConstitutionDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.cpp new file mode 100644 index 0000000..adc7587 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_HitChance.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_HitChance::UPHY_MMC_HitChance() +{ + AgilityDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetAgilityAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(AgilityDef); +} + +float UPHY_MMC_HitChance::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Agility = 0.f; + GetCapturedAttributeMagnitude(AgilityDef, Spec, Params, Agility); + Agility = FMath::Max(0.f, Agility); + + constexpr float Base = 0.9f; + constexpr float K = 0.001f; + return FMath::Clamp(Base + Agility * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.h new file mode 100644 index 0000000..8a05fbb --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_HitChance.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_HitChance.generated.h" + +/** HitChance = Base(0.9) + Agility * 0.001 */ +UCLASS() +class PHY_API UPHY_MMC_HitChance : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_HitChance(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition AgilityDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.cpp new file mode 100644 index 0000000..769103c --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_InnerPowerRegenRate::UPHY_MMC_InnerPowerRegenRate() +{ + InnerBreathDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetInnerBreathAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(InnerBreathDef); +} + +float UPHY_MMC_InnerPowerRegenRate::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float InnerBreath = 0.f; + GetCapturedAttributeMagnitude(InnerBreathDef, Spec, Params, InnerBreath); + InnerBreath = FMath::Max(0.f, InnerBreath); + + constexpr float K = 0.15f; + return InnerBreath * K; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.h new file mode 100644 index 0000000..0112123 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_InnerPowerRegenRate.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_InnerPowerRegenRate.generated.h" + +/** InnerPowerRegenRate = InnerBreath * 0.15 */ +UCLASS() +class PHY_API UPHY_MMC_InnerPowerRegenRate : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_InnerPowerRegenRate(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition InnerBreathDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.cpp new file mode 100644 index 0000000..54c1e9b --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_MaxHealth.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_MaxHealth::UPHY_MMC_MaxHealth() +{ + ConstitutionDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetConstitutionAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true /* snapshot */); + + RelevantAttributesToCapture.Add(ConstitutionDef); +} + +float UPHY_MMC_MaxHealth::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters EvalParams; + EvalParams.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + EvalParams.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Constitution = 0.f; + GetCapturedAttributeMagnitude(ConstitutionDef, Spec, EvalParams, Constitution); + Constitution = FMath::Max(0.f, Constitution); + + constexpr float BaseMaxHealth = 100.f; + constexpr float ConstitutionToHealth = 25.f; + return BaseMaxHealth + Constitution * ConstitutionToHealth; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.h new file mode 100644 index 0000000..1c79c26 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxHealth.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_MaxHealth.generated.h" + +/** MaxHealth = Base(100) + Constitution * 25 */ +UCLASS() +class PHY_API UPHY_MMC_MaxHealth : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_MaxHealth(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition ConstitutionDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.cpp new file mode 100644 index 0000000..243de10 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_MaxInnerPower.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_MaxInnerPower::UPHY_MMC_MaxInnerPower() +{ + InnerBreathDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetInnerBreathAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(InnerBreathDef); +} + +float UPHY_MMC_MaxInnerPower::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float InnerBreath = 0.f; + GetCapturedAttributeMagnitude(InnerBreathDef, Spec, Params, InnerBreath); + InnerBreath = FMath::Max(0.f, InnerBreath); + + constexpr float Base = 100.f; + constexpr float K = 20.f; + return Base + InnerBreath * K; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.h new file mode 100644 index 0000000..f1718a5 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MaxInnerPower.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_MaxInnerPower.generated.h" + +/** MaxInnerPower = Base(100) + InnerBreath * 20 */ +UCLASS() +class PHY_API UPHY_MMC_MaxInnerPower : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_MaxInnerPower(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition InnerBreathDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.cpp new file mode 100644 index 0000000..e8969a9 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_MoveSpeed.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_MoveSpeed::UPHY_MMC_MoveSpeed() +{ + AgilityDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetAgilityAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true /* snapshot */); + + RelevantAttributesToCapture.Add(AgilityDef); +} + +float UPHY_MMC_MoveSpeed::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters EvalParams; + EvalParams.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + EvalParams.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Agility = 0.f; + GetCapturedAttributeMagnitude(AgilityDef, Spec, EvalParams, Agility); + Agility = FMath::Max(0.f, Agility); + + constexpr float BaseMoveSpeed = 600.f; + constexpr float AgilityToMoveSpeed = 2.f; + return BaseMoveSpeed + Agility * AgilityToMoveSpeed; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.h new file mode 100644 index 0000000..f3e6826 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_MoveSpeed.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_MoveSpeed.generated.h" + +/** MoveSpeed = Base(600) + Agility * 2 */ +UCLASS() +class PHY_API UPHY_MMC_MoveSpeed : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_MoveSpeed(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition AgilityDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.cpp new file mode 100644 index 0000000..bf03457 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_ParryChance.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_ParryChance::UPHY_MMC_ParryChance() +{ + StrengthDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetStrengthAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(StrengthDef); +} + +float UPHY_MMC_ParryChance::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Strength = 0.f; + GetCapturedAttributeMagnitude(StrengthDef, Spec, Params, Strength); + Strength = FMath::Max(0.f, Strength); + + constexpr float Base = 0.03f; + constexpr float K = 0.001f; + return FMath::Clamp(Base + Strength * K, 0.f, 1.f); +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.h new file mode 100644 index 0000000..572930a --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_ParryChance.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_ParryChance.generated.h" + +/** ParryChance = Base(0.03) + Strength * 0.001 */ +UCLASS() +class PHY_API UPHY_MMC_ParryChance : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_ParryChance(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition StrengthDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.cpp new file mode 100644 index 0000000..92d855e --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_PhysicalAttack.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_PhysicalAttack::UPHY_MMC_PhysicalAttack() +{ + StrengthDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetStrengthAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true /* snapshot */); + + RelevantAttributesToCapture.Add(StrengthDef); +} + +float UPHY_MMC_PhysicalAttack::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters EvalParams; + EvalParams.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + EvalParams.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Strength = 0.f; + GetCapturedAttributeMagnitude(StrengthDef, Spec, EvalParams, Strength); + Strength = FMath::Max(0.f, Strength); + + constexpr float BaseAttack = 10.f; + constexpr float StrengthToAttack = 2.f; + return BaseAttack + Strength * StrengthToAttack; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.h new file mode 100644 index 0000000..33030dd --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalAttack.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_PhysicalAttack.generated.h" + +/** PhysicalAttack = Base(10) + Strength * 2 */ +UCLASS() +class PHY_API UPHY_MMC_PhysicalAttack : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_PhysicalAttack(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition StrengthDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.cpp new file mode 100644 index 0000000..a3f7f1e --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_PhysicalDefense.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_PhysicalDefense::UPHY_MMC_PhysicalDefense() +{ + ConstitutionDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetConstitutionAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true /* snapshot */); + + RelevantAttributesToCapture.Add(ConstitutionDef); +} + +float UPHY_MMC_PhysicalDefense::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters EvalParams; + EvalParams.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + EvalParams.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float Constitution = 0.f; + GetCapturedAttributeMagnitude(ConstitutionDef, Spec, EvalParams, Constitution); + Constitution = FMath::Max(0.f, Constitution); + + constexpr float BaseDefense = 5.f; + constexpr float ConstitutionToDefense = 1.5f; + return BaseDefense + Constitution * ConstitutionToDefense; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.h new file mode 100644 index 0000000..62eff31 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_PhysicalDefense.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_PhysicalDefense.generated.h" + +/** PhysicalDefense = Base(5) + Constitution * 1.5 */ +UCLASS() +class PHY_API UPHY_MMC_PhysicalDefense : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_PhysicalDefense(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition ConstitutionDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.cpp b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.cpp new file mode 100644 index 0000000..59c9887 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "AbilitySystem/MMC/PHY_MMC_Tenacity.h" + +#include "AbilitySystem/Attributes/PHYAttributeSet.h" + +UPHY_MMC_Tenacity::UPHY_MMC_Tenacity() +{ + InnerBreathDef = FGameplayEffectAttributeCaptureDefinition( + UPHYAttributeSet::GetInnerBreathAttribute(), + EGameplayEffectAttributeCaptureSource::Target, + true); + + RelevantAttributesToCapture.Add(InnerBreathDef); +} + +float UPHY_MMC_Tenacity::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const +{ + FAggregatorEvaluateParameters Params; + Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); + Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); + + float InnerBreath = 0.f; + GetCapturedAttributeMagnitude(InnerBreathDef, Spec, Params, InnerBreath); + InnerBreath = FMath::Max(0.f, InnerBreath); + + constexpr float Base = 0.f; + constexpr float K = 1.f; + return Base + InnerBreath * K; +} + diff --git a/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.h b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.h new file mode 100644 index 0000000..b03e1ed --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/MMC/PHY_MMC_Tenacity.h @@ -0,0 +1,22 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayModMagnitudeCalculation.h" +#include "PHY_MMC_Tenacity.generated.h" + +/** Tenacity = Base(0) + InnerBreath * 1 */ +UCLASS() +class PHY_API UPHY_MMC_Tenacity : public UGameplayModMagnitudeCalculation +{ + GENERATED_BODY() + +public: + UPHY_MMC_Tenacity(); + virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; + +protected: + FGameplayEffectAttributeCaptureDefinition InnerBreathDef; +}; + diff --git a/Source/PHY/Private/AbilitySystem/PHYCharacterClass.h b/Source/PHY/Private/AbilitySystem/PHYCharacterClass.h new file mode 100644 index 0000000..33630c7 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/PHYCharacterClass.h @@ -0,0 +1,17 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "PHYCharacterClass.generated.h" + +/** 可根据项目需要扩展更多职业/门派/流派 */ +UENUM(BlueprintType) +enum class EPHYCharacterClass : uint8 +{ + None UMETA(DisplayName = "无"), + Warrior UMETA(DisplayName = "武者"), + Tank UMETA(DisplayName = "铁卫"), + Healer UMETA(DisplayName = "医者"), + Assassin UMETA(DisplayName = "刺客"), +}; + diff --git a/Source/PHY/Private/AbilitySystem/PHYClassDefaults.h b/Source/PHY/Private/AbilitySystem/PHYClassDefaults.h new file mode 100644 index 0000000..f1bdc95 --- /dev/null +++ b/Source/PHY/Private/AbilitySystem/PHYClassDefaults.h @@ -0,0 +1,67 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "AbilitySystem/PHYCharacterClass.h" +#include "PHYClassDefaults.generated.h" + +USTRUCT(BlueprintType) +struct FPHYPrimaryAttributes +{ + GENERATED_BODY() + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + float Strength = 10.f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + float Constitution = 10.f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + float InnerBreath = 10.f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + float Agility = 10.f; +}; + +USTRUCT(BlueprintType) +struct FPHYClassDefaultsRow +{ + GENERATED_BODY() + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + EPHYCharacterClass Class = EPHYCharacterClass::None; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + FPHYPrimaryAttributes Primary; +}; + +/** DataAsset holding per-class initial attributes. */ +UCLASS(BlueprintType) +class PHY_API UPHYClassDefaults : public UDataAsset +{ + GENERATED_BODY() + +public: + /** Defaults used when Class not found */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + FPHYPrimaryAttributes FallbackPrimary; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + TArray Classes; + + UFUNCTION(BlueprintCallable) + FPHYPrimaryAttributes GetPrimaryForClass(EPHYCharacterClass InClass) const + { + for (const FPHYClassDefaultsRow& Row : Classes) + { + if (Row.Class == InClass) + { + return Row.Primary; + } + } + return FallbackPrimary; + } +}; + diff --git a/Source/PHY/Private/Anim/RetargeterAnim.cpp b/Source/PHY/Private/Anim/RetargeterAnim.cpp new file mode 100644 index 0000000..5ad1048 --- /dev/null +++ b/Source/PHY/Private/Anim/RetargeterAnim.cpp @@ -0,0 +1,50 @@ +// + + +#include "RetargeterAnim.h" + +#include "Components/RetargeterComponent.h" +#include "Retargeter/IKRetargeter.h" + +void URetargeterAnim::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); + + // 尝试从拥有者获取RetargeterComponent + if (const AActor* Owner = GetOwningActor()) + { + RetargeterComponent = Owner->FindComponentByClass(); + + if (RetargeterComponent) + { + // 绑定到retarget改变事件 + RetargeterComponent->OnRetargetChanged.AddDynamic(this,&URetargeterAnim::OnRetargetChanged); + + // 获取当前的retarget信息 + CurrentRetargetInfo = RetargeterComponent->GetRetargetInfo_Implementation(); + OnRetargetChanged(CurrentRetargetInfo); + } + } +} + +void URetargeterAnim::BeginDestroy() +{ + // 解绑代理以防止内存泄漏 + if (RetargeterComponent) + { + RetargeterComponent->OnRetargetChanged.RemoveDynamic(this, &URetargeterAnim::OnRetargetChanged); + RetargeterComponent = nullptr; + } + + Super::BeginDestroy(); +} + +void URetargeterAnim::OnRetargetChanged(const FRetargetInfo& NewRetargetInfo) +{ + CurrentRetargetInfo = NewRetargetInfo; + if (!CurrentRetargetInfo.Retargeter.IsNull()) + { + IkRetargeter = CurrentRetargetInfo.Retargeter.LoadSynchronous(); + } +} + diff --git a/Source/PHY/Private/Anim/RetargeterAnim.h b/Source/PHY/Private/Anim/RetargeterAnim.h new file mode 100644 index 0000000..09bd994 --- /dev/null +++ b/Source/PHY/Private/Anim/RetargeterAnim.h @@ -0,0 +1,43 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimInstance.h" +#include "Gameplay/PHYGameInstance.h" +#include "RetargeterAnim.generated.h" + +struct FRetargetProfile; +struct FRetargetInfo; + +class URetargeterComponent; + +/** + * Retargeter动画实例,用于处理角色网格的IK retarget逻辑 + */ +UCLASS() +class PHY_API URetargeterAnim : public UAnimInstance +{ + GENERATED_BODY() + +protected: + virtual void NativeInitializeAnimation() override; + virtual void BeginDestroy() override; + +private: + /** 当前的retarget信息 */ + FRetargetInfo CurrentRetargetInfo; + + /** IK Retargeter实例 */ + UPROPERTY(BlueprintReadOnly, Category = "PHY|Retargeter", meta = (AllowPrivateAccess = "true")) + UIKRetargeter* IkRetargeter; + + /** Retargeter组件的引用 */ + UPROPERTY() + TObjectPtr RetargeterComponent; + + /** 当retarget信息改变时调用 */ + UFUNCTION() + void OnRetargetChanged(const FRetargetInfo& NewRetargetInfo); + +}; diff --git a/Source/PHY/Private/Character/AICharacter.cpp b/Source/PHY/Private/Character/AICharacter.cpp new file mode 100644 index 0000000..e445a06 --- /dev/null +++ b/Source/PHY/Private/Character/AICharacter.cpp @@ -0,0 +1,32 @@ +// + + +#include "AICharacter.h" + + +// Sets default values +AAICharacter::AAICharacter() +{ + // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; +} + +// Called when the game starts or when spawned +void AAICharacter::BeginPlay() +{ + Super::BeginPlay(); + +} + +// Called every frame +void AAICharacter::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); +} + +// Called to bind functionality to input +void AAICharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + Super::SetupPlayerInputComponent(PlayerInputComponent); +} + diff --git a/Source/PHY/Private/Character/AICharacter.h b/Source/PHY/Private/Character/AICharacter.h new file mode 100644 index 0000000..cd23c84 --- /dev/null +++ b/Source/PHY/Private/Character/AICharacter.h @@ -0,0 +1,28 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "PHYCharacter.h" +#include "AICharacter.generated.h" + +UCLASS() +class PHY_API AAICharacter : public APHYCharacter +{ + GENERATED_BODY() + +public: + // Sets default values for this character's properties + AAICharacter(); + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + +public: + // Called every frame + virtual void Tick(float DeltaTime) override; + + // Called to bind functionality to input + virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; +}; diff --git a/Source/PHY/Private/Character/PHYCharacter.cpp b/Source/PHY/Private/Character/PHYCharacter.cpp new file mode 100644 index 0000000..b33583f --- /dev/null +++ b/Source/PHY/Private/Character/PHYCharacter.cpp @@ -0,0 +1,32 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Character/PHYCharacter.h" + +#include "GIS_InventorySystemComponent.h" +#include "GMS_CharacterMovementSystemComponent.h" +#include "Components/RetargeterComponent.h" + +// Sets default values +APHYCharacter::APHYCharacter() +{ + // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + + InventorySystemComponent = CreateDefaultSubobject(TEXT("InventorySystem")); + MovementSystemComponent = CreateDefaultSubobject(TEXT("MovementSystem")); + RetargeterComponent = CreateDefaultSubobject(TEXT("Retargeter")); +} + +// Called when the game starts or when spawned +void APHYCharacter::BeginPlay() +{ + Super::BeginPlay(); + + // 库存初始化应由服务器执行(组件接口也标记了 BlueprintAuthorityOnly)。 + if (HasAuthority() && InventorySystemComponent) + { + InventorySystemComponent->InitializeInventorySystem(); + } +} + diff --git a/Source/PHY/Private/Character/PHYCharacter.h b/Source/PHY/Private/Character/PHYCharacter.h new file mode 100644 index 0000000..fa672b8 --- /dev/null +++ b/Source/PHY/Private/Character/PHYCharacter.h @@ -0,0 +1,55 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Character.h" +#include "PHYCharacter.generated.h" + +class URetargeterComponent; + +class UGIS_InventorySystemComponent; +class UGMS_CharacterMovementSystemComponent; + + +UCLASS() +class APHYCharacter : public ACharacter +{ + GENERATED_BODY() + +public: + // Sets default values for this character's properties + APHYCharacter(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Inventory") + UGIS_InventorySystemComponent* GetInventorySystemComponent() const { return InventorySystemComponent; } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Movement") + UGMS_CharacterMovementSystemComponent* GetMovementSystemComponent() const { return MovementSystemComponent; } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Retargeter") + URetargeterComponent* GetRetargeterComponent() const { return RetargeterComponent; } + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + + /** + * 角色的库存系统组件(来自 GenericInventorySystem 插件 GIS)。 + * 由服务器初始化,客户端通过复制拿到数据。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Inventory", meta = (AllowPrivateAccess = "true")) + TObjectPtr InventorySystemComponent; + + /** + * 角色的移动系统组件(来自 GenericMovementSystem 插件 GMS)。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Movement", meta = (AllowPrivateAccess = "true")) + TObjectPtr MovementSystemComponent; + + /** + * 角色的Retargeter组件,用于处理角色网格的retarget逻辑。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter", meta = (AllowPrivateAccess = "true")) + TObjectPtr RetargeterComponent; +}; diff --git a/Source/PHY/Private/Character/PHYPlayerCharacter.cpp b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp new file mode 100644 index 0000000..e2e4741 --- /dev/null +++ b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp @@ -0,0 +1,230 @@ +// + + +#include "PHYPlayerCharacter.h" + +#include "GIPS_InputSystemComponent.h" +#include "InputActionValue.h" +#include "Camera/CameraComponent.h" +#include "GameFramework/SpringArmComponent.h" +#include "Gameplay/Player/PHYPlayerState.h" +#include "Gameplay/PHYGameInstance.h" +#include "GameplayTags/InputTags.h" +#include "Components/RetargeterComponent.h" +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h" +#include "AbilitySystem/Effects/PHYGE_InitPrimary.h" +#include "AbilitySystem/Effects/PHYGE_RegenTick.h" +#include "AbilitySystem/PHYClassDefaults.h" +#include "GameplayTags/InitAttributeTags.h" +#include "GameplayTags/RegenTags.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "UI/HUD/PHYGameHUD.h" + + +APHYPlayerCharacter::APHYPlayerCharacter() +{ + PrimaryActorTick.bCanEverTick = true; + CameraBoom = CreateDefaultSubobject(TEXT("CameraBoom")); + CameraBoom->SetupAttachment(RootComponent); + CameraBoom->TargetArmLength = 300.f; + CameraBoom->bUsePawnControlRotation = true; + Camera = CreateDefaultSubobject(TEXT("Camera")); + Camera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); + Camera->bUsePawnControlRotation = false; + InputSystemComponent = CreateDefaultSubobject(TEXT("InputSystemComponent")); + RetargetMeshComponent = CreateDefaultSubobject(TEXT("RetargetMeshComponent")); + RetargetMeshComponent->SetupAttachment(GetMesh()); +} + +FRotator APHYPlayerCharacter::GetRotationInput_Implementation() const +{ + if (InputSystemComponent) + { + const FInputActionValue LookInputValue = InputSystemComponent->GetInputActionValueOfInputTag(::InputTags::Tag__Input_Look); + const FVector2D LookInputVector = LookInputValue.Get(); + return FRotator(LookInputVector.Y, LookInputVector.X, 0.f); + } + return FRotator::ZeroRotator; +} + +FVector APHYPlayerCharacter::GetMovementInput_Implementation() const +{ + if (InputSystemComponent) + { + const FInputActionValue MoveInputValue = InputSystemComponent->GetInputActionValueOfInputTag(::InputTags::Tag__Input_Move); + const FVector2D MoveInputVector = MoveInputValue.Get(); + return FVector(MoveInputVector.X, MoveInputVector.Y, 0.f); + } + return FVector::ZeroVector; +} + + +void APHYPlayerCharacter::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + // 将自定义的RetargetMeshComponent设置到RetargeterComponent + if (RetargeterComponent && RetargetMeshComponent) + { + RetargeterComponent->CustomRetargetMeshComponent = RetargetMeshComponent; + } +} + +void APHYPlayerCharacter::PossessedBy(AController* NewController) +{ + Super::PossessedBy(NewController); + + InitializeGAS(); + // 初始化hud + InitializeHUD(); + if (APHYPlayerState* PS = GetPlayerState()) + { + if (RetargeterComponent && RetargeterComponent->GetDefaultRetargeterTag().IsValid() && !PS->GetReTargeterTag().IsValid()) + { + PS->ServerSetReTargeterTag(RetargeterComponent->GetDefaultRetargeterTag()); + } + } +} + +void APHYPlayerCharacter::InitializeHUD() +{ + if (const APlayerController* PC = Cast(GetController())) + { + if (APHYGameHUD* GameHUD = Cast(PC->GetHUD())) + { + GameHUD->InitializeHUD(); + } + } +} + +void APHYPlayerCharacter::OnRep_PlayerState() +{ + Super::OnRep_PlayerState(); + InitializeGAS(); + // 初始化hud + InitializeHUD(); +} + +void APHYPlayerCharacter::InitializeGAS() +{ + APHYPlayerState* PS = GetPlayerState(); + if (!PS) return; + + UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent(); + if (!ASC) return; + + ASC->InitAbilityActorInfo(PS, this); + + // Server applies init effects. + if (HasAuthority()) + { + // Init primary values + { + FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext(); + Ctx.AddSourceObject(this); + FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(UPHYGE_InitPrimary::StaticClass(), 1.f, Ctx); + if (Spec.IsValid()) + { + // Per-class init values (fallback to defaults if no asset is set) + FPHYPrimaryAttributes Primary; + if (const UPHYGameInstance* GI = GetGameInstance()) + { + if (const UPHYClassDefaults* Defaults = GI->GetClassDefaults()) + { + Primary = Defaults->GetPrimaryForClass(CharacterClass); + } + else + { + Primary = FPHYPrimaryAttributes{}; + } + } + else + { + Primary = FPHYPrimaryAttributes{}; + } + + // Use SetByCaller so the same GE can be reused for every class. + Spec.Data->SetSetByCallerMagnitude(InitAttributeTags::Tag__Data_Init_Primary_Strength, Primary.Strength); + Spec.Data->SetSetByCallerMagnitude(InitAttributeTags::Tag__Data_Init_Primary_Constitution, Primary.Constitution); + Spec.Data->SetSetByCallerMagnitude(InitAttributeTags::Tag__Data_Init_Primary_InnerBreath, Primary.InnerBreath); + Spec.Data->SetSetByCallerMagnitude(InitAttributeTags::Tag__Data_Init_Primary_Agility, Primary.Agility); + + ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get()); + } + } + + // Apply derived attributes (MMC driven) + { + FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext(); + Ctx.AddSourceObject(this); + FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(UPHYGE_DerivedAttributes::StaticClass(), 1.f, Ctx); + if (Spec.IsValid()) + { + ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get()); + } + } + + // Set current Health to MaxHealth once derived is applied. + if (const UPHYAttributeSet* AS = PS->GetAttributeSet()) + { + ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), AS->GetMaxHealth()); + ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), AS->GetMaxInnerPower()); + } + + StopRegen(); + if (RegenInterval > 0.f) + { + GetWorldTimerManager().SetTimer(RegenTimerHandle, this, &APHYPlayerCharacter::RegenTick, RegenInterval, true); + } + } + + // Push MoveSpeed to CharacterMovement (both sides, will converge via replication) + if (UCharacterMovementComponent* MoveComp = GetCharacterMovement()) + { + if (const UPHYAttributeSet* AS = PS->GetAttributeSet()) + { + const float DesiredSpeed = AS->GetMoveSpeed(); + if (DesiredSpeed > 0.f) + { + MoveComp->MaxWalkSpeed = DesiredSpeed; + } + } + } +} + +void APHYPlayerCharacter::StopRegen() +{ + if (!HasAuthority()) return; + GetWorldTimerManager().ClearTimer(RegenTimerHandle); +} + +void APHYPlayerCharacter::RegenTick() +{ + if (!HasAuthority()) return; + + APHYPlayerState* PS = GetPlayerState(); + if (!PS) return; + + UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent(); + if (!ASC) return; + + const UPHYAttributeSet* AS = PS->GetAttributeSet(); + if (!AS) return; + + // Calculate per-tick amounts (rate is per-second) + const float HealthDelta = AS->GetHealthRegenRate() * RegenInterval; + const float InnerPowerDelta = AS->GetInnerPowerRegenRate() * RegenInterval; + + if (HealthDelta <= 0.f && InnerPowerDelta <= 0.f) return; + + FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext(); + Ctx.AddSourceObject(this); + FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(UPHYGE_RegenTick::StaticClass(), 1.f, Ctx); + if (!Spec.IsValid()) return; + + Spec.Data->SetSetByCallerMagnitude(RegenTags::Tag__Data_Regen_Health, FMath::Max(0.f, HealthDelta)); + Spec.Data->SetSetByCallerMagnitude(RegenTags::Tag__Data_Regen_InnerPower, FMath::Max(0.f, InnerPowerDelta)); + ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get()); +} diff --git a/Source/PHY/Private/Character/PHYPlayerCharacter.h b/Source/PHY/Private/Character/PHYPlayerCharacter.h new file mode 100644 index 0000000..d6f34dc --- /dev/null +++ b/Source/PHY/Private/Character/PHYPlayerCharacter.h @@ -0,0 +1,73 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "PHYCharacter.h" +#include "AbilitySystem/PHYCharacterClass.h" +#include "Pawn/UGC_PawnInterface.h" +#include "PHYPlayerCharacter.generated.h" + +class UGIPS_InputSystemComponent; +class USpringArmComponent; +class UCameraComponent; +class URetargeterComponent; + +UCLASS() +class PHY_API APHYPlayerCharacter : public APHYCharacter, +public IUGC_PawnInterface +{ + GENERATED_BODY() + +public: + + APHYPlayerCharacter(); + +protected: + /** 职业/门派/流派:决定初始四维 */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|Class") + EPHYCharacterClass CharacterClass = EPHYCharacterClass::Warrior; + + /** + * 角色的相机组件 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Camera") + TObjectPtr Camera; + /** + * 角色的相机臂组件 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Camera") + TObjectPtr CameraBoom; + /** + * 角色的输入组件 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Input") + TObjectPtr InputSystemComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Retarget") + TObjectPtr RetargetMeshComponent; + + //~ Begin IUGC_PawnInterface + virtual FRotator GetRotationInput_Implementation() const override; + virtual FVector GetMovementInput_Implementation() const override; + //~ End IUGC_PawnInterface + +protected: + virtual void PossessedBy(AController* NewController) override; + void InitializeHUD(); + virtual void OnRep_PlayerState() override; + + /** Server: apply init effects. Both sides: init actor info. */ + void InitializeGAS(); + +protected: + virtual void PostInitializeComponents() override; + + /** Regen tick interval (seconds). Server-only. */ + UPROPERTY(EditDefaultsOnly, Category="PHY|Regen") + float RegenInterval = 1.0f; + + FTimerHandle RegenTimerHandle; + void RegenTick(); + void StopRegen(); +}; diff --git a/Source/PHY/Private/Components/RetargeterComponent.cpp b/Source/PHY/Private/Components/RetargeterComponent.cpp new file mode 100644 index 0000000..d0345bb --- /dev/null +++ b/Source/PHY/Private/Components/RetargeterComponent.cpp @@ -0,0 +1,128 @@ +// + +#include "RetargeterComponent.h" + +#include "Gameplay/PHYGameInstance.h" +#include "Components/SkeletalMeshComponent.h" +#include "GameFramework/Character.h" + + + +URetargeterComponent::URetargeterComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void URetargeterComponent::BeginPlay() +{ + Super::BeginPlay(); + + // 延迟一帧初始化,确保所有组件都已完全初始化 + if (bAutoInitializeRetargeter) + { + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() + { + if (IsValid(this)) + { + InitializeRetargeter(); + } + }); + } +} + +void URetargeterComponent::InitializeRetargeter() +{ + // 先刷新网格设置 + RefreshMeshSettings(); + + // 然后设置retargeter + SetupRetargeter(); +} + +void URetargeterComponent::SetupRetargeter() +{ + // 获取retarget mesh + USkeletalMeshComponent* RetargetMeshComponent = GetRetargetMeshComponent_Implementation(); + + // 获取动画实例 + TSubclassOf RetargeterAnim = GetRetargeterAnimInstance_Implementation(); + + // 设置retarget mesh的skeletal mesh和动画实例 + if (RetargetMeshComponent && RetargeterAnim) + { + RetargetMeshComponent->SetAnimInstanceClass(RetargeterAnim); + RetargetMeshComponent->SetCollisionProfileName(TEXT("CharacterMesh")); + RetargetMeshComponent->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECollisionResponse::ECR_Block); + } +} + +void URetargeterComponent::RefreshMeshSettings() +{ + UE_LOG(LogTemp, Log, TEXT("URetargeterComponent::RefreshMeshSettings()")); + + // 获取retarget mesh组件 + USkeletalMeshComponent* RetargetMeshComponent = GetRetargetMeshComponent_Implementation(); + + // 获取retarget信息 + FRetargetInfo RetargetInfo = GetRetargetInfo_Implementation(); + + if (RetargetMeshComponent && !RetargetInfo.Mesh.IsNull()) + { + USkeletalMesh* RetargetMesh = RetargetInfo.Mesh.LoadSynchronous(); + + RetargetMeshComponent->SetSkeletalMesh(RetargetMesh); + // 广播retarget改变事件 + if (OnRetargetChanged.IsBound()) + { + OnRetargetChanged.Broadcast(RetargetInfo); + + } + } +} + +void URetargeterComponent::ChangeRetargetTag(const FGameplayTag& NewRetargeterTag) +{ + DefaultRetargeterTag = NewRetargeterTag; + RefreshMeshSettings(); +} + +FRetargetInfo URetargeterComponent::GetRetargetInfo_Implementation() +{ + FRetargetInfo RetargetInfo; + + if (UPHYGameInstance* GameInstance = Cast(GetWorld()->GetGameInstance())) + { + if (TOptional Info = GameInstance->GetRetargetInfo(DefaultRetargeterTag); Info.IsSet()) + { + RetargetInfo = Info.GetValue(); + } + } + + return RetargetInfo; +} + +USkeletalMeshComponent* URetargeterComponent::GetMainMeshComponent_Implementation() +{ + if (const ACharacter* CharacterOwner = Cast(GetOwner())) + { + return CharacterOwner->GetMesh(); + } + return nullptr; +} + +USkeletalMeshComponent* URetargeterComponent::GetRetargetMeshComponent_Implementation() +{ + // 如果设置了自定义的Retarget Mesh组件,则使用它 + if (CustomRetargetMeshComponent) + { + return CustomRetargetMeshComponent; + } + + // 否则使用主mesh + return GetMainMeshComponent_Implementation(); +} + +TSubclassOf URetargeterComponent::GetRetargeterAnimInstance_Implementation() +{ + return RetargeterAnimInstance; +} \ No newline at end of file diff --git a/Source/PHY/Private/Components/RetargeterComponent.h b/Source/PHY/Private/Components/RetargeterComponent.h new file mode 100644 index 0000000..0d31dc7 --- /dev/null +++ b/Source/PHY/Private/Components/RetargeterComponent.h @@ -0,0 +1,72 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Interface/Retargetable.h" +#include "Gameplay/PHYGameInstance.h" +#include "RetargeterComponent.generated.h" + +// 声明Retarget信息改变的代理 +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRetargetChanged, const FRetargetInfo&, NewRetargetInfo); + + +UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) +class PHY_API URetargeterComponent : public UActorComponent, public IRetargetable +{ + GENERATED_BODY() + +public: + URetargeterComponent(); + + /** 设置retargeter,包括动画实例和碰撞设置 */ + UFUNCTION(BlueprintCallable, Category = "PHY|Retargeter") + void SetupRetargeter(); + + /** 刷新网格设置,应用新的骨骼网格 */ + UFUNCTION(BlueprintCallable, Category = "PHY|Retargeter") + void RefreshMeshSettings(); + + /** 初始化retargeter(通常在BeginPlay中调用) */ + UFUNCTION(BlueprintCallable, Category = "PHY|Retargeter") + void InitializeRetargeter(); + + /** 更改Retargeter Tag并刷新retarget设置 */ + UFUNCTION(BlueprintCallable, Category = "PHY|Retargeter") + void ChangeRetargetTag(const FGameplayTag& NewRetargeterTag); + + /** 获取默认的Retargeter Tag */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Retargeter") + FGameplayTag GetDefaultRetargeterTag() const { return DefaultRetargeterTag; } + +protected: + virtual void BeginPlay() override; + +public: + // IRetargetable interface implementation + virtual FRetargetInfo GetRetargetInfo_Implementation() override; + virtual USkeletalMeshComponent* GetMainMeshComponent_Implementation() override; + virtual USkeletalMeshComponent* GetRetargetMeshComponent_Implementation() override; + virtual TSubclassOf GetRetargeterAnimInstance_Implementation() override; + + /** Retarget信息改变时触发的代理 */ + UPROPERTY(BlueprintAssignable, Category = "PHY|Retargeter") + FOnRetargetChanged OnRetargetChanged; + + /** 默认的Retargeter Tag,用于从GameInstance获取Retargeter信息 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter", meta = (Categories = "Retargeter")) + FGameplayTag DefaultRetargeterTag; + + /** Retargeter使用的动画实例类 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + TSubclassOf RetargeterAnimInstance; + + /** 是否在BeginPlay时自动初始化Retargeter */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + bool bAutoInitializeRetargeter = true; + + /** 自定义的Retarget Mesh组件(如果为空,则使用角色的主Mesh) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + TObjectPtr CustomRetargetMeshComponent; +}; \ No newline at end of file diff --git a/Source/PHY/Private/Gameplay/PHYGameInstance.cpp b/Source/PHY/Private/Gameplay/PHYGameInstance.cpp new file mode 100644 index 0000000..42f87db --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameInstance.cpp @@ -0,0 +1,11 @@ +// + + +#include "PHYGameInstance.h" + +TOptional UPHYGameInstance::GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const +{ + const FRetargetInfo* Info = Retargeters.Find(RetargetInfoTag); + if (!Info) return TOptional(); + return TOptional(*Info); +} diff --git a/Source/PHY/Private/Gameplay/PHYGameInstance.h b/Source/PHY/Private/Gameplay/PHYGameInstance.h new file mode 100644 index 0000000..7589201 --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameInstance.h @@ -0,0 +1,42 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "Engine/GameInstance.h" +#include "PHYGameInstance.generated.h" + +class UIKRetargeter; +class UPHYClassDefaults; +/** + * + */ +USTRUCT(BlueprintType) +struct FRetargetInfo +{ + GENERATED_BODY() + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + TSoftObjectPtr Retargeter; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + TSoftObjectPtr Mesh; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PHY|Retargeter") + bool bMale = false; +}; + +UCLASS() +class PHY_API UPHYGameInstance : public UGameInstance +{ + GENERATED_BODY() + UPROPERTY(EditAnywhere, Category = "Config|Character",meta=(Categories = "Targeter")) + TMap Retargeters; + + /** 全局职业/门派默认四维配置 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|Attributes", meta=(AllowPrivateAccess=true)) + TObjectPtr ClassDefaults; + +public: + TOptional GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const; + const UPHYClassDefaults* GetClassDefaults() const { return ClassDefaults; } + +}; diff --git a/Source/PHY/Private/Gameplay/PHYGameMode.cpp b/Source/PHY/Private/Gameplay/PHYGameMode.cpp new file mode 100644 index 0000000..83fdf6a --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameMode.cpp @@ -0,0 +1,4 @@ +// + + +#include "PHYGameMode.h" diff --git a/Source/PHY/Private/Gameplay/PHYGameMode.h b/Source/PHY/Private/Gameplay/PHYGameMode.h new file mode 100644 index 0000000..c8261c0 --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameMode.h @@ -0,0 +1,16 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "PHYGameMode.generated.h" + +/** + * + */ +UCLASS() +class PHY_API APHYGameMode : public AGameModeBase +{ + GENERATED_BODY() +}; diff --git a/Source/PHY/Private/Gameplay/PHYGameState.cpp b/Source/PHY/Private/Gameplay/PHYGameState.cpp new file mode 100644 index 0000000..afad231 --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameState.cpp @@ -0,0 +1,4 @@ +// + + +#include "PHYGameState.h" diff --git a/Source/PHY/Private/Gameplay/PHYGameState.h b/Source/PHY/Private/Gameplay/PHYGameState.h new file mode 100644 index 0000000..8dc61d2 --- /dev/null +++ b/Source/PHY/Private/Gameplay/PHYGameState.h @@ -0,0 +1,16 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameStateBase.h" +#include "PHYGameState.generated.h" + +/** + * + */ +UCLASS() +class PHY_API APHYGameState : public AGameStateBase +{ + GENERATED_BODY() +}; diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerController.cpp b/Source/PHY/Private/Gameplay/Player/PHYPlayerController.cpp new file mode 100644 index 0000000..64778be --- /dev/null +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerController.cpp @@ -0,0 +1,5 @@ + + + +#include "Gameplay/Player/PHYPlayerController.h" + diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerController.h b/Source/PHY/Private/Gameplay/Player/PHYPlayerController.h new file mode 100644 index 0000000..c0b5ea6 --- /dev/null +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerController.h @@ -0,0 +1,17 @@ + + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "PHYPlayerController.generated.h" + +/** + * + */ +UCLASS() +class APHYPlayerController : public APlayerController +{ + GENERATED_BODY() + +}; diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp new file mode 100644 index 0000000..feb1b6e --- /dev/null +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp @@ -0,0 +1,83 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "PHYPlayerState.h" + +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYAttributeSet.h" +#include "Character/PHYPlayerCharacter.h" +#include "Components/RetargeterComponent.h" +#include "Gameplay/PHYGameInstance.h" +#include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" + +APHYPlayerState::APHYPlayerState() +{ + // 创建AbilitySystemComponent + AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent")); + AttributeSet = CreateDefaultSubobject(TEXT("AttributeSet")); + + // 设置AbilitySystemComponent的网络复制 + AbilitySystemComponent->SetIsReplicated(true); + + // 使用Minimal复制模式(推荐用于PlayerState) + AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal); + +} + +UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const +{ + return AbilitySystemComponent; +} + +void APHYPlayerState::OnRep_ReTargeterTagChanged(FGameplayTag OldTag) +{ + //获取character + if (APHYPlayerCharacter* Character = Cast(GetPawn())) + { + //获取retargeter组件 + if (URetargeterComponent* RetargeterComponent = Character->GetRetargeterComponent()) + { + //从GameInstance获取新的RetargetInfo + if (const UPHYGameInstance* GI = GetGameInstance()) + { + if (const TOptional NewRetargetInfo = GI->GetRetargetInfo(ReTargeterTag); NewRetargetInfo.IsSet()) + { + //应用新的RetargetInfo到组件 + RetargeterComponent->ChangeRetargetTag(ReTargeterTag); + } + } + } + } +} + +void APHYPlayerState::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + FDoRepLifetimeParams SharedParams; + SharedParams.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, ReTargeterTag, SharedParams); +} + +void APHYPlayerState::SetReTargeterTag(const FGameplayTag& NewReTargeterTag) +{ + MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, ReTargeterTag, this); + if (const ENetMode NetMode = GetNetMode(); NetMode == NM_Standalone || NetMode == NM_ListenServer) + { + OnRep_ReTargeterTagChanged(ReTargeterTag); + } + ReTargeterTag = NewReTargeterTag; + ForceNetUpdate(); +} + +void APHYPlayerState::ServerSetReTargeterTag_Implementation(const FGameplayTag& NewReTargeterTag) +{ + if (!NewReTargeterTag.IsValid() || NewReTargeterTag == ReTargeterTag) return; + if (const UPHYGameInstance* GI = GetGameInstance()) + { + if (GI->GetRetargetInfo(NewReTargeterTag).IsSet()) + { + SetReTargeterTag(NewReTargeterTag); + } + } +} diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h new file mode 100644 index 0000000..4407583 --- /dev/null +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h @@ -0,0 +1,62 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemInterface.h" +#include "GameplayTagContainer.h" +#include "GameFramework/PlayerState.h" +#include "PHYPlayerState.generated.h" + +class UAbilitySystemComponent; +class UPHYAttributeSet; + +/** + * 玩家状态类,包含GAS支持 + * 用于管理玩家属性、技能等游戏玩法系统 + */ +UCLASS() +class PHY_API APHYPlayerState : public APlayerState, public IAbilitySystemInterface +{ + GENERATED_BODY() + +public: + APHYPlayerState(); + + // IAbilitySystemInterface implementation + virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; + +protected: + /** GAS Ability System Component */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", Meta = (AllowPrivateAccess = true)) + TObjectPtr AbilitySystemComponent; + + /** GAS AttributeSet (Primary/Vitals/Derived). Lives on PlayerState for replication. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", Meta = (AllowPrivateAccess = true)) + TObjectPtr AttributeSet; + +public: + + /** 获取Ability System Component */ + UFUNCTION(BlueprintCallable, Category = "Abilities") + UAbilitySystemComponent* GetPHYAbilitySystemComponent() const { return AbilitySystemComponent; } + + UFUNCTION(BlueprintCallable, Category = "Abilities") + const UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; } + + // 原有的ReTargeterTag相关代码 + UPROPERTY(VisibleAnywhere,Category = "Config|Character",ReplicatedUsing=OnRep_ReTargeterTagChanged) + FGameplayTag ReTargeterTag; + + UFUNCTION() + void OnRep_ReTargeterTagChanged(FGameplayTag OldTag); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + void SetReTargeterTag(const FGameplayTag& NewReTargeterTag); + +public: + UFUNCTION(Server, Reliable,BlueprintCallable) + void ServerSetReTargeterTag(const FGameplayTag& NewReTargeterTag); + FGameplayTag GetReTargeterTag() const { return ReTargeterTag; } +}; diff --git a/Source/PHY/Private/GameplayTags/InitAttributeTags.cpp b/Source/PHY/Private/GameplayTags/InitAttributeTags.cpp new file mode 100644 index 0000000..05fc64d --- /dev/null +++ b/Source/PHY/Private/GameplayTags/InitAttributeTags.cpp @@ -0,0 +1,12 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "GameplayTags/InitAttributeTags.h" + +namespace InitAttributeTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Init_Primary_Strength, "Data.Init.Primary.Strength", "Init Primary Strength (SetByCaller)"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Init_Primary_Constitution, "Data.Init.Primary.Constitution", "Init Primary Constitution (SetByCaller)"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Init_Primary_InnerBreath, "Data.Init.Primary.InnerBreath", "Init Primary InnerBreath (SetByCaller)"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Init_Primary_Agility, "Data.Init.Primary.Agility", "Init Primary Agility (SetByCaller)"); +} + diff --git a/Source/PHY/Private/GameplayTags/InitAttributeTags.h b/Source/PHY/Private/GameplayTags/InitAttributeTags.h new file mode 100644 index 0000000..a42405c --- /dev/null +++ b/Source/PHY/Private/GameplayTags/InitAttributeTags.h @@ -0,0 +1,16 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +/** GameplayTags used for initializing attributes (SetByCaller). */ +namespace InitAttributeTags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Init_Primary_Strength); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Init_Primary_Constitution); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Init_Primary_InnerBreath); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Init_Primary_Agility); +} + diff --git a/Source/PHY/Private/GameplayTags/InputTags.cpp b/Source/PHY/Private/GameplayTags/InputTags.cpp new file mode 100644 index 0000000..4e6483e --- /dev/null +++ b/Source/PHY/Private/GameplayTags/InputTags.cpp @@ -0,0 +1,12 @@ +// + + +#include "InputTags.h" + + +namespace InputTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Input_Move,"Input.Move","Tag for move input"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Input_Look,"Input.Look","Tag for look input"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Input_Jump,"Input.Jump","Tag for jump input"); +} diff --git a/Source/PHY/Private/GameplayTags/InputTags.h b/Source/PHY/Private/GameplayTags/InputTags.h new file mode 100644 index 0000000..a8bfbba --- /dev/null +++ b/Source/PHY/Private/GameplayTags/InputTags.h @@ -0,0 +1,16 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +/** + * + */ +namespace InputTags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Input_Move); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Input_Look); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Input_Jump); +}; diff --git a/Source/PHY/Private/GameplayTags/MovementTags.cpp b/Source/PHY/Private/GameplayTags/MovementTags.cpp new file mode 100644 index 0000000..dff32d6 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/MovementTags.cpp @@ -0,0 +1,13 @@ +// + + +#include "MovementTags.h" + + +namespace MovementTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__MovementSet_Default,"GMS.MovementSet.Default","Tag for GMS MovementSet Default"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__MovementSet_ADS,"GMS.MovementSet.ADS","Tag for GMS MovementSet ADS"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__MovementSet_ADS_Crouched,"GMS.MovementSet.ADS.Crouched","Tag for GMS MovementSet ADS Crouched"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__MovementSet_Crouched,"GMS.MovementSet.Crouched","Tag for GMS MovementSet Crouched"); +} diff --git a/Source/PHY/Private/GameplayTags/MovementTags.h b/Source/PHY/Private/GameplayTags/MovementTags.h new file mode 100644 index 0000000..e35da5d --- /dev/null +++ b/Source/PHY/Private/GameplayTags/MovementTags.h @@ -0,0 +1,17 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +/** + * + */ +namespace MovementTags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__MovementSet_Default); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__MovementSet_ADS); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__MovementSet_ADS_Crouched); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__MovementSet_Crouched); +}; diff --git a/Source/PHY/Private/GameplayTags/ReTargeterTags.cpp b/Source/PHY/Private/GameplayTags/ReTargeterTags.cpp new file mode 100644 index 0000000..21fa6dc --- /dev/null +++ b/Source/PHY/Private/GameplayTags/ReTargeterTags.cpp @@ -0,0 +1,15 @@ +// + + +#include "ReTargeterTags.h" + + +namespace ReTargeterTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_F_Warrior,"Retargeter.F.Warrior","Tag for retargeting to the F_Warrior retargeter"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_F_Warrior_Newbie,"Retargeter.F.Warrior.Newbie","Tag for retargeting to the F_Warrior_Newbee retargeter"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_F_Warrior_Addvanced,"Retargeter.F.Warrior.Addvanced","Tag for retargeting to the F_Warrior_Addvanced retargeter"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_M_Warrior,"Retargeter.M.Warrior","Tag for retargeting to the M_Warrior retargeter"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_M_Warrior_Newbie,"Retargeter.M.Warrior.Newbie","Tag for retargeting to the M_Warrior_Newbee retargeter"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Retargeter_M_Warrior_Addvanced,"Retargeter.M.Warrior.Addvanced","Tag for retargeting to the M_Warrior_Addvanced retargeter"); +} diff --git a/Source/PHY/Private/GameplayTags/ReTargeterTags.h b/Source/PHY/Private/GameplayTags/ReTargeterTags.h new file mode 100644 index 0000000..2698010 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/ReTargeterTags.h @@ -0,0 +1,19 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +/** + * + */ +namespace ReTargeterTags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_F_Warrior); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_F_Warrior_Newbie); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_F_Warrior_Addvanced); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_M_Warrior); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_M_Warrior_Newbie); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Retargeter_M_Warrior_Addvanced); +}; diff --git a/Source/PHY/Private/GameplayTags/RegenTags.cpp b/Source/PHY/Private/GameplayTags/RegenTags.cpp new file mode 100644 index 0000000..cf1510e --- /dev/null +++ b/Source/PHY/Private/GameplayTags/RegenTags.cpp @@ -0,0 +1,10 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "GameplayTags/RegenTags.h" + +namespace RegenTags +{ + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Regen_Health, "Data.Regen.Health", "SetByCaller: Health regen per tick"); + UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__Data_Regen_InnerPower, "Data.Regen.InnerPower", "SetByCaller: InnerPower regen per tick"); +} + diff --git a/Source/PHY/Private/GameplayTags/RegenTags.h b/Source/PHY/Private/GameplayTags/RegenTags.h new file mode 100644 index 0000000..7d7e308 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/RegenTags.h @@ -0,0 +1,13 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +namespace RegenTags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Regen_Health); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__Data_Regen_InnerPower); +} + diff --git a/Source/PHY/Private/GameplayTags/UITags.cpp b/Source/PHY/Private/GameplayTags/UITags.cpp new file mode 100644 index 0000000..ee6a4f6 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/UITags.cpp @@ -0,0 +1,13 @@ +// + + +#include "UITags.h" + + +namespace UITags +{ + UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Game, "UI.Layer.Game"); + UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_GameMenu, "UI.Layer.GameMenu"); + UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Menu, "UI.Layer.Menu"); + UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Modal, "UI.Layer.Modal"); +} diff --git a/Source/PHY/Private/GameplayTags/UITags.h b/Source/PHY/Private/GameplayTags/UITags.h new file mode 100644 index 0000000..c5198f3 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/UITags.h @@ -0,0 +1,17 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "NativeGameplayTags.h" + +/** + * + */ +namespace UITags +{ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Game); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_GameMenu); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Menu); + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Modal); +}; diff --git a/Source/PHY/Private/Input/InputHelper.h b/Source/PHY/Private/Input/InputHelper.h new file mode 100644 index 0000000..5f563e7 --- /dev/null +++ b/Source/PHY/Private/Input/InputHelper.h @@ -0,0 +1,17 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GIPS_InputSystemComponent.h" + +/** + * + */ +namespace InputHelper +{ + static APawn* GetOwnerPawn(const UGIPS_InputSystemComponent* InputSystemComponent) + { + return Cast(InputSystemComponent->GetOwner()); + } +} \ No newline at end of file diff --git a/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.cpp b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.cpp new file mode 100644 index 0000000..f99595a --- /dev/null +++ b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.cpp @@ -0,0 +1,26 @@ +// + + +#include "InputProcessor_Look.h" + +#include "GameplayTags/InputTags.h" +#include "Input/InputHelper.h" + +UInputProcessor_Look::UInputProcessor_Look() +{ + InputTags.AddTag(::InputTags::Tag__Input_Look); + TriggerEvents.Empty(); + TriggerEvents.AddUnique(ETriggerEvent::Triggered); +} + +void UInputProcessor_Look::HandleInputTriggered_Implementation(UGIPS_InputSystemComponent* IC, + const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ + if (InputTag != InputTags::Tag__Input_Look) return; + APawn* OwnerPawn = InputHelper::GetOwnerPawn(IC); + if (!OwnerPawn) return; + const FInputActionValue LookInputValue = IC->GetInputActionValueOfInputTag(InputTag); + const FVector2D LookInputVector = LookInputValue.Get(); + OwnerPawn->AddControllerYawInput(LookInputVector.X); + OwnerPawn->AddControllerPitchInput(LookInputVector.Y); +} diff --git a/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.h b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.h new file mode 100644 index 0000000..17d9449 --- /dev/null +++ b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Look.h @@ -0,0 +1,20 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GIPS_InputProcessor.h" +#include "InputProcessor_Look.generated.h" + +/** + * + */ +UCLASS() +class PHY_API UInputProcessor_Look : public UGIPS_InputProcessor +{ + GENERATED_BODY() + +public: + UInputProcessor_Look(); + virtual void HandleInputTriggered_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const override; +}; diff --git a/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.cpp b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.cpp new file mode 100644 index 0000000..c6eb40e --- /dev/null +++ b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.cpp @@ -0,0 +1,32 @@ +// + + +#include "InputProcessor_Move.h" + +#include "GameplayTags/InputTags.h" +#include "Input/InputHelper.h" + +UInputProcessor_Move::UInputProcessor_Move() +{ + InputTags.AddTag(::InputTags::Tag__Input_Move); + TriggerEvents.Empty(); + TriggerEvents.AddUnique(ETriggerEvent::Triggered); +} + +void UInputProcessor_Move::HandleInputTriggered_Implementation(UGIPS_InputSystemComponent* IC, + const FInputActionInstance& ActionData, FGameplayTag InputTag) const +{ + if (InputTag != InputTags::Tag__Input_Move) return; + APawn* OwnerPawn = InputHelper::GetOwnerPawn(IC); + if (!OwnerPawn) return; + const FInputActionValue MoveInputValue = IC->GetInputActionValueOfInputTag(InputTag); + const FVector2D MoveInputVector = MoveInputValue.Get(); + // 计算向右 + const FRotator Rotation = OwnerPawn->GetControlRotation(); + const FRotator YawRotation(0.f, Rotation.Yaw, 0.f); + const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); + const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); + // 添加输入 + OwnerPawn->AddMovementInput(ForwardDirection, MoveInputVector.Y); + OwnerPawn->AddMovementInput(RightDirection, MoveInputVector.X); +} diff --git a/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.h b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.h new file mode 100644 index 0000000..108f13e --- /dev/null +++ b/Source/PHY/Private/Input/Processor/Movement/InputProcessor_Move.h @@ -0,0 +1,21 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GIPS_InputProcessor.h" +#include "InputProcessor_Move.generated.h" + +/** + * + */ +UCLASS() +class PHY_API UInputProcessor_Move : public UGIPS_InputProcessor +{ + GENERATED_BODY() + +public: + UInputProcessor_Move(); + + virtual void HandleInputTriggered_Implementation(UGIPS_InputSystemComponent* IC, const FInputActionInstance& ActionData, FGameplayTag InputTag) const override; +}; diff --git a/Source/PHY/Private/Interface/Retargetable.cpp b/Source/PHY/Private/Interface/Retargetable.cpp new file mode 100644 index 0000000..6a08692 --- /dev/null +++ b/Source/PHY/Private/Interface/Retargetable.cpp @@ -0,0 +1,7 @@ +// + + +#include "Retargetable.h" + + +// Add default functionality here for any IRetargeterable functions that are not pure virtual. diff --git a/Source/PHY/Private/Interface/Retargetable.h b/Source/PHY/Private/Interface/Retargetable.h new file mode 100644 index 0000000..f0044c7 --- /dev/null +++ b/Source/PHY/Private/Interface/Retargetable.h @@ -0,0 +1,37 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "Gameplay/PHYGameInstance.h" +#include "UObject/Interface.h" +#include "Retargetable.generated.h" + + +// This class does not need to be modified. +UINTERFACE() +class URetargetable : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class PHY_API IRetargetable +{ + GENERATED_BODY() + + // Add interface functions to this class. This is the class that will be inherited to implement this interface. +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "PHY|Retargeter") + FRetargetInfo GetRetargetInfo(); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "PHY|Retargeter") + USkeletalMeshComponent* GetMainMeshComponent(); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "PHY|Retargeter") + USkeletalMeshComponent* GetRetargetMeshComponent(); + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "PHY|Retargeter") + TSubclassOf GetRetargeterAnimInstance(); +}; diff --git a/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp b/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp new file mode 100644 index 0000000..41b6694 --- /dev/null +++ b/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp @@ -0,0 +1,63 @@ +// + + +#include "PHYGameHUD.h" + +#include "GameplayTags/UITags.h" +#include "UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h" + +UGUIS_GameUISubsystem* APHYGameHUD::GetUISubsystem() const +{ + if (const UGameInstance* GameInstance = GetGameInstance()) + { + return GameInstance->GetSubsystem(); + } + return nullptr; +} + +void APHYGameHUD::OnBeforePushOverlapWidget(UCommonActivatableWidget* UserWidget) +{ +} + +void APHYGameHUD::OnAfterPushOverlapWidget(UCommonActivatableWidget* UserWidget) +{ +} + +void APHYGameHUD::InitializeHUD() +{ + // 获取当前的player controller + APlayerController* PC = GetOwningPlayerController(); + if (!PC) return; + if (ULocalPlayer* LocalPlayer = PC->GetLocalPlayer()) + { + if (UGUIS_GameUISubsystem* UISubsystem = GetUISubsystem()) + { + // 创建并注册HUD的UI到本地玩家 + UISubsystem->AddPlayer(LocalPlayer); + } + } + if (OverlapWidgetClass.IsNull()) return; + // 将重叠提示控件推送到游戏UI层 + if (UGUIS_AsyncAction_PushContentToUILayer * PushAction = UGUIS_AsyncAction_PushContentToUILayer::PushContentToUILayerForPlayer(PC, OverlapWidgetClass, UITags::Tag__UI_Layer_Game)) + { + PushAction->BeforePush.AddDynamic(this, &APHYGameHUD::OnBeforePushOverlapWidget); + PushAction->AfterPush.AddDynamic(this, &APHYGameHUD::OnAfterPushOverlapWidget); + PushAction->Activate(); + } +} + +void APHYGameHUD::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (const APlayerController* PC = GetOwningPlayerController()) + { + if (ULocalPlayer* LocalPlayer = PC->GetLocalPlayer()) + { + if (UGUIS_GameUISubsystem* UISubsystem = GetUISubsystem()) + { + // 创建并注册HUD的UI到本地玩家 + UISubsystem->RemovePlayer(LocalPlayer); + } + } + } + Super::EndPlay(EndPlayReason); +} diff --git a/Source/PHY/Private/UI/HUD/PHYGameHUD.h b/Source/PHY/Private/UI/HUD/PHYGameHUD.h new file mode 100644 index 0000000..d544464 --- /dev/null +++ b/Source/PHY/Private/UI/HUD/PHYGameHUD.h @@ -0,0 +1,38 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/HUD.h" +#include "UI/GUIS_GameUISubsystem.h" +#include "PHYGameHUD.generated.h" + + +class UCommonActivatableWidget; +/** + * + */ +UCLASS() +class PHY_API APHYGameHUD : public AHUD +{ + GENERATED_BODY() + + // 获取GUIS_GameSubsystem + UGUIS_GameUISubsystem* GetUISubsystem() const; + +public: + + void InitializeHUD(); + +protected: + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + UPROPERTY(EditDefaultsOnly, Category="PHY|UI") + TSoftClassPtr OverlapWidgetClass; + +private: + UFUNCTION() + void OnBeforePushOverlapWidget(UCommonActivatableWidget* UserWidget); + UFUNCTION() + void OnAfterPushOverlapWidget(UCommonActivatableWidget* UserWidget); +}; diff --git a/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp b/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp new file mode 100644 index 0000000..c85ed62 --- /dev/null +++ b/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp @@ -0,0 +1,4 @@ +// + + +#include "Menu_Overlap.h" diff --git a/Source/PHY/Private/UI/Menu/Menu_Overlap.h b/Source/PHY/Private/UI/Menu/Menu_Overlap.h new file mode 100644 index 0000000..6874968 --- /dev/null +++ b/Source/PHY/Private/UI/Menu/Menu_Overlap.h @@ -0,0 +1,16 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "UI/GUIS_ActivatableWidget.h" +#include "Menu_Overlap.generated.h" + +/** + * + */ +UCLASS() +class PHY_API UMenu_Overlap : public UGUIS_ActivatableWidget +{ + GENERATED_BODY() +}; diff --git a/Source/PHY/Private/UI/PHYGameUILayout.cpp b/Source/PHY/Private/UI/PHYGameUILayout.cpp new file mode 100644 index 0000000..687fba6 --- /dev/null +++ b/Source/PHY/Private/UI/PHYGameUILayout.cpp @@ -0,0 +1,37 @@ +// Copyright 2026 PHY. All Rights Reserved. + +#include "UI/PHYGameUILayout.h" + +#include "GameplayTags/UITags.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYGameUILayout) + +UPHYGameUILayout::UPHYGameUILayout(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UPHYGameUILayout::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + // Register available layers. Widget Blueprint can bind any subset. + if (Layer_Game) + { + RegisterLayer(UITags::Tag__UI_Layer_Game, Layer_Game); + } + if (Layer_GameMenu) + { + RegisterLayer(UITags::Tag__UI_Layer_GameMenu, Layer_GameMenu); + } + if (Layer_Menu) + { + RegisterLayer(UITags::Tag__UI_Layer_Menu, Layer_Menu); + } + if (Layer_Modal) + { + RegisterLayer(UITags::Tag__UI_Layer_Modal, Layer_Modal); + } +} + diff --git a/Source/PHY/Private/UI/PHYGameUILayout.h b/Source/PHY/Private/UI/PHYGameUILayout.h new file mode 100644 index 0000000..ef14836 --- /dev/null +++ b/Source/PHY/Private/UI/PHYGameUILayout.h @@ -0,0 +1,47 @@ +// Copyright 2026 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/GUIS_GameUILayout.h" +#include "PHYGameUILayout.generated.h" + +class UCommonActivatableWidgetStack; +class UCommonActivatableWidgetContainerBase; + +/** + * Root UI Layout for a single local player. + * + * In BP (Widget Blueprint derived from this), you should: + * - Create several UCommonActivatableWidgetStack (or other ContainerBase) widgets + * - Bind them to the properties below + * - In PreConstruct/Construct, call RegisterLayer for each stack with tags from UITags + */ +UCLASS(Abstract, BlueprintType) +class PHY_API UPHYGameUILayout : public UGUIS_GameUILayout +{ + GENERATED_BODY() + +public: + UPHYGameUILayout(const FObjectInitializer& ObjectInitializer); + +protected: + virtual void NativeOnInitialized() override; + + /** Game HUD layer stack. Tag: UI.Layer.Game */ + UPROPERTY(meta=(BindWidgetOptional), BlueprintReadOnly) + TObjectPtr Layer_Game = nullptr; + + /** In-game menu layer stack. Tag: UI.Layer.GameMenu */ + UPROPERTY(meta=(BindWidgetOptional), BlueprintReadOnly) + TObjectPtr Layer_GameMenu = nullptr; + + /** Main menu layer stack. Tag: UI.Layer.Menu */ + UPROPERTY(meta=(BindWidgetOptional), BlueprintReadOnly) + TObjectPtr Layer_Menu = nullptr; + + /** Modal layer stack. Tag: UI.Layer.Modal */ + UPROPERTY(meta=(BindWidgetOptional), BlueprintReadOnly) + TObjectPtr Layer_Modal = nullptr; +}; + diff --git a/Source/PHY/Private/UI/PHYGameUIPolicy.h b/Source/PHY/Private/UI/PHYGameUIPolicy.h new file mode 100644 index 0000000..fca4a7d --- /dev/null +++ b/Source/PHY/Private/UI/PHYGameUIPolicy.h @@ -0,0 +1,20 @@ +// Copyright 2026 PHY. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/GUIS_GameUIPolicy.h" +#include "PHYGameUIPolicy.generated.h" + +/** + * Game-specific UI policy. + * + * This class mainly exists so we can set up a default LayoutClass in a Blueprint child, + * and keep all UI wiring inside the game module. + */ +UCLASS(BlueprintType) +class PHY_API UPHYGameUIPolicy : public UGUIS_GameUIPolicy +{ + GENERATED_BODY() +}; + diff --git a/Source/PHY/Private/UI/Synty/Synty_IconFrame.cpp b/Source/PHY/Private/UI/Synty/Synty_IconFrame.cpp new file mode 100644 index 0000000..1eb94ae --- /dev/null +++ b/Source/PHY/Private/UI/Synty/Synty_IconFrame.cpp @@ -0,0 +1,40 @@ +// + + +#include "Synty_IconFrame.h" + +#include "Components/Image.h" +#include "Kismet/KismetMaterialLibrary.h" + +void USynty_IconFrame::UpdateIconImage(TSoftObjectPtr NewTexture) +{ + IconTexture = NewTexture; + if (IconImage && !IconTexture.IsNull()) + { + if (IconMaterialInstance) + { + IconMaterialInstance->SetTextureParameterValue(FName("InputTexture"), IconTexture.LoadSynchronous()); + FSlateBrush Brush; + Brush.SetResourceObject(IconMaterialInstance); + Brush.DrawAs = ESlateBrushDrawType::Box; + Brush.SetImageSize(IconSize); + Brush.Margin = IconPadding; + IconImage->SetBrush(Brush); + } + else + { + IconImage->SetBrushFromTexture(IconTexture.LoadSynchronous()); + } + } + +} + +void USynty_IconFrame::NativePreConstruct() +{ + Super::NativePreConstruct(); + if (IconMaterialInterface) + { + IconMaterialInstance = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, IconMaterialInterface); + } + UpdateIconImage(IconTexture); +} diff --git a/Source/PHY/Private/UI/Synty/Synty_IconFrame.h b/Source/PHY/Private/UI/Synty/Synty_IconFrame.h new file mode 100644 index 0000000..03f365c --- /dev/null +++ b/Source/PHY/Private/UI/Synty/Synty_IconFrame.h @@ -0,0 +1,34 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Synty_IconFrame.generated.h" + +/** + * Synty风格的图标框架Widget + */ +UCLASS() +class PHY_API USynty_IconFrame : public UUserWidget +{ + GENERATED_BODY() + + UPROPERTY(meta=(BindWidget)) + class UImage* IconImage; + + UPROPERTY(EditAnywhere,Category="Synty|Config") + TSoftObjectPtr IconTexture; + UPROPERTY(EditAnywhere,Category="Synty|Config") + FVector2D IconSize; + UPROPERTY(EditAnywhere,Category="Synty|Config") + FMargin IconPadding; + UPROPERTY(EditAnywhere,Category="Synty|Config") + UMaterialInterface* IconMaterialInterface; + UPROPERTY() + UMaterialInstanceDynamic* IconMaterialInstance; +public: + void UpdateIconImage(TSoftObjectPtr NewTexture); +protected: + virtual void NativePreConstruct() override; +}; diff --git a/Source/PHYEditor.Target.cs b/Source/PHYEditor.Target.cs new file mode 100644 index 0000000..ffd3a09 --- /dev/null +++ b/Source/PHYEditor.Target.cs @@ -0,0 +1,21 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class PHYEditorTarget : TargetRules +{ + public PHYEditorTarget( TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V6; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7; + ExtraModuleNames.Add("PHY"); + RegisterModulesCreatedByRider(); + } + + private void RegisterModulesCreatedByRider() + { + ExtraModuleNames.AddRange(new string[] { "PHYInventory" }); + } +} diff --git a/Source/PHYInventory/PHYInventory.Build.cs b/Source/PHYInventory/PHYInventory.Build.cs new file mode 100644 index 0000000..5d71b7f --- /dev/null +++ b/Source/PHYInventory/PHYInventory.Build.cs @@ -0,0 +1,34 @@ +using UnrealBuildTool; + +public class PHYInventory : ModuleRules +{ + public PHYInventory(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "GenericUISystem", + "GenericInputSystem", + "CommonUI", + "UMG", + "Slate", + "SlateCore", + "GameplayTags", + "GenericInventorySystem" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore" + } + ); + } +} \ No newline at end of file diff --git a/Source/PHYInventory/Private/ItemFilterInterface.cpp b/Source/PHYInventory/Private/ItemFilterInterface.cpp new file mode 100644 index 0000000..0e8b0e9 --- /dev/null +++ b/Source/PHYInventory/Private/ItemFilterInterface.cpp @@ -0,0 +1,7 @@ +// + + +#include "ItemFilterInterface.h" + + +// Add default functionality here for any IItemFilterInterface functions that are not pure virtual. diff --git a/Source/PHYInventory/Private/PHYInventory.cpp b/Source/PHYInventory/Private/PHYInventory.cpp new file mode 100644 index 0000000..bc15dea --- /dev/null +++ b/Source/PHYInventory/Private/PHYInventory.cpp @@ -0,0 +1,17 @@ +#include "PHYInventory.h" + +#define LOCTEXT_NAMESPACE "FPHYInventoryModule" + +void FPHYInventoryModule::StartupModule() +{ + +} + +void FPHYInventoryModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FPHYInventoryModule, PHYInventory) \ No newline at end of file diff --git a/Source/PHYInventory/Private/UI/ItemStacks/ItemData.cpp b/Source/PHYInventory/Private/UI/ItemStacks/ItemData.cpp new file mode 100644 index 0000000..5ffe55a --- /dev/null +++ b/Source/PHYInventory/Private/UI/ItemStacks/ItemData.cpp @@ -0,0 +1,35 @@ +// + + +#include "UI/ItemStacks/ItemData.h" + +#include "UI/ItemStacks/ItemStackContainer.h" + +UItemStackContainer* UItemData::GetContainer() const +{ + return OwningItemStackContainer.Get(); +} + +void UItemData::SetContainer(UItemStackContainer* InContainer) +{ + OwningItemStackContainer = InContainer; +} + +void UItemData::Reset() +{ + ItemInfo = FGIS_ItemInfo(); +} + +bool UItemData::IsValidItem() const +{ + return ItemInfo.IsValid(); +} + +int32 UItemData::GetItemSlotIndex() const +{ + if (OwningItemStackContainer.IsValid()) + { + return OwningItemStackContainer.Get()->FindItemSlotIndex(this); + } + return INDEX_NONE; +} diff --git a/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDragDropOperation.cpp b/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDragDropOperation.cpp new file mode 100644 index 0000000..64a728e --- /dev/null +++ b/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDragDropOperation.cpp @@ -0,0 +1,66 @@ +// + + +#include "UI/ItemStacks/ItemDataDragDropOperation.h" +#include "UI/ItemStacks/ItemData.h" +#include "UI/ItemStacks/ItemStackContainer.h" + +UItemStackContainer* UItemDataDragDropOperation::GetTargetItemStackView() const +{ + return TargetItemStackView.Get(); +} + +void UItemDataDragDropOperation::SetTargetItemStackView(UItemStackContainer* InItemStackContainer) +{ + TargetItemStackView = TWeakObjectPtr(InItemStackContainer); +} + +UItemData* UItemDataDragDropOperation::GetTargetItemData() const +{ + return TargetItemData.Get(); +} + +void UItemDataDragDropOperation::SetTargetItemData(UItemData* InItemData) +{ + TargetItemData = TWeakObjectPtr(InItemData); +} + +int32 UItemDataDragDropOperation::GetTargetItemIndex() const +{ + return TargetItemIndex; +} + +void UItemDataDragDropOperation::SetTargetItemIndex(int32 InTargetItemIndex) +{ + TargetItemIndex = InTargetItemIndex; +} + +UItemStackContainer* UItemDataDragDropOperation::GetSourceItemStackView() const +{ + return SourceItemStackView.Get(); +} + +void UItemDataDragDropOperation::SetSourceItemStackView(UItemStackContainer* InItemStackContainer) +{ + SourceItemStackView = TWeakObjectPtr(InItemStackContainer); +} + +UItemData* UItemDataDragDropOperation::GetSourceItemData() const +{ + return SourceItemData.Get(); +} + +void UItemDataDragDropOperation::SetSourceItemData(UItemData* InItemData) +{ + SourceItemData = TWeakObjectPtr(InItemData); +} + +int32 UItemDataDragDropOperation::GwtSourceItemIndex() const +{ + return SourceItemIndex; +} + +void UItemDataDragDropOperation::SetSourceItemIndex(int32 InSourceItemIndex) +{ + SourceItemIndex = InSourceItemIndex; +} diff --git a/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDraggingWidget.cpp b/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDraggingWidget.cpp new file mode 100644 index 0000000..a4a3fa0 --- /dev/null +++ b/Source/PHYInventory/Private/UI/ItemStacks/ItemDataDraggingWidget.cpp @@ -0,0 +1,38 @@ +// + + +#include "UI/ItemStacks/ItemDataDraggingWidget.h" + +#include "CommonTextBlock.h" +#include "Components/Image.h" +#include "Components/SizeBox.h" + +void UItemDataDraggingWidget::NativePreConstruct() +{ + Super::NativePreConstruct(); + if (MainSizeBox) + { + MainSizeBox->SetHeightOverride(Height); + MainSizeBox->SetWidthOverride(Width); + } + SetAmount(ItemAmount); + SetIcon(IconTexture); +} + +void UItemDataDraggingWidget::SetAmount(const int32 InAmount) +{ + ItemAmount = InAmount; + if (AmountText) + { + AmountText->SetText(FText::AsNumber(ItemAmount)); + } +} + +void UItemDataDraggingWidget::SetIcon(UTexture2D* InIconTexture) +{ + IconTexture = InIconTexture; + if (IconImage) + { + IconImage->SetBrushFromTexture(IconTexture); + } +} diff --git a/Source/PHYInventory/Private/UI/ItemStacks/ItemStackContainer.cpp b/Source/PHYInventory/Private/UI/ItemStacks/ItemStackContainer.cpp new file mode 100644 index 0000000..5d9ae3e --- /dev/null +++ b/Source/PHYInventory/Private/UI/ItemStacks/ItemStackContainer.cpp @@ -0,0 +1,493 @@ +// + + +#include "UI/ItemStacks/ItemStackContainer.h" + +#include "CommonTabListWidgetBase.h" +#include "GIS_InventoryFunctionLibrary.h" +#include "GIS_InventorySystemComponent.h" +#include "GIS_ItemCollection.h" +#include "GIS_ItemSlotCollection.h" +#include "ItemFilterInterface.h" +#include "Async/GIS_AsyncAction_WaitInventorySystem.h" +#include "Components/ListView.h" + +void UItemStackContainer::SetOwningActor_Implementation(AActor* NewOwningActor) +{ + OwningActor = NewOwningActor; +} + +void UItemStackContainer::OnDeactivated_Implementation() +{ + IGUIS_UserWidgetInterface::OnDeactivated_Implementation(); +} + +void UItemStackContainer::HandleTabSelected(const FName TabId) +{ + ApplyFilter(TabId); +} + +void UItemStackContainer::HandleInventorySystemInitialized() +{ + WaitInventorySystem = nullptr; + if (OwningActor.IsValid()) + { + InventorySystemComponent = UGIS_InventorySystemComponent::GetInventorySystemComponent(OwningActor.Get()); + if (InventorySystemComponent.IsValid()) + { + // 订阅库存系统的消息 + InventorySystemComponent.Get()->OnInventoryStackUpdate.AddDynamic(this,&UItemStackContainer::HandleInventoryStackChanged); + } + if (FilterTabsReference) + { + FilterTabsReference.Get()->OnTabSelected.AddDynamic(this,&UItemStackContainer::HandleTabSelected); + } + // 创建默认的item data槽位 + CreateDefaultItemDataSlots(); + // 同步数据 + SyncAll(); + } +} + +void UItemStackContainer::CreateDefaultItemDataSlots() +{ + if (!InventorySystemComponent.IsValid()) return; + UGIS_ItemCollection* OutCollection; + // 从库存系统中获取集合,并添加到item data中 + if (InventorySystemComponent->FindTypedCollectionByTag(CollectionToTrackTag,UGIS_ItemSlotCollection::StaticClass(),OutCollection)) + { + if (UGIS_ItemSlotCollection* SlotCollection = Cast(OutCollection)) + { + const TArray& Definitions = SlotCollection->GetMyDefinition()->GetSlotDefinitions(); + for (int i = 0; i < Definitions.Num(); ++i) + { + SetOrAddItemInfoToItemDataArray(FGIS_ItemInfo(),i); + } + for (int i = 0; i < ItemDataArray.Num(); ++i) + { + ItemDataArray[i]->SetItemSlotDefinition(Definitions[i]); + } + return; + } + } + // 如果没有找到slot collection,则创建默认数量的item data槽位 + if (bCreateDefaultItemDataSlots) + { + for (int i = 0; i < DefaultItemDataSlotCount; ++i) + { + SetOrAddItemInfoToItemDataArray(FGIS_ItemInfo(),i); + } + } +} + +void UItemStackContainer::OnActivated_Implementation() +{ + // 注册list view events + RegisterListViewEvents(); + OwningActor = Execute_GetOwningActor(this); + if (OwningActor.IsValid()) + { + WaitInventorySystem = UGIS_AsyncAction_WaitInventorySystemInitialized::WaitInventorySystemInitialized(this,OwningActor.Get()); + if (WaitInventorySystem) + { + WaitInventorySystem->OnCompleted.AddDynamic(this,&UItemStackContainer::HandleInventorySystemInitialized); + } + } + +} + +AActor* UItemStackContainer::GetOwningActor_Implementation() +{ + if (!OwningActor.IsValid()) + { + if (GetOwningPlayerPawn()) + { + OwningActor = GetOwningPlayerPawn(); + } + else + { + OwningActor = GetOwningPlayer(); + } + } + return OwningActor.Get(); +} + +void UItemStackContainer::AddFilterObject(UObject* InFilterObject) +{ + if (InFilterObject && InFilterObject->Implements()) + { + ItemFilterObjects.AddUnique(InFilterObject); + } +} + +void UItemStackContainer::RemoveFilterObject(UObject* InFilterObject) +{ + if (InFilterObject && InFilterObject->Implements()) + { + ItemFilterObjects.Remove(InFilterObject); + } +} + +void UItemStackContainer::SetSelectedIndexForNewItem(const int32 NewSelectedIndex) +{ + SelectedIndexForNewItem = NewSelectedIndex; +} + +bool UItemStackContainer::SwapItemDataSlots(const int32 IndexA, const int32 IndexB) +{ + if (!CanMoveItem(IndexA,this,IndexB)) return false; + if (ItemDataArray.IsValidIndex(IndexA) && ItemDataArray.IsValidIndex(IndexB)) + { + // 缓存A的数据 + const UItemData* TempItemData = ItemDataArray[IndexA]; + const UItemData* TargetItemData = ItemDataArray[IndexB]; + // 缓存其中的数据 + TOptional TempItemInfo = TempItemData ? TOptional(TempItemData->GetItemInfo()) : TOptional(); + // 交换数据 + if (TOptional TempTargetItemInfo = TargetItemData ? TOptional(TargetItemData->GetItemInfo()) : TOptional(); TempTargetItemInfo.IsSet()) + { + AssignItemInfoToItemData(TempTargetItemInfo.GetValue(), IndexA); + } + if (TempItemInfo.IsSet()) + { + AssignItemInfoToItemData(TempItemInfo.GetValue(), IndexB); + } + LastDropIndex = IndexB; + // 同步list view + SyncItemDataToListView(); + return true; + } + return false; +} + +bool UItemStackContainer::SwapItemDataToOtherContainer(const int32 SourceIndex, UItemStackContainer* TargetContainer, + const int32 TargetIndex) +{ + if (!TargetContainer) return false; + if (!CanMoveItem(SourceIndex,TargetContainer,TargetIndex)) return false; + if (ItemDataArray.IsValidIndex(SourceIndex) && TargetContainer->GetItemDataArray().IsValidIndex(TargetIndex)) + { + // 缓存A的数据 + const UItemData* TempItemData = ItemDataArray[SourceIndex]; + const UItemData* TargetItemData = TargetContainer->GetItemDataArray()[TargetIndex]; + // 缓存其中的数据 + TOptional TempItemInfo = TempItemData ? TOptional(TempItemData->GetItemInfo()) : TOptional(); + if (TOptional TempTargetItemInfo = TargetItemData ? TOptional(TargetItemData->GetItemInfo()) : TOptional(); TempTargetItemInfo.IsSet()) + { + AssignItemInfoToItemData(TempTargetItemInfo.GetValue(), SourceIndex); + } + if (TempItemInfo.IsSet()) + { + TargetContainer->AssignItemInfoToItemData(TempItemInfo.GetValue(), TargetIndex); + } + TargetContainer->LastDropIndex = TargetIndex; + // 同步list view + SyncItemDataToListView(); + TargetContainer->SyncItemDataToListView(); + return true; + } + return false; +} + +bool UItemStackContainer::CanMoveItem(const int32 IndexA,UItemStackContainer* TargetContainer,const int32 IndexB) +{ + return true; +} + +int32 UItemStackContainer::FindItemSlotIndex(const UItemData* InItemData) const +{ + if (!InItemData) return INDEX_NONE; + return ItemDataArray.IndexOfByKey(InItemData); +} + +void UItemStackContainer::AssignItemInfoToItemData(const FGIS_ItemInfo& InInfo, const int32 SlotIndex) +{ + if (ItemDataArray.IsValidIndex(SlotIndex)) + { + if (UItemData* ItemData = ItemDataArray[SlotIndex]) + { + ItemData->SetItemInfo(InInfo); + } + } +} + +void UItemStackContainer::UnassignItemInfoToItemData(const FGIS_ItemInfo& InInfo) +{ + if (const int32 Index = FindItemIndexFromDataByStackID(InInfo.StackId); Index != INDEX_NONE) + { + const FGIS_ItemInfo EmptyInfo; + AssignItemInfoToItemData(EmptyInfo, Index); + } +} + +int32 UItemStackContainer::FindItemIndexFromDataByStackID(const FGuid& StackID) const +{ + for (int32 Index = 0; Index < ItemDataArray.Num(); ++Index) + { + if (const UItemData* ItemData = ItemDataArray[Index]) + { + if (ItemData->GetItemInfo().StackId == StackID) + { + return Index; + } + } + } + return INDEX_NONE; +} + +TArray UItemStackContainer::GetItemInfoArrayFromInventorySystem() +{ + TArray LocalItemInfos; + if (InventorySystemComponent.IsValid()) + { + if (CollectionToTrackTag.IsValid()) + { + if (const UGIS_ItemCollection* ItemCollection = InventorySystemComponent.Get()->GetCollectionByTag(CollectionToTrackTag)) + { + LocalItemInfos = ItemCollection->GetAllItemInfos(); + } + } + } + if (LocalItemInfos.IsEmpty()) return LocalItemInfos; + // 通过配置的name,和query 过滤 + if (ItemFilterMap.Num() > 0 && ItemFilterName.IsValid()) + { + if (const FGameplayTagQuery* Query = ItemFilterMap.Find(ItemFilterName)) + { + LocalItemInfos = UGIS_InventoryFunctionLibrary::FilterItemInfosByTagQuery(LocalItemInfos,*Query); + } + } + // 通过Object过滤 + if (ItemFilterObjects.Num() > 0) + { + for (UObject* FilterObject : ItemFilterObjects) + { + if (FilterObject && FilterObject->Implements()) + { + LocalItemInfos = IItemFilterInterface::Execute_FilterItemInfo(FilterObject,LocalItemInfos); + } + } + } + return LocalItemInfos; +} + +void UItemStackContainer::ResetItemDataArray() +{ + for (UItemData* ItemData : ItemDataArray) + { + ItemData->Reset(); + } +} + +void UItemStackContainer::SetOrAddItemInfoToItemDataArray(const FGIS_ItemInfo& InInfo, const int32 SlotIndex) +{ + if (SlotIndex < 0) + { + return; + } + // 确保数组大小足够 + if (ItemDataArray.Num() <= SlotIndex) + { + ItemDataArray.SetNum(SlotIndex + 1); + } + + // 确保该槽位有对象 + UItemData* ItemDataRef = ItemDataArray[SlotIndex]; + if (!ItemDataRef) + { + ItemDataRef = NewObject(this); + } + ItemDataRef->SetItemInfo(InInfo); +} + +void UItemStackContainer::SyncItemInfoToItemDataArray(const TArray& InItemInfos) +{ + if (InventorySystemComponent.IsValid() && CollectionToTrackTag.IsValid()) + { + // 获取集合,如果是slotted collection则根据slot来赋值 + if (const UGIS_ItemSlotCollection* SlotCollection = Cast(InventorySystemComponent->GetCollectionByTag(CollectionToTrackTag))) + { + for (int i = 0; i < InItemInfos.Num(); ++i) + { + const FGIS_ItemInfo& Info = InItemInfos[i]; + const int32 SlotIndex = SlotCollection->GetItemSlotIndex(Info.Item); + SetOrAddItemInfoToItemDataArray(Info, SlotIndex == INDEX_NONE ? i : SlotIndex); + } + } + else + { + // 如果是普通的集合,则直接按顺序赋值 + for (int i = 0; i < InItemInfos.Num(); ++i) + { + SetOrAddItemInfoToItemDataArray(InItemInfos[i], i); + } + } + } +} + +void UItemStackContainer::SyncItemDataToListView() +{ + if (ItemListView == nullptr) return; + ItemListView->ClearSelection(); + ItemListView->ClearListItems(); + ItemListView->SetListItems(ItemDataArray); + ItemListView->RegenerateAllEntries(); + // 恢复上次拖拽的选中状态 + if (ItemDataArray.IsValidIndex(LastDropIndex) && LastDropIndex != INDEX_NONE) + { + ItemListView->SetSelectedItem(ItemDataArray[LastDropIndex]); + LastDropIndex = INDEX_NONE; + } else + { + // 默认选中第一个 + if (ItemDataArray.IsValidIndex(0) && ItemDataArray[0]->IsValidItem()) + { + ItemListView->SetSelectedItem(ItemDataArray[0]); + } + } +} + +int32 UItemStackContainer::FindSuitableItemDataSlotForNewItem() +{ + if (SelectedIndexForNewItem == INDEX_NONE) + { + for (int i = 0; i < ItemDataArray.Num(); ++i) + { + if (const UItemData* ItemData = ItemDataArray[i]) + { + if (!ItemData->IsValidItem()) + { + return i; + } + } + } + return ItemDataArray.Num(); + } + const int32 ReturnIndex = SelectedIndexForNewItem; + SelectedIndexForNewItem = INDEX_NONE; + return ReturnIndex; +} + +void UItemStackContainer::SyncAll() +{ + ResetItemDataArray(); + SyncItemInfoToItemDataArray(GetItemInfoArrayFromInventorySystem()); + SyncItemDataToListView(); +} + +void UItemStackContainer::ApplyFilter(const FName& InFilterName) +{ + if (InFilterName.IsNone()) return; + if (ItemFilterMap.Contains(InFilterName)) + { + ItemFilterName = InFilterName; + SyncAll(); + } +} + +void UItemStackContainer::HandleItemSelectionChanged(UObject* Object) const +{ + if (UItemData* SelectedItemData = Cast(Object)) + { + OnItemSelectionChanged.Broadcast(SelectedItemData,true); + return; + } + OnItemSelectionChanged.Broadcast(nullptr,false); +} + +void UItemStackContainer::HandleItemHoveredChanged(UObject* Object, bool bIsHovered) const +{ + if (UItemData* HoveredItemData = Cast(Object)) + { + OnItemHoveredChanged.Broadcast(HoveredItemData,bIsHovered); + } +} + +void UItemStackContainer::HandleItemClicked(UObject* Object) const +{ + if (UItemData* ClickedItemData = Cast(Object)) + { + OnItemClicked.Broadcast(ClickedItemData); + } +} + +void UItemStackContainer::HandleListViewEntryWidgetGenerated(UUserWidget& UserWidget) const +{ + OnItemEntryGenerated.Broadcast(&UserWidget); +} + +void UItemStackContainer::HandleItemDoubleClicked(UObject* Object) const +{ + if (UItemData* ClickedItemData = Cast(Object)) + { + OnItemDoubleClicked.Broadcast(ClickedItemData); + } +} + +void UItemStackContainer::RegisterListViewEvents() +{ + if (ItemListView) + { + // 注册事件 + ItemSelectionChangedHandle = ItemListView->OnItemSelectionChanged().AddUObject(this, &UItemStackContainer::HandleItemSelectionChanged); + ItemHoveredChangedHandle = ItemListView->OnItemIsHoveredChanged().AddUObject(this, &UItemStackContainer::HandleItemHoveredChanged); + ItemClickedHandle = ItemListView->OnItemClicked().AddUObject(this, &UItemStackContainer::HandleItemClicked); + ListViewEntryWidgetGeneratedHandle = ItemListView->OnEntryWidgetGenerated().AddUObject(this, &UItemStackContainer::HandleListViewEntryWidgetGenerated); + ItemDoubleClickedHandle = ItemListView->OnItemDoubleClicked().AddUObject(this, &UItemStackContainer::HandleItemDoubleClicked); + } +} + +void UItemStackContainer::UnregisterListViewEvents() const +{ + if (ItemListView) + { + // 注销事件 + ItemListView->OnItemSelectionChanged().Remove(ItemSelectionChangedHandle); + ItemListView->OnItemIsHoveredChanged().Remove(ItemHoveredChangedHandle); + ItemListView->OnItemClicked().Remove(ItemClickedHandle); + ItemListView->OnEntryWidgetGenerated().Remove(ListViewEntryWidgetGeneratedHandle); + ItemListView->OnItemDoubleClicked().Remove(ItemDoubleClickedHandle); + } +} + +void UItemStackContainer::HandleInventoryStackChanged(const FGIS_InventoryStackUpdateMessage& Message) +{ + if (const UGIS_InventorySystemComponent* Inventory = Message.Inventory) + { + // 仅处理关联的库存系统组件的消息 + if (!InventorySystemComponent.IsValid() && Inventory != InventorySystemComponent.Get()) return; + // 获取集合 + if (UGIS_ItemCollection* ItemCollection = Inventory->GetCollectionById(Message.CollectionId)) + { + // 仅处理配置的集合tag的消息 + if (ItemCollection->GetCollectionTag() != CollectionToTrackTag) return; + FGIS_ItemInfo ChangedItemInfo = FGIS_ItemInfo(Message.Instance,Message.NewCount,ItemCollection,Message.StackId); + switch (Message.ChangeType) { + case EGIS_ItemStackChangeType::WasAdded: + // 添加 + { + const int32 SlotIndex = FindSuitableItemDataSlotForNewItem(); + SetOrAddItemInfoToItemDataArray(ChangedItemInfo, SlotIndex); + } + break; + case EGIS_ItemStackChangeType::WasRemoved: + // 移除 + { + ChangedItemInfo.Amount = Message.NewCount - Message.Delta; + UnassignItemInfoToItemData(ChangedItemInfo); + } + break; + case EGIS_ItemStackChangeType::Changed: + // 修改 + { + if (const int32 ChangedIndex = FindItemIndexFromDataByStackID(Message.StackId); ChangedIndex != INDEX_NONE) + { + SetOrAddItemInfoToItemDataArray(ChangedItemInfo, ChangedIndex); + } + } + break; + } + SyncItemDataToListView(); + } + } +} diff --git a/Source/PHYInventory/Private/UI/ItemStacks/ItemStack_Base.cpp b/Source/PHYInventory/Private/UI/ItemStacks/ItemStack_Base.cpp new file mode 100644 index 0000000..04fb0fe --- /dev/null +++ b/Source/PHYInventory/Private/UI/ItemStacks/ItemStack_Base.cpp @@ -0,0 +1,186 @@ +// + + +#include "UI/ItemStacks/ItemStack_Base.h" + +#include "GIS_ItemDefinition.h" +#include "GIS_ItemInstance.h" +#include "Components/ListView.h" +#include "Components/MenuAnchor.h" +#include "Components/OverlaySlot.h" +#include "UI/Actions/GUIS_UIActionWidget.h" +#include "UI/ItemStacks/ItemData.h" +#include "UI/ItemStacks/ItemDataDragDropOperation.h" +#include "UI/ItemStacks/ItemDataDraggingWidget.h" +#include "UI/ItemStacks/ItemStackContainer.h" +#include "UI/Widgets/Normal/AmountContainerBase.h" + +void UItemStack_Base::UpdateAmount() +{ + if (!AmountContainer) return; + if (ItemData && ItemData->IsValidItem()) + { + AmountContainer->UpdateAmount(ItemData->GetItemInfo().Amount); + AmountContainer->SetVisibility(ESlateVisibility::SelfHitTestInvisible); + return; + } + AmountContainer->SetVisibility(ESlateVisibility::Collapsed); +} + +void UItemStack_Base::ResetState() +{ + if (DynamicUIActionWidget) { + DynamicUIActionWidget->UnregisterActions(); + } + if (ActionsAnchor) { + ActionsAnchor->Close(); + } +} + +void UItemStack_Base::NativeOnListItemObjectSet(UObject* ListItemObject) +{ + Super::NativeOnListItemObjectSet(ListItemObject); + ItemData = Cast(ListItemObject); + UpdateAmount(); + ResetState(); + //将UI数据与UI操作空间关联 + if (DynamicUIActionWidget) { + DynamicUIActionWidget->SetAssociatedData(ItemData); + } +} + + + +void UItemStack_Base::NativeOnEntryReleased() +{ + Super::NativeOnEntryReleased(); + ResetState(); + if (DynamicUIActionWidget) { + DynamicUIActionWidget->SetAssociatedData(nullptr); + } +} + +void UItemStack_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + if (ActionsAnchor) + { + if (UOverlaySlot* OverlaySlot = Cast(ActionsAnchor->Slot)) + { + OverlaySlot->SetHorizontalAlignment(ActionAnchorHorizontalAlignment); + OverlaySlot->SetVerticalAlignment(ActionAnchorVerticalAlignment); + } + } +} + +void UItemStack_Base::NativeOnDeselected(bool bBroadcast) +{ + Super::NativeOnDeselected(bBroadcast); + if (ItemData && ItemData->GetContainer()) + { + if (ItemData->GetContainer()->IsItemActionAllowed()) + { + if (DynamicUIActionWidget) + { + DynamicUIActionWidget->UnregisterActions(); + } + } + } +} + +void UItemStack_Base::NativeOnSelected(bool bBroadcast) +{ + Super::NativeOnSelected(bBroadcast); + if (ItemData && ItemData->GetContainer()) + { + if (ItemData->GetContainer()->IsItemActionAllowed()) + { + if (DynamicUIActionWidget) + { + DynamicUIActionWidget->RegisterActionsWithFactory(UIActionFactory); + } + } + } +} + +UItemDataDragDropOperation* UItemStack_Base::DragFromSource(const FString& Tag) +{ + if (!DraggingWidgetClass || !ItemData || !ItemData->IsValidItem()) return nullptr; + + // 创建拖拽视觉widget + UItemDataDraggingWidget* DraggingWidget = CreateWidget(this, DraggingWidgetClass); + if (!DraggingWidget) return nullptr; + if (const UGIS_ItemDefinition* ItemDef = ItemData->GetItemInfo().Item.Get()->GetDefinition(); ItemDef && ItemDef->Icon) + { + DraggingWidget->SetIcon(ItemDef->Icon); + } + DraggingWidget->SetAmount(ItemData->GetItemInfo().Amount); + // 创建拖拽操作 + UItemDataDragDropOperation* DragDropOp = NewObject(this); + DragDropOp->DefaultDragVisual = DraggingWidget; + DragDropOp->Pivot = EDragPivot::CenterCenter; + DragDropOp->Tag = Tag; + // 设置源数据 + DragDropOp->SetSourceItemData(ItemData); + DragDropOp->SetSourceItemStackView(ItemData->GetContainer()); + DragDropOp->SetSourceItemIndex(ItemData->GetItemSlotIndex()); + return DragDropOp; +} + +void UItemStack_Base::DropToDest(UItemDataDragDropOperation& DragOperation) const +{ + if (!ItemData || !ItemData->IsValidItem()) return + DragOperation.SetTargetItemData(ItemData); + DragOperation.SetTargetItemStackView(ItemData->GetContainer()); + DragOperation.SetTargetItemIndex(ItemData->GetItemSlotIndex()); +} + +void UItemStack_Base::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, + UDragDropOperation*& OutOperation) +{ + Super::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation); + if (!ItemData || !ItemData->GetContainer()) return; + if (ItemData->GetContainer()->IsItemActionAllowed() && ItemData->IsValidItem()) + { + ActionsAnchor->Close(); + if (UItemDataDragDropOperation* DragOp = DragFromSource(TEXT("MoveIndex"))) + { + OutOperation = DragOp; + } + } +} + +bool UItemStack_Base::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, + UDragDropOperation* InOperation) +{ + if (ItemData && ItemData->GetContainer() && ItemData->GetContainer()->IsItemActionAllowed()) + { + if (UItemDataDragDropOperation* DragOp = Cast(InOperation)) + { + DropToDest(*DragOp); + if (DragOp->Tag == TEXT("MoveIndex")) + { + UItemStackContainer* SourceContainer = DragOp->GetSourceItemStackView(); + UItemStackContainer* TargetContainer = DragOp->GetTargetItemStackView(); + const int32 SourceIndex = DragOp->GwtSourceItemIndex(); + const int32 TargetIndex = DragOp->GetTargetItemIndex(); + if (SourceContainer && TargetContainer) + { + if (SourceContainer == TargetContainer) + { + // 交换数据槽位 + SourceContainer->SwapItemDataSlots(SourceIndex, TargetIndex); + return true; + } + + { + // 跨容器交换数据槽位 + SourceContainer->SwapItemDataToOtherContainer(SourceIndex, TargetContainer, TargetIndex); + return true; + } + } + } + } + } + return Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); +} diff --git a/Source/PHYInventory/Private/UI/Widgets/Normal/AmountContainerBase.cpp b/Source/PHYInventory/Private/UI/Widgets/Normal/AmountContainerBase.cpp new file mode 100644 index 0000000..cc885b3 --- /dev/null +++ b/Source/PHYInventory/Private/UI/Widgets/Normal/AmountContainerBase.cpp @@ -0,0 +1,14 @@ +// + + +#include "UI/Widgets/Normal/AmountContainerBase.h" + +#include "CommonTextBlock.h" + +void UAmountContainerBase::UpdateAmount(const float InAmount,const FNumberFormattingOptions* const Options) const +{ + if (AmountText) + { + AmountText->SetText(FText::AsNumber(InAmount, Options)); + } +} diff --git a/Source/PHYInventory/Public/ItemFilterInterface.h b/Source/PHYInventory/Public/ItemFilterInterface.h new file mode 100644 index 0000000..7f017ea --- /dev/null +++ b/Source/PHYInventory/Public/ItemFilterInterface.h @@ -0,0 +1,29 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_ItemInfo.h" +#include "UObject/Interface.h" +#include "ItemFilterInterface.generated.h" + + +// 用于过滤item的接口 +UINTERFACE() +class UItemFilterInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class PHYINVENTORY_API IItemFilterInterface +{ + GENERATED_BODY() + + +public: + UFUNCTION(BlueprintNativeEvent) + TArray FilterItemInfo(const TArray& InItemInfoArray); +}; diff --git a/Source/PHYInventory/Public/PHYInventory.h b/Source/PHYInventory/Public/PHYInventory.h new file mode 100644 index 0000000..c79a675 --- /dev/null +++ b/Source/PHYInventory/Public/PHYInventory.h @@ -0,0 +1,11 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FPHYInventoryModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/PHYInventory/Public/UI/ItemStacks/ItemData.h b/Source/PHYInventory/Public/UI/ItemStacks/ItemData.h new file mode 100644 index 0000000..83f974b --- /dev/null +++ b/Source/PHYInventory/Public/UI/ItemStacks/ItemData.h @@ -0,0 +1,39 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GIS_CoreStructLibray.h" +#include "GIS_ItemInfo.h" +#include "UObject/Object.h" +#include "ItemData.generated.h" + +/** + * + */ + +class UItemStackContainer; + +UCLASS() +class PHYINVENTORY_API UItemData : public UObject +{ + GENERATED_BODY() + +private: + // item info + FGIS_ItemInfo ItemInfo; + // item slot definition + FGIS_ItemSlotDefinition ItemSlotDefinition; + TWeakObjectPtr OwningItemStackContainer; +public: + const FGIS_ItemInfo& GetItemInfo() const { return ItemInfo; } + void SetItemInfo(const FGIS_ItemInfo& InInfo) { ItemInfo = InInfo; } + UItemStackContainer* GetContainer() const; + void SetContainer(UItemStackContainer* InContainer); + + const FGIS_ItemSlotDefinition& GetItemSlotDefinition() const { return ItemSlotDefinition; } + void SetItemSlotDefinition(const FGIS_ItemSlotDefinition& InDef) { ItemSlotDefinition = InDef; } + void Reset(); + bool IsValidItem() const; + int32 GetItemSlotIndex() const; +}; diff --git a/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDragDropOperation.h b/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDragDropOperation.h new file mode 100644 index 0000000..2daec2c --- /dev/null +++ b/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDragDropOperation.h @@ -0,0 +1,44 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/DragDropOperation.h" +#include "ItemDataDragDropOperation.generated.h" + +class UItemData; +class UItemStackContainer; +/** + * + */ +UCLASS() +class PHYINVENTORY_API UItemDataDragDropOperation : public UDragDropOperation +{ + GENERATED_BODY() + +protected: + TWeakObjectPtr TargetItemStackView; + TWeakObjectPtr TargetItemData; + int32 TargetItemIndex = INDEX_NONE; + TWeakObjectPtr SourceItemStackView; + TWeakObjectPtr SourceItemData; + int32 SourceItemIndex = INDEX_NONE; +public: + UItemStackContainer* GetTargetItemStackView() const; + void SetTargetItemStackView(UItemStackContainer* InItemStackContainer); + + UItemData* GetTargetItemData() const; + void SetTargetItemData(UItemData* InItemData); + + int32 GetTargetItemIndex() const; + void SetTargetItemIndex(int32 InTargetItemIndex); + + UItemStackContainer* GetSourceItemStackView() const; + void SetSourceItemStackView(UItemStackContainer* InItemStackContainer); + + UItemData* GetSourceItemData() const; + void SetSourceItemData(UItemData* InItemData); + + int32 GwtSourceItemIndex() const; + void SetSourceItemIndex(int32 InSourceItemIndex); +}; diff --git a/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDraggingWidget.h b/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDraggingWidget.h new file mode 100644 index 0000000..7d00db7 --- /dev/null +++ b/Source/PHYInventory/Public/UI/ItemStacks/ItemDataDraggingWidget.h @@ -0,0 +1,40 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "ItemDataDraggingWidget.generated.h" + +class UCommonTextBlock; +class UImage; +class USizeBox; +/** + * 拖拽时候显示的物品数据小部件 + */ +UCLASS() +class PHYINVENTORY_API UItemDataDraggingWidget : public UCommonUserWidget +{ + GENERATED_BODY() + + UPROPERTY(meta=(BindWidget)) + USizeBox* MainSizeBox; + UPROPERTY(meta=(BindWidget)) + UImage* IconImage; + UPROPERTY(meta=(BindWidget)) + UCommonTextBlock* AmountText; + + UPROPERTY(EditDefaultsOnly) + UTexture2D* IconTexture; + UPROPERTY(EditDefaultsOnly) + int32 ItemAmount = 0; + UPROPERTY(EditDefaultsOnly) + float Height = 64.f; + UPROPERTY(EditDefaultsOnly) + float Width = 64.f; +protected: + virtual void NativePreConstruct() override; +public: + void SetAmount(int32 InAmount); + void SetIcon(UTexture2D* InIconTexture); +}; diff --git a/Source/PHYInventory/Public/UI/ItemStacks/ItemStackContainer.h b/Source/PHYInventory/Public/UI/ItemStacks/ItemStackContainer.h new file mode 100644 index 0000000..bd15761 --- /dev/null +++ b/Source/PHYInventory/Public/UI/ItemStacks/ItemStackContainer.h @@ -0,0 +1,146 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "GIS_InventoryMeesages.h" +#include "GIS_ItemInfo.h" +#include "ItemData.h" + +#include "UI/Common/GUIS_UserWidgetInterface.h" +#include "ItemStackContainer.generated.h" + +class UCommonTabListWidgetBase; +class UGIS_AsyncAction_WaitInventorySystem; +class UListView; +class IItemFilterInterface; +/** + * + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FItemSelectionOrHoveredChanged,UItemData*,SelectedItemData,bool,bSelected); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FItemClickedEvent,UItemData*,ClickedItemData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FItemEntryGeneratedEvent,UUserWidget*,Widget); + +UCLASS() +class PHYINVENTORY_API UItemStackContainer : public UCommonUserWidget,public IGUIS_UserWidgetInterface +{ + GENERATED_BODY() + +protected: + //~ Begin IGUIS_UserWidgetInterface Interface + virtual void SetOwningActor_Implementation(AActor* NewOwningActor) override; + virtual void OnDeactivated_Implementation() override; + virtual void OnActivated_Implementation() override; + virtual AActor* GetOwningActor_Implementation() override; + //~ End IGUIS_UserWidgetInterface Interface +public: + // 添加过滤器对象 + void AddFilterObject(UObject* InFilterObject); + // 移除过滤器对象 + void RemoveFilterObject(UObject* InFilterObject); + UPROPERTY(BlueprintAssignable) + FItemSelectionOrHoveredChanged OnItemSelectionChanged; + UPROPERTY(BlueprintAssignable) + FItemSelectionOrHoveredChanged OnItemHoveredChanged; + UPROPERTY(BlueprintAssignable) + FItemClickedEvent OnItemClicked; + UPROPERTY(BlueprintAssignable) + FItemEntryGeneratedEvent OnItemEntryGenerated; + UPROPERTY(BlueprintAssignable) + FItemClickedEvent OnItemDoubleClicked; + UPROPERTY() + UGIS_AsyncAction_WaitInventorySystem* WaitInventorySystem; + void SetSelectedIndexForNewItem(const int32 NewSelectedIndex); + // 交换两个item data槽位 + bool SwapItemDataSlots(const int32 IndexA,const int32 IndexB); + bool SwapItemDataToOtherContainer(const int32 SourceIndex,UItemStackContainer* TargetContainer,const int32 TargetIndex); + // 检查是否可以移动item + virtual bool CanMoveItem(const int32 IndexA,UItemStackContainer* TargetContainer,const int32 IndexB); + // 是否允许拖拽操作 + UFUNCTION(BlueprintImplementableEvent,BlueprintPure,BlueprintCallable) + bool IsDraggingAllowed() const; + FORCEINLINE bool IsItemActionAllowed() const { return bAllowItemAction; } + int32 FindItemSlotIndex(const UItemData* InItemData) const; + FORCEINLINE TArray> GetItemDataArray() const { return ItemDataArray; } + // 同步item data数组到listview + void SyncItemDataToListView(); + // 用来拖拽操作时记录上一次放下的item index + int32 LastDropIndex = INDEX_NONE; +private: + TWeakObjectPtr OwningActor; + TWeakObjectPtr InventorySystemComponent; + // 配置 + UPROPERTY(EditAnywhere,Category="Config|ItemStackContainer") + FGameplayTag CollectionToTrackTag; + // Item所有tag过滤器映射表,可以通过name来过滤Item,用于tab切换等功能 + UPROPERTY(EditAnywhere,Category="Config|Filter") + TMap ItemFilterMap; + // 要过滤的Item名称 + UPROPERTY(EditAnywhere,Category="Config|Filter") + FName ItemFilterName; + UPROPERTY(EditAnywhere,Category="Config") + bool bAllowItemAction = true; + UPROPERTY(EditAnywhere,Category="Config") + bool bCreateDefaultItemDataSlots = true; + UPROPERTY(EditAnywhere,Category="Config",meta=(EditCondition="bCreateDefaultItemDataSlots")) + int32 DefaultItemDataSlotCount = 20; + UPROPERTY() + TArray ItemFilterObjects; + // 容器用来展示item的listview + UPROPERTY(BlueprintReadOnly,meta=(AllowPrivateAccess="true")) + TObjectPtr ItemListView; + UPROPERTY() + TObjectPtr FilterTabsReference; + // 当前容器中关联的item数据 + UPROPERTY() + TArray> ItemDataArray; + + // The item stack data + // 给指定槽位的ItemData赋值 + void AssignItemInfoToItemData(const FGIS_ItemInfo& InInfo,const int32 SlotIndex); + void UnassignItemInfoToItemData(const FGIS_ItemInfo& InInfo); + int32 FindItemIndexFromDataByStackID(const FGuid& StackID) const; + + // 从库存系统获取数据 + TArray GetItemInfoArrayFromInventorySystem(); + // 重置item data数组 + void ResetItemDataArray(); + // 设置或添加item info到item data数组 + void SetOrAddItemInfoToItemDataArray(const FGIS_ItemInfo& InInfo,const int32 SlotIndex); + // 同步item infos到item data数组 + void SyncItemInfoToItemDataArray(const TArray& InItemInfos); + + // 为新物品查找合适的item data槽位 + int32 FindSuitableItemDataSlotForNewItem(); + // 聚合函数,同步所有数据 + void SyncAll(); +public: + // 必须配置filter map才能使用 + void ApplyFilter(const FName& InFilterName); +private: + void HandleItemSelectionChanged(UObject* Object) const; + void HandleItemHoveredChanged(UObject* Object, bool bIsHovered) const; + void HandleItemClicked(UObject* Object) const; + void HandleListViewEntryWidgetGenerated(UUserWidget& UserWidget) const; + void HandleItemDoubleClicked(UObject* Object) const; + void RegisterListViewEvents(); + void UnregisterListViewEvents() const; + FDelegateHandle ItemSelectionChangedHandle; + FDelegateHandle ItemHoveredChangedHandle; + FDelegateHandle ItemClickedHandle; + FDelegateHandle ListViewEntryWidgetGeneratedHandle; + FDelegateHandle ItemDoubleClickedHandle; + int32 SelectedIndexForNewItem = INDEX_NONE; + + // 处理库存系统的item堆栈变化消息 + UFUNCTION() + void HandleInventoryStackChanged(const FGIS_InventoryStackUpdateMessage& Message); + UFUNCTION() + void HandleTabSelected(FName TabId); + UFUNCTION() + void HandleInventorySystemInitialized(); + // 创建默认的item data槽位 + void CreateDefaultItemDataSlots(); +}; diff --git a/Source/PHYInventory/Public/UI/ItemStacks/ItemStack_Base.h b/Source/PHYInventory/Public/UI/ItemStacks/ItemStack_Base.h new file mode 100644 index 0000000..dbf7fd7 --- /dev/null +++ b/Source/PHYInventory/Public/UI/ItemStacks/ItemStack_Base.h @@ -0,0 +1,60 @@ +// + +#pragma once + +#include "CoreMinimal.h" + +#include "UI/Common/GUIS_ListEntry.h" +#include "ItemStack_Base.generated.h" + +class UItemDataDragDropOperation; +class UItemDataDraggingWidget; +class UAmountContainerBase; +class USizeBox; +class UMenuAnchor; +class UGUIS_UIActionWidget; +class UGUIS_UIActionFactory; +class UItemData; +/** + * + */ +UCLASS() +class PHYINVENTORY_API UItemStack_Base : public UGUIS_ListEntry +{ + GENERATED_BODY() + +private: + UPROPERTY() + UItemData* ItemData; + UPROPERTY(meta=(BindWidgetOptional)) + UGUIS_UIActionWidget* DynamicUIActionWidget; + UPROPERTY(EditAnywhere, Category="Config") + TSoftObjectPtr UIActionFactory; + UPROPERTY(meta=(BindWidgetOptional)) + UMenuAnchor* ActionsAnchor; + UPROPERTY(meta=(BindWidgetOptional)) + UAmountContainerBase* AmountContainer; + UPROPERTY(EditAnywhere, Category="Config") + TEnumAsByte ActionAnchorHorizontalAlignment = HAlign_Right; + UPROPERTY(EditAnywhere, Category="Config") + TEnumAsByte ActionAnchorVerticalAlignment = VAlign_Center; + UPROPERTY(EditAnywhere, Category="Config") + TSubclassOf DraggingWidgetClass; +protected: + virtual void UpdateAmount(); + virtual void ResetState(); + virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override; + + virtual void NativeOnEntryReleased() override; + virtual void NativePreConstruct() override; + virtual void NativeOnDeselected(bool bBroadcast) override; + virtual void NativeOnSelected(bool bBroadcast) override; + + // 拖拽操作 + // 从这个item发起拖拽得时候调用 + UItemDataDragDropOperation* DragFromSource(const FString& Tag); + // 放下到这个item的时候调用 + void DropToDest(UItemDataDragDropOperation& DragOperation) const; + virtual void NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) override; + virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override; +}; diff --git a/Source/PHYInventory/Public/UI/Widgets/Normal/AmountContainerBase.h b/Source/PHYInventory/Public/UI/Widgets/Normal/AmountContainerBase.h new file mode 100644 index 0000000..2507530 --- /dev/null +++ b/Source/PHYInventory/Public/UI/Widgets/Normal/AmountContainerBase.h @@ -0,0 +1,24 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "AmountContainerBase.generated.h" + +/** + * 用来显示数量的容器基类 + */ +UCLASS() +class PHYINVENTORY_API UAmountContainerBase : public UCommonUserWidget +{ + GENERATED_BODY() + +protected: + UPROPERTY(meta=(BindWidget)) + class USizeBox* Container; + UPROPERTY(meta=(BindWidget)) + class UCommonTextBlock* AmountText; +public: + void UpdateAmount(float InAmount, const FNumberFormattingOptions* const Options = nullptr) const; +}; diff --git a/shadertoolsconfig.json b/shadertoolsconfig.json new file mode 100644 index 0000000..9e8ecb4 --- /dev/null +++ b/shadertoolsconfig.json @@ -0,0 +1,8 @@ +{ + "hlsl.preprocessorDefinitions": { + }, + " hlsl.additionalIncludeDirectories": [ + ], + "hlsl.virtualDirectoryMappings": { + } +}