steveh
Topic Author
Posts: 25
Joined: 07 Oct 2019, 12:50

Generic notify system from Storyboards

05 Aug 2020, 19:29

Hi guys,

We have a "notify" system which allows us to trigger notifies from Storyboards. The notify is basically a custom class which derives from FrameworkElement and has a boolean dependency property called "Trigger". It is false by default, and we trigger it to true during a storyboard. It's very similar to this idea: viewtopic.php?t=1844
class CustomTrigger : public Noesis::FrameworkElement {
private:
	static void OnCustomTriggerPropertyValueChanged(Noesis::DependencyObject *pDependcyObject, const Noesis::DependencyPropertyChangedEventArgs &args);
	void InternalTrigger();

	f32 m_fLastTriggerRequest;

public:
	///////////////////////////////////////////////////
	// Dependency Property Interface
	static const Noesis::DependencyProperty *TriggerProperty;
	bool GetTrigger() const;
	void SetTrigger(bool bTrigger);

	NS_DECLARE_REFLECTION(CustomTrigger, Noesis::FrameworkElement);
};
Because these triggers may be called multiple times, I have to reset the value to false in order to get the animation to change the value again, e.g. if we play the storyboard 2 times in a row I need to reset it to false after the first trigger to get the 2nd trigger to retrigger it again.

This is where the m_fLastTriggerRequest comes in. It's a bit of a hack, but basically each time I get the dependency changed event callback through, I set the value to false, clear the animation, and only actually call InternalTrigger() if there's been substantial time between the last time it was set:
/*static */void CustomTrigger::OnCustomTriggerPropertyValueChanged(DependencyObject *pDependcyObject, const DependencyPropertyChangedEventArgs &args)
{
	if (args.prop == TriggerProperty)
	{
		SelfClass* pThis = DynamicCast<SelfClass*>(pDependcyObject);
		if (pThis != nullptr)
		{
			if ((*(bool*)args.newValue) == true)
			{
				f32 fCurrentRequest = GetCurrentTime();
				// Test for some certain elapsed time
				if ((fCurrentRequest - pThis->m_fLastTriggerRequest) > 0.1f)
				{
					pThis->InternalTrigger();
				}
				pThis->m_fLastTriggerRequest = fCurrentRequest;
				pThis->ClearAnimation<bool>(TriggerProperty);
				pThis->SetTrigger(false);
			}
		}
	}
}
Now... This is quite a bit of a hack, but it works. I have a single keyframe in a storyboard which sets Trigger to true at the time we want to trigger the animation. We get the dependency property changed event through, because we haven't tried to trigger it yet the elapsed time is greater than 0.1 seconds so we trigger it. We then set the value back to false and clear the animation.

Then, each time we update the animation clocks the animation tries to change the value back to true again. However, each time this happens, we get the callback but we don't call InternalTrigger because the time elapsed is shorter than 0.1 seconds.

This goes on until the storyboard ends. We can then retrigger the storyboard at a later stage, and it'll retrigger the notify again at the desired time because the time elapsed will be > 0.1 seconds.

This works until we create a looping animation. The storyboard never ends and TriggerProperty always wants to be set to true by the animation.

Is there any way around this? Are there any alternative ways I can solve this? My only solution I can think of at the moment is to add another keyframe at the end of the storyboard to set Trigger dependency property back to false, so the animation has enough time to stop calling my DependencyChangedEvent delegate so the elapsed time can get > 0.1 seconds. However, this means that I have to tell all our designers / artists about this caveat, and it's not very intuitive. Is there a better way around all this? I've searched online and I can't see anyone trying to do anything similar to this using WPF / XAML, but this is common in games, we often want to trigger VFX / SFX from the UI, and it's nice to be able to just trigger things at a certain point in the animation.

Cheers,

-Steven
 
User avatar
sfernandez
Site Admin
Posts: 1911
Joined: 22 Dec 2011, 19:20

Re: Generic notify system from Storyboards

10 Aug 2020, 14:25

Hi Steve,

I've been doing some tests in WPF and the only way to correctly flip the trigger value is doing it using keyframes in the storyboard.
      <BooleanAnimationUsingKeyFrames Storyboard.TargetName="Rect1" Storyboard.TargetProperty="(local:CustomTrigger.Trigger)">
        <DiscreteBooleanKeyFrame KeyTime="0:0:0.5" Value="True"/>
        <DiscreteBooleanKeyFrame KeyTime="0:0:0.51" Value="False"/>
      </BooleanAnimationUsingKeyFrames>
If I try in WPF to clear the value in code I need to remove the animation on the property,
element.BeginAnimation(CustomTrigger.TriggerProperty, null);
so it would be impossible to trigger it more than once in the same storyboard or trigger it in a looping storyboard.

Let me think about it to see if there is a better way to achieve what you want.
 
User avatar
sfernandez
Site Admin
Posts: 1911
Joined: 22 Dec 2011, 19:20

Re: Generic notify system from Storyboards

11 Aug 2020, 20:37

After some thinking it looks to me that the best approach to do this is by using a Trigger property of type float, so it can get many different values and trigger every time it changes. The property could use the keyframe time as an easy way to assign the value:
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rect1" Storyboard.TargetProperty="(local:CustomTrigger.Trigger)">
  <DiscreteDoubleKeyFrame KeyTime="0:0:0.5" Value="0.5"/>
  <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="1"/>
  <DiscreteDoubleKeyFrame KeyTime="0:0:1.5" Value="1.5"/>
