Browse Source

Added a level component for spawning decals over a bus exposed to behavior context. This should allow for script canvas to spawn decals that self-destruct after their lifetime is over, without the need for entities or components.

Signed-off-by: Ken Pruiksma <[email protected]>
(cherry picked from commit e099c0fa6252e73ea8c6f636b1ebd5c5e9ee7923)
Ken Pruiksma 2 years ago
parent
commit
61571fe926

+ 77 - 0
Gem/Code/Include/DecalBus.h

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Math/Transform.h>
+#include <AzCore/EBus/EBus.h>
+#include <Atom/RPI.Public/Scene.h>
+
+namespace MultiplayerSample
+{
+    struct SpawnDecalConfig
+    {
+        AZ_RTTI(MultiplayerSample::SpawnDecalConfig, "{FC3DA616-174B-48FD-9BFB-BC277132FB47}");
+        inline static void Reflect(AZ::ReflectContext* context);
+
+        float m_scale = 1.0f;             // Scale in meters
+        float m_opacity = 1.0f;           // How visible the decal is
+        float m_attenutationAngle = 1.0f; // How much to attenuate based on the angle of the geometry vs the decal
+        float m_lifeTime = 0.0f;          // Time until the decal begins to fade, in seconds.
+        float m_fadeTime = 1.0f;          // Time it takes the decal to fade, in seconds.
+        uint8_t m_sortKey = 0;            // Higher numbers sort in front of lower numbers
+    };
+
+    void SpawnDecalConfig::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serialize->Class<MultiplayerSample::SpawnDecalConfig>()
+                ->Version(0)
+                ;
+        }
+
+        if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<SpawnDecalConfig>("SpawnDecalConfig")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Graphics")
+                ->Attribute(AZ::Script::Attributes::Module, "decals")
+                ->Constructor()
+                ->Constructor<const SpawnDecalConfig&>()
+                ->Property("scale", BehaviorValueProperty(&SpawnDecalConfig::m_scale))
+                ->Property("opacity", BehaviorValueProperty(&SpawnDecalConfig::m_opacity))
+                ->Property("m_attenutationAngle", BehaviorValueProperty(&SpawnDecalConfig::m_attenutationAngle))
+                ->Property("m_lifeTime", BehaviorValueProperty(&SpawnDecalConfig::m_lifeTime))
+                ->Property("m_sortKey", BehaviorValueProperty(&SpawnDecalConfig::m_sortKey))
+                ;
+        }
+    }
+
+    class DecalRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
+        using BusIdType = AZ::RPI::SceneId;
+
+        AZ_RTTI(MultiplayerSample::DecalRequests, "{A3643473-25ED-4F86-8BEE-D65A1A54867B}");
+        virtual ~DecalRequests() = default;
+
+        /**
+         * \brief Spawn a decal
+         * \param worldTm Where to spawn the decal.
+         * \param materialAssetId The asset ID of the material to use for the decal
+         * \param config The configuration of the decal to spawn (opacity, scale, etc).
+         */
+        virtual void SpawnDecal(const AZ::Transform& worldTm, AZ::Data::AssetId materialAssetId, const SpawnDecalConfig& config) = 0;
+    };
+
+    using DecalRequestBus = AZ::EBus<DecalRequests>;
+}

+ 164 - 0
Gem/Code/Source/Components/ScriptableDecalComponent.cpp

