3

I'm writing an n-body simulation as an exercise in C# using WPF and I ran into what seems like a fundamental design issue with displaying the results of my program.

I have an Entity class, which stores basic information like position, velocity, mass. Then there's the PhysicsEngine class which has an ObservableCollection of Entities and does all the maths. The problem arises when I have to bind the position of the Entities to some graphical elements to show their movement on screen. The distances in space are very big, so I obviously need to process the position information somehow and transform it to screen coordinates. As a quick hack, I added this to the Entity class:

public Vector3 ScreenPosition
{
     get
     {
          return new Vector3(
               (Pos.X / Scale) - (Diameter / 2),
               (Pos.Y / Scale) - (Diameter / 2),
               (Pos.Z / Scale) - (Diameter / 2)
               // where Scale is an arbitrary number depending on how big the simulation is
          );
     }
}

which just returns the position with some math done to fit everything on the screen. That worked fine when the "camera" was static, but now I want to make it movable - maybe center the camera on a certain planet, or zoom in and out.

Continuing on using this hack seems ugly - I'd have to expose all kinds of details about the camera's position and zoom level to the low-level class of Entity, which really shouldn't care about the View or know anything about how it's being displayed.

I tried making a 2nd ObservableCollection of DisplayEntities which holds all the data needed to display each Entity, and on each simulation tick it'd loop through the List of Entities and then update their respective brethren in DisplayEntities, and in the code-behind of the View I programmatically add and bind geometrical shapes to each DisplayEntity, but that turned out to be really slow and impractical as well - I need to loop through all Entities, check if they already have a DisplayEntity, update if so, add a new DisplayEntity if not, not to mention what happens when an Entity is deleted.

I also tried wrapping the Entity class in another class which contains all the information needed to display it, which removes the issue with the two collections, but then it seems like the same abstraction problem as before shows up - the EntityVM must know the camera position, angle, zoom level, and I must loop over each and every one of them every tick and update their values - again slow and inflexible.

Coming from immediate graphics in WinForms, this situation seems really frustrating - in WinForms I could just make a function in code-behind that when called draws circles at such-and-such coordinates, doing whatever math I want to, since I don't need to think about binding. I just need to pass it a List of coordinates whenever I want to draw anything and it doesn't care about my actual objects at all, which ironically seems to separate the View better from the Model than the spaghetti I've cooked with WPF.

How do I approach the design of this in order to produce an elegant and non-clusterfuck solution?