</DoubleAnimationUsingKeyFrames>
Instead of using a bool property and try to force the value back to false in code, and add all that elapsed time logic, you can have a simple callback that always calls the trigger function whenever the value changes:
struct CustomTrigger
{
public:
    static const Noesis::DependencyProperty* TriggerProperty;

private:
    static void OnTriggerChanged(DependencyObject*, const DependencyPropertyChangedEventArgs& e)
    {
        NS_LOG_INFO("Trigger!!");
    }

    NS_IMPLEMENT_INLINE_REFLECTION(CustomTrigger, NoParent, "Testing.CustomTrigger")
    {
        DependencyData* data = NsMeta<DependencyData>(TypeOf<CustomTrigger>());
        data->RegisterProperty<float>(TriggerProperty, "Trigger",
            PropertyMetadata::Create(-1.0f, PropertyChangedCallback(OnTriggerChanged)));
    }
    ...
};
You can also use negative values as a way to reset and avoid calling the trigger function, for example if you have a looping animation with a single trigger keyframe:
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rect1"
                               Storyboard.TargetProperty="(local:CustomTrigger.Trigger)"
                               RepeatBehavior="Forever">
  <DiscreteDoubleKeyFrame KeyTime="0" Value="-1"/>
  <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="1"/>
</DoubleAnimationUsingKeyFrames>
    static void OnTriggerChanged(DependencyObject*, const DependencyPropertyChangedEventArgs& e)
    {
        float f = *static_cast<const float*>(e.newValue);
        if (f >= 0.0f)
        {
            NS_LOG_INFO("Trigger!!");
        }
    }
Do you think this could work for you?
 
steveh
Topic Author
Posts: 25
Joined: 07 Oct 2019, 12:50

Re: Generic notify system from Storyboards

27 Aug 2020, 15:24

Sorry for the delay Sergio, I completely forgot I posted this. Thank you for the reply.

I like your idea with changing the property to a float, but I don't think it solves everything. Designers are setting these up and I don't want them to have to think about the back end tech, so I want them to be able to just trigger these nodes at a given point in a storyboard.

If we have a looping animation where we want to trigger a notify at 0.5 seconds into an animation, I basically want it so the designers just specify a single keyframe. In the cast of the notify it'll be as follows:
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rect1"
                               Storyboard.TargetProperty="(local:CustomTrigger.Trigger)"
                               RepeatBehavior="Forever">
  <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="1"/>
</DoubleAnimationUsingKeyFrames>
I don't want to have the designers insert a dummy keyframe at the beginning just to reset the animated value.

As far as I can tell, there's no easy way to achieve this with the WPF storyboard system. I can't think of a nicer way to expose this to designers so they can just say "I want a trigger at this point in the animation" and they don't have to insert extra keyframes to alter the dependency property value at the start / end of the animation.

Basically I'm trying to get it as simple as adding a notify in Unreal:

If this is not possible, I could always write a tool which process the *.xaml file and scans for any keyframe animation nodes which are affecting the depdency property (local:CustomTrigger.Trigger), and edit the *.XAML to insert / ensure that there is a keyframe at KeyTime 0, however, I'd like to avoid this as it requires that I create a tool to edit the XAML which just feels like an extra step :)

Cheers,

-Steven
 
User avatar
sfernandez
Site Admin
Posts: 1911
Joined: 22 Dec 2011, 19:20

Re: Generic notify system from Storyboards

28 Aug 2020, 10:42

I investigated a bit more and did some tests and maybe I'm close enough to what you need.

Following my idea of using a float property as trigger, and considering negative value as invalid and positive value as trigger values, you can have the default value of the property to be -1 for example, and use FillBehavior="Stop" on the timeline so it resets to initial value when animation ends.
<Storyboard x:Key="anim" Duration="0:0:1" RepeatBehavior="3x">
  <DoubleAnimationUsingKeyFrames FillBehavior="Stop" Storyboard.TargetName="rect" Storyboard.TargetProperty="(local:CustomTrigger.Trigger)">
    <DiscreteDoubleKeyFrame KeyTime="0:0:0.5" Value="0.5"/>
  </DoubleAnimationUsingKeyFrames>
</Storyboard>
Previous animation will trigger 3 times because each loop starts with the initial value (the default -1) so on time 0.5 it always changes. And if you fire it again it will trigger too because when previous animation ended it reset the trigger value to -1.
 
steveh
Topic Author
Posts: 25
Joined: 07 Oct 2019, 12:50

Re: Generic notify system from Storyboards

28 Aug 2020, 11:35

Interesting, cheers Sergio. I'll take a look at using the fill behaviour value, this seems like it could work.

Much appreciated!

-Steven
 
User avatar
jsantos
Site Admin
Posts: 2899
Joined: 20 Jan 2012, 17:18
Contact:

Re: Generic notify system from Storyboards

02 Sep 2020, 13:19

Even if it works I think it is better if we already provide a clean solution in the core library. Please, let us know about it.

Who is online

Users browsing this forum: No registered users and 2 guests