HateDread
Posts: 71
Joined: 06 Feb 2020, 10:27

Re: Unit health bars with NoesisGUI

14 Jul 2021, 17:29

1. Canvas positions the top-left corner of child elements by default, so if you want to center them you can create alternative attached properties that calculate the center position and set that instead:
public static class CenteredCanvas
{
    public static float GetX(FrameworkElement element) { return (float)element.GetValue(XProperty); }
    public static void SetX(FrameworkElement element, float value) { element.SetValue(XProperty, value); }
    public static readonly DependencyProperty XProperty = DependencyProperty.RegisterAttached(
        "X", typeof(float), typeof(CenteredCanvas), new PropertyMetadata(0.0f, OnPosChanged));

    public static float GetY(FrameworkElement element) { return (float)element.GetValue(YProperty); }
    public static void SetY(FrameworkElement element, float value) { element.SetValue(YProperty, value); }
    public static readonly DependencyProperty YProperty = DependencyProperty.RegisterAttached(
        "Y", typeof(float), typeof(CenteredCanvas), new PropertyMetadata(0.0f, OnPosChanged));

    private static void OnPosChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement element = d as FrameworkElement;
        if (element != null)
        {
            element.SizeChanged -= OnElementSizeChanged;
            element.SizeChanged += OnElementSizeChanged;
            UpdatePos(element);
        }
    }

    private static void OnElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdatePos((FrameworkElement)sender);
    }

    private static void UpdatePos(FrameworkElement element)
    {
        Canvas.SetLeft(element, GetX(element) - element.ActualWidth * 0.5);
        Canvas.SetTop(element, GetY(element) - element.ActualHeight * 0.5);
    }
}
<ItemsControl.ItemContainerStyle>
  <Style TargetType="{x:Type ContentPresenter}">
    <Setter Property="CenteredCanvas.X" Value="{Binding ScreenPosX}"/>
    <Setter Property="CenteredCanvas.Y" Value="{Binding ScreenPosY}"/>
  </Style>
</ItemsControl.ItemContainerStyle>
2. Is this Unity? We update our layout during MonoBehavior.LateUpdate() of the NoesisView component so you should update your viewmodels before that in order to reflect those changes in the same frame.
Awesome, thanks. I'm trying that first suggestion - always learning more about the options! Two questions:

1. What is the purpose of unbinding and then rebinding the OnElementSizeChanged call?
2. How would one translate the OnPosChanged function to C++? Here is my attempt, but struggling to get the event binding right for a static member function. I suppose it could be a free function and is static just due to C# -ness? EDIT: Same problem arises with a free function. It seems to be deducing to the FromFunctor version, but it's a free/static function.
class CenteredCanvas : public Noesis::BaseComponent
{
public:	
	static float GetX(Noesis::FrameworkElement* element) {
		return element->GetValue<float>(XProperty);
	}
	static void SetX(Noesis::FrameworkElement* element, float value) {
		element->SetValue<float>(XProperty, value);
	}
	inline static const Noesis::DependencyProperty* XProperty;

	static float GetY(Noesis::FrameworkElement* element) {
		return element->GetValue<float>(YProperty);
	}
	static void SetY(Noesis::FrameworkElement* element, float value) {
		element->SetValue<float>(YProperty, value);
	}
	inline static const Noesis::DependencyProperty* YProperty;

	using OnElementSizeChangedDelegate = Noesis::Delegate<void(Noesis::BaseComponent* sender, Noesis::SizeChangedEventArgs e)>;
	static void OnPosChanged(Noesis::BaseComponent* obj, Noesis::DependencyPropertyChangedEventArgs e)
	{
		if (Noesis::FrameworkElement* element = Noesis::DynamicCast<Noesis::FrameworkElement*>(obj))
		{
			element->SizeChanged() -= OnElementSizeChangedDelegate(&CenteredCanvas::OnElementSizeChanged);
			element->SizeChanged() += OnElementSizeChangedDelegate(&CenteredCanvas::OnElementSizeChanged);
			UpdatePos(element);
		}
	}