@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Components/ScriptableDecalComponent.h>
+
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/std/chrono/chrono.h>
+
+#include <Atom/RPI.Public/Scene.h>
+
+namespace MultiplayerSample
+{
+    void ScriptableDecalComponent::Reflect(AZ::ReflectContext* context)
+    {
+        SpawnDecalConfig::Reflect(context);
+
+        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serialize->Class<MultiplayerSample::ScriptableDecalComponent, AZ::Component>()
+                ->Version(0)
+                ;
+
+            AZ::EditContext* editContext = serialize->GetEditContext();
+            if (editContext)
+            {
+                editContext->Class<MultiplayerSample::ScriptableDecalComponent>(
+                    "Scriptable Decals", "Allows spawning decals directly from script without prefabs.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::Category, "Graphics")
+                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector<AZ::Crc32>({ AZ_CRC_CE("Level") }))
+                    ;
+            }
+        }
+
+        AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
+        if (behaviorContext)
+        {
+            behaviorContext->EBus<DecalRequestBus>("RequestBus")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Rendering")
+                ->Attribute(AZ::Script::Attributes::Module, "rendering")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Event(
+                    "SpawnDecal",
+                    &DecalRequestBus::Events::SpawnDecal)
+                ;
+        }
+    }
+
+    void ScriptableDecalComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
+    {
+        services.push_back(AZ_CRC_CE("ScriptableDecalService"));
+    }
+
+    void ScriptableDecalComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
+    {
+        services.push_back(AZ_CRC_CE("ScriptableDecalService"));
+    }
+
+    void ScriptableDecalComponent::Activate()
+    {
+        AZ_Printf("ScriptableDecalComponent", "Activate");
+        m_decalFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntity<AZ::Render::DecalFeatureProcessorInterface>(GetEntityId());
+        if (m_decalFeatureProcessor)
+        {
+            AZ::RPI::Scene* scene = m_decalFeatureProcessor->GetParentScene();
+            DecalRequestBus::Handler::BusConnect(scene->GetId());
+            AZ::TickBus::Handler::BusConnect();
+        }
+    }
+
+    void ScriptableDecalComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+        DecalRequestBus::Handler::BusDisconnect();
+        m_decalFeatureProcessor = nullptr;
+    }
+
+    void ScriptableDecalComponent::SpawnDecal(const AZ::Transform& worldTm, AZ::Data::AssetId materialAssetId, const SpawnDecalConfig& config)
+    {
+        DecalHandle handle = m_decalFeatureProcessor->AcquireDecal();
+
+        AZ::Transform transform = worldTm * AZ::Transform::CreateUniformScale(config.m_scale);
+
+        m_decalFeatureProcessor->SetDecalTransform(handle, transform);
+        m_decalFeatureProcessor->SetDecalMaterial(handle, materialAssetId);
+        m_decalFeatureProcessor->SetDecalOpacity(handle, config.m_opacity);
+        m_decalFeatureProcessor->SetDecalAttenuationAngle(handle, config.m_attenutationAngle);
+        m_decalFeatureProcessor->SetDecalSortKey(handle, config.m_sortKey);
+
+        uint32_t currentTimeMs = GetCurrentTimeMs();
+
+        if (config.m_lifeTime > 0.0)
+        {
+            uint32_t lifetimeMs = static_cast<uint32_t>(config.m_lifeTime * 1000.0f);
+            uint32_t despawnTimeMs = currentTimeMs + lifetimeMs;
+
+            m_decalHeap.push_back({ config , handle, despawnTimeMs });
+            AZStd::push_heap(m_decalHeap.begin(), m_decalHeap.end(), HeapCompare);
+        }
+        else
+        {
+            m_animatingDecals.push_back({ config , handle, currentTimeMs });
+        }
+    }
+
+    void ScriptableDecalComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time)
+    {
+        uint32_t currentTimeMs = static_cast<uint32_t>(time.GetMilliseconds());
+
+        // Check to see if any decals need to despawn
+        while (!m_decalHeap.empty() && m_decalHeap.front().m_despawnMs < currentTimeMs)
+        {
+            AZStd::pop_heap(m_decalHeap.begin(), m_decalHeap.end(), HeapCompare);
+            m_animatingDecals.push_back(m_decalHeap.back());
+            m_decalHeap.pop_back();
+        }
+
+        // Animate despawning decals, remove those that are expired.
+        for (size_t i = 0; i < m_animatingDecals.size();)
+        {
+            DecalInstance& decalInstance = m_animatingDecals.at(i);
+
+            float currentFadeTimeMs = static_cast<float>(currentTimeMs - decalInstance.m_despawnMs);
+            float totalFadeTimeMs = decalInstance.m_config.m_fadeTime * 1000.0f;
+            if (currentFadeTimeMs > totalFadeTimeMs)
+            {
+                // Despawn the decal, it's done animating;
+                m_decalFeatureProcessor->ReleaseDecal(decalInstance.m_handle);
+
+                // Replace this instance with the one on the back
+                decalInstance = m_animatingDecals.back();
+                m_animatingDecals.pop_back();
+
+                // Don't increment, next iteration needs to process the item just moved to this spot.
+            }
+            else
+            {
+                float opacity = 1.0f - (currentFadeTimeMs / totalFadeTimeMs);
+                m_decalFeatureProcessor->SetDecalOpacity(decalInstance.m_handle, opacity);
+                ++i;
+            }
+        }
+    }
+
+    bool ScriptableDecalComponent::HeapCompare(const DecalInstance& value1, const DecalInstance& value2)
+    {
+        return value1.m_despawnMs < value2.m_despawnMs;
+    }
+
+    uint32_t ScriptableDecalComponent::GetCurrentTimeMs()
+    {
+        auto now = AZStd::chrono::steady_clock::now().time_since_epoch();
+        auto nowMs = AZStd::chrono::duration_cast<AZStd::chrono::milliseconds>(now).count();
+        return static_cast<uint32_t>(nowMs);
+    }
+}