(Thank you in advance and please let me know if my post is lacking in some aspect, it's my first time posting :) )


EDIT: For clarity, here's the important part of the code-behind of my view:

public void AddEntitiesToCanvas()
{
    PhysicsEngine engine = (PhysicsEngine)this.DataContext;
    for (int i = 0; i < engine.Entities.Count; i++)
    {
        Binding xBinding = new Binding("Entities[" + i + "].VPos.X");
        Binding yBinding = new Binding("Entities[" + i + "].VPos.Y");
        Binding DiameterBinding = new Binding("Entities[" + i + "].Diameter");

        Ellipse EntityShape = new Ellipse();
        EntityShape.Fill = new SolidColorBrush(Colors.Black);

        EntityShape.SetBinding(WidthProperty, DiameterBinding);
        EntityShape.SetBinding(HeightProperty, DiameterBinding);
        EntityShape.SetBinding(Canvas.LeftProperty, xBinding);
        EntityShape.SetBinding(Canvas.TopProperty, yBinding);
        EntityShape.SetValue(Canvas.ZIndexProperty, 100);

        canvas.Children.Add(EntityShape);
    }
}

The XML file contains just an empty Canvas.


EDIT 2: Here's the important part of my updated view

<DataTemplate>
    <Path Fill="Black">
        <Path.RenderTransform>
            <TranslateTransform X="{Binding VPos.X}" Y="{Binding VPos.Y}"/>
        </Path.RenderTransform>
        <Path.Data>
            <EllipseGeometry Center="0, 0" RadiusX="{Binding Diameter}" RadiusY="{Binding Diameter}"/>
        </Path.Data>
    </Path>
</DataTemplate>

EDIT 3: I tried using a Binding converter; however, the converter also needs access to the camera information from the PhysicsEngine class in order to perform the calculations. I thought of making the converter a property of the PhysicsEngine class so that it has access to all the private information and then doing this, which obviously doesn't work:

<Path.RenderTransform>
    <TranslateTransform X="{Binding Pos.X, Converter={Binding ScreenPosConverter}}" Y="{Binding Pos.Y, Converter={Binding ScreenPosConverter}}"/>
</Path.RenderTransform>

Is a Binding Converter the right tool for the job, and if yes, how can I pass the camera informaton to it?

Karl Rasch
  • 39
  • 4
  • There shouldn't be something like a "screen position" for each entity. Instead, the view should somehow be able to transform world coordinates to view coordinates. We can however not help you with that, because we don't know anything about the view part of your application. – Clemens Nov 18 '20 at 14:40
  • @Clemens That's exactly what I was thinking, I tried using a view matrix and transforming the position coordinates with it, but I can't see a way to apply that while using bindings. I updated my post with code from the view part. – Karl Rasch Nov 18 '20 at 14:50
  • 1
    Can't you just bind directly to your raw entity objects but use binding converters to do the view calcs? – GazTheDestroyer Nov 18 '20 at 15:27
  • 1
    As a note, you should seriously consider using an ItemsControl for displaying the entities. Use a Canvas as its ItemsPanel, and declare the Ellipse with its Bindings in the ItemTemplate. See e.g. this for reference: https://stackoverflow.com/a/22325266/1136211 – Clemens Nov 18 '20 at 15:30
  • 1
    The Canvas may then set an appropriate RenderTransform for the coordinate transformations. – Clemens Nov 18 '20 at 15:30
  • 1
    In order to get circles that are centered at an item position, you may also use a Path element with an EllipseGeometry instead of an Ellipse. – Clemens Nov 18 '20 at 15:35
  • 1
    @KarlRasch I think this is a good question but I can see why it may have received some close votes for seeming to lack focus (not from me). I edited the title which may help with that a bit and I encourage more editing to help readers understand the key point more. – StayOnTarget Nov 18 '20 at 15:37
  • 1
    You should also take care that the scaling factor between word and view units does not get too large or small to avoid possible rounding errors. If you are simulating a solar system, it would not be a good idea to transform from meters to pixels, but perhaps use something like a million kilometers as model unit. – Clemens Nov 18 '20 at 15:44
  • @Clemens Thank you, I followed your ItemsControl and Path suggestion and it already seems a lot cleaner. However, the problem with transforming the coordinates seemingly remains - I need: 1) camera information from the Engine class (let's say a scale factor and x,y offset and 2) a way to multiply real-world coordinates with the camera information. I don't see a way to access the camera information from a binding converter, and in XAML where I can access it I can't do math. How would one go around this? – Karl Rasch Nov 19 '20 at 08:15
  • Many thanks @UuDdLrLrSs, this title is much more concise and closer to what I had in mind :) – Karl Rasch Nov 19 '20 at 08:18

1 Answers1

0

I ultimately did it with a MultiBinding and an IMultiValueConverter:


<ItemsControl ItemsSource="{Binding Entities}">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas x:Name="canvas"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Fill="{Binding Color}">

                <Path.RenderTransform>
                    <TranslateTransform>

                        <TranslateTransform.X>
                            <MultiBinding  Converter="{StaticResource converter}">
                                <Binding Path="Pos.X"/>
                                <Binding Path="DataContext.Camera.X" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                            </MultiBinding>
                        </TranslateTransform.X>

                        <TranslateTransform.Y>
                            <MultiBinding Converter="{StaticResource converter}">
                                <Binding Path="Pos.Y"/>
                                <Binding Path="DataContext.Camera.Y" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                            </MultiBinding>
                        </TranslateTransform.Y>

                    </TranslateTransform>
                </Path.RenderTransform>

                <Path.Data>
                    <EllipseGeometry Center="0, 0" RadiusX="{Binding Diameter}" RadiusY="{Binding Diameter}"/>
                </Path.Data>

            </Path>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>

Karl Rasch
  • 39
  • 4