	static void OnElementSizeChanged(Noesis::BaseComponent* sender, Noesis::SizeChangedEventArgs e)
	{
		UpdatePos(Noesis::DynamicCast<Noesis::FrameworkElement*>(sender));
	}

	static void UpdatePos(Noesis::FrameworkElement* element)
	{
		Noesis::Canvas::SetLeft(element, GetX(element) - element->GetActualWidth() * 0.5f);
		Noesis::Canvas::SetTop(element, GetY(element) - element->GetActualHeight() * 0.5f);
	}

	NS_IMPLEMENT_INLINE_REFLECTION(CenteredCanvas, BaseComponent, "rtg.Controls.CenteredCanvas")
	{
		Noesis::DependencyData* data = NsMeta<Noesis::DependencyData>(Noesis::TypeOf<ShipListEntryView>());

		data->RegisterProperty<float>(XProperty, "X",
			Noesis::PropertyMetadata::Create(0.0f, &CenteredCanvas::OnPosChanged));
		data->RegisterProperty<float>(YProperty, "Y",
			Noesis::PropertyMetadata::Create(0.0f, &CenteredCanvas::OnPosChanged));
	}
};
(EDIT: Resolved the code issues and have this working, but am still unsure of the resize remove/add flow above. The rest of the post still applies)
void OnElementSizeChanged(Noesis::BaseComponent* sender, const Noesis::SizeChangedEventArgs& e);

class CenteredCanvas : public Noesis::BaseComponent
{
public:	
	static float GetX(Noesis::FrameworkElement* element) {
		return element->GetValue<float>(XProperty);
	}
	static void SetX(Noesis::FrameworkElement* element, float value) {
		element->SetValue<float>(XProperty, value);
	}
	inline static const Noesis::DependencyProperty* XProperty;

	static float GetY(Noesis::FrameworkElement* element) {
		return element->GetValue<float>(YProperty);
	}
	static void SetY(Noesis::FrameworkElement* element, float value) {
		element->SetValue<float>(YProperty, value);
	}
	inline static const Noesis::DependencyProperty* YProperty;

	static void OnPosChanged(Noesis::DependencyObject* d, const Noesis::DependencyPropertyChangedEventArgs& e)
	{
		if (Noesis::FrameworkElement* element = Noesis::DynamicCast<Noesis::FrameworkElement*>(d))
		{
			auto sizeFunc = [](Noesis::BaseComponent* component, const Noesis::SizeChangedEventArgs& e) {
				OnElementSizeChanged(component, e);
			};

			element->SizeChanged() -= sizeFunc;
			element->SizeChanged() += sizeFunc;

			UpdatePos(element);
		}
	}

	static void UpdatePos(Noesis::FrameworkElement* element)
	{
		Noesis::Canvas::SetLeft(element, GetX(element) - element->GetActualWidth() * 0.5f);
		Noesis::Canvas::SetTop(element, GetY(element) - element->GetActualHeight() * 0.5f);
	}

	NS_IMPLEMENT_INLINE_REFLECTION(CenteredCanvas, BaseComponent, "rtg.Controls.CenteredCanvas")
	{
		Noesis::DependencyData* data = NsMeta<Noesis::DependencyData>(Noesis::TypeOf<ShipListEntryView>());

		data->RegisterProperty<float>(XProperty, "X",
			Noesis::PropertyMetadata::Create(0.0f, &CenteredCanvas::OnPosChanged));
		data->RegisterProperty<float>(YProperty, "Y",
			Noesis::PropertyMetadata::Create(0.0f, &CenteredCanvas::OnPosChanged));
	}
};

void OnElementSizeChanged(Noesis::BaseComponent* sender, const Noesis::SizeChangedEventArgs& e)
{
	CenteredCanvas::UpdatePos(Noesis::DynamicCast<Noesis::FrameworkElement*>(sender));
}
----------------------------------------------

And as the above may suggest; no, not Unity here - mostly custom C++. Your reply re. Unity and frame business made me re-evaluate how I ordered some things in the engine and I managed to fix the latency.