+ 62 - 0
Gem/Code/Source/Components/ScriptableDecalComponent.h

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/EBus/Event.h>
+#include <DecalBus.h>
+
+#include <Atom/Feature/Decals/DecalFeatureProcessorInterface.h>
+
+namespace MultiplayerSample
+{
+
+    class ScriptableDecalComponent
+        : public AZ::Component
+        , public DecalRequestBus::Handler
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MultiplayerSample::ScriptableDecalComponent, "{79AEB56C-E886-4A6A-9BAA-0FE5D6D01F78}");
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
+        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        using DecalHandle = AZ::Render::DecalFeatureProcessorInterface::DecalHandle;
+
+        struct DecalInstance
+        {
+            SpawnDecalConfig m_config;
+            DecalHandle m_handle;
+            uint32_t m_despawnMs = 0;
+        };
+
+        // DecalRequestBus::Handler...
+        void SpawnDecal(const AZ::Transform& worldTm, AZ::Data::AssetId materialAssetId, const SpawnDecalConfig& config) override;
+
+        // TickBus::Handler...
+        virtual void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        static bool HeapCompare(const DecalInstance& value1, const DecalInstance& value2);
+        static uint32_t GetCurrentTimeMs();
+
+        AZ::Render::DecalFeatureProcessorInterface* m_decalFeatureProcessor = nullptr;
+
+        AZStd::vector<DecalInstance> m_decalHeap;
+        AZStd::vector<DecalInstance> m_animatingDecals;
+    };
+}

+ 3 - 1
Gem/Code/Source/MultiplayerSampleModule.cpp

@@ -15,6 +15,7 @@
 #include <Components/UI/UiCoinCountComponent.h>
 #include <Components/UI/UiGameOverComponent.h>
 #include <Components/UI/UiPlayerArmorComponent.h>
+#include <Components/ScriptableDecalComponent.h>
 #if AZ_TRAIT_CLIENT
     #include <Components/UI/HUDComponent.h>
     #include <Components/UI/UiMatchPlayerCoinCountsComponent.h>
@@ -51,7 +52,8 @@ namespace MultiplayerSample
                     UiPlayerArmorComponent::CreateDescriptor(),
                     UiMatchPlayerCoinCountsComponent::CreateDescriptor(),
                     UiRestBetweenRoundsComponent::CreateDescriptor(),
-                    UiStartMenuComponent::CreateDescriptor()
+                    UiStartMenuComponent::CreateDescriptor(),
+                    ScriptableDecalComponent::CreateDescriptor(),
                 #endif
             });
 

+ 4 - 0
Gem/Code/multiplayersample_client_files.cmake

@@ -6,6 +6,8 @@
 #
 
 set(FILES
+    Include/DecalBus.h
+
     Source/Components/UI/UiGameOverComponent.cpp
     Source/Components/UI/UiGameOverComponent.h
     Source/Components/UI/HUDComponent.cpp
@@ -18,4 +20,6 @@ set(FILES
     Source/Components/UI/UiRestBetweenRoundsComponent.h
     Source/Components/UI/UiStartMenuComponent.cpp
     Source/Components/UI/UiStartMenuComponent.h
+    Source/Components/ScriptableDecalComponent.cpp
+    Source/Components/ScriptableDecalComponent.h
 )