HateDread
Posts: 17
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: 2255
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);
  }
}

Who is online

Users browsing this forum: Bing [Bot] and 2 guests