And lastly; anything you'd suggest looking into to have the UI elements scale based on distance of the unit they represent to the camera? Always having the same space on screen will, as you zoom out, make the UI elements look larger and larger relative to the world. But I'm not looking to put them in-world, at least not in terms of e.g. rotation, so no need for a 3D widget. I was thinking of stashing some rough 'distance to camera' type of value in its ViewModel which is used in the bound source for the ItemsControl, and maybe just calculating some kind of size/scaling relationship to distance? EDIT: didn't think about this until I saw it, but... overlapping icons. I guess intuition would suggest the closer units' icons should be on top of the further units' icons (although the overlap should be avoided through design etc). I wonder if that's an argument for some kind of in-world/3D UI solution instead.
 
User avatar
sfernandez
Site Admin
Posts: 2984
Joined: 22 Dec 2011, 19:20

Re: Unit health bars with NoesisGUI

15 Jul 2021, 19:14

1. I was unbinding first to make sure I only register once to the element's SizeChanged event.
2. In C++ a property changed function should be like this:
static void OnPosChanged(Noesis::BaseComponent* obj, const Noesis::DependencyPropertyChangedEventArgs& e)
{
  if (Noesis::FrameworkElement* element = Noesis::DynamicCast<Noesis::FrameworkElement*>(obj))
  {
    element->SizeChanged() -= OnElementSizeChanged;
    element->SizeChanged() += OnElementSizeChanged;
    UpdatePos(element);
  }
}
 
HateDread
Posts: 71
Joined: 06 Feb 2020, 10:27

Re: Unit health bars with NoesisGUI

20 Jan 2024, 08:52

1. I was unbinding first to make sure I only register once to the element's SizeChanged event.
2. In C++ a property changed function should be like this:
static void OnPosChanged(Noesis::BaseComponent* obj, const Noesis::DependencyPropertyChangedEventArgs& e)
{
  if (Noesis::FrameworkElement* element = Noesis::DynamicCast<Noesis::FrameworkElement*>(obj))
  {
    element->SizeChanged() -= OnElementSizeChanged;
    element->SizeChanged() += OnElementSizeChanged;
    UpdatePos(element);
  }
}
Coming back to this years later - I ended up using it regularly, ignoring the warnings it produced, but I'm trying to apply some downward pressure on my Noesis logs.

The '-=' operator triggers 'Event 'SizeChanged' has no handlers registered' the first time through, but there's no way to check if there are any existing usages of the UIElement::RoutedEvent_. Any advice for using the above function without causing the warnings? Can be quite spammy when spawning a lot of icons.
 
User avatar
sfernandez
Site Admin
Posts: 2984
Joined: 22 Dec 2011, 19:20

Re: Unit health bars with NoesisGUI

23 Jan 2024, 13:45

I verified that WPF does not raise any error or warning when removing a handler from an event that was not previously registered, so this is wrong in Noesis. Could you please report it in our bugtracker, we shouldn't show any error message in that case either.

In the meantime one simple workaround would be to filter out those error messages in the LogHandler if you have your own set.
Or define another attached property in the CenteredCanvas, a bool that indicates if SizeChanged event has been registered:
public static bool GetIsRegistered(FrameworkElement element) { return (bool)element.GetValue(IsRegisteredProperty); }
public static void SetIsRegistered(FrameworkElement element, bool value) { element.SetValue(IsRegisteredProperty, value); }
public static readonly DependencyProperty IsRegisteredProperty = DependencyProperty.RegisterAttached(
        "IsRegistered", typeof(bool), typeof(CenteredCanvas), new PropertyMetadata(false));

private static void OnPosChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  FrameworkElement element = d as FrameworkElement;
  if (element != null)
  {
    if (!GetIsRegistered(element))
    {
      element.SizeChanged += OnElementSizeChanged;
      SetIsRegistered(element, true);
    }
    UpdatePos(element);
  }
}
 
HateDread
Posts: 71
Joined: 06 Feb 2020, 10:27

Re: Unit health bars with NoesisGUI

23 Jan 2024, 14:43

 
User avatar
jsantos
Site Admin
Posts: 3906
Joined: 20 Jan 2012, 17:18
Contact:

Re: Unit health bars with NoesisGUI

24 Jan 2024, 12:28

Fixed, thanks!

Who is online

Users browsing this forum: Semrush [Bot] and 29 guests