Issue adding multiple Polygon objects to canvas - c#

I currently programming the classic arcade game Asteroids in C# WPF to get some practice.
I have run into an issue that I cannot seem to solve.
I am running into issues while generating asteroids and adding to the canvas element which contains all of my game objects.
I have a generateAsteroids method that gets called every 20 milliseconds along over methods that update the player ships position and so on. generateAsteroids method performs various calculations (comments in function) to determine how many asteroids to add to the asteroidCollection list. This all works fine.
The issue arises when I try to add the asteroids Polygon object to the games canvas.
I get the following error: "ArugementException was unhandled by user code: Specified Visual is already a child of another Visual or the root of a CompositionTarget"
Now I understand what this means (I think), all of the Asteroid objects are called "asteroid" which obviously is not ideal, Ive researched and found that you cannot dynamically create variable names for objects on the fly.
I have tried giving the Polygon a dynamic name each time one gets added to the canvas.
Can anyone in the know of this issue please help me out?
Ive added all the code that I think is relevant, please let me know if you need to see more.
Thanks
C#:
public void drawAsteroid(Asteroid theAsteroid)
{
// entityShape is a Polygon object
theAsteroid.entityShape.Name = "asteroid" + this.asteroidsAdded.ToString();
theAsteroid.entityShape.Stroke = Brushes.White;
theAsteroid.entityShape.StrokeThickness = 2;
theAsteroid.entityShape.Points = theAsteroid.getEntityDimensions();
gameCanvas.Children.Add(theAsteroid.entityShape);
}
// Called every 20 milliseconds by method that updates the game canvas. Possibly quite inefficient
public void generateAsteroids()
{
// Number of asteroids to add to the collection = the length of the game so far / 3, then subtract the amount of asteroids that have already been added
int asteroidNum = Convert.ToInt32(Math.Ceiling((DateTime.Now - gameStartTime).TotalSeconds / 3));
asteroidNum -= asteroidsAdded;
for (int i = 0; i <= asteroidNum; i ++)
{
asteroidCollection.Add(new Asteroid());
this.asteroidsAdded += 1;
}
foreach (Asteroid asteroid in asteroidCollection)
{
drawAsteroid(asteroid);
}
}
XAML:
<Window x:Name="GameWindow" x:Class="AsteroidsAttempt2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="1000" Height="1000" HorizontalAlignment="Left" VerticalAlignment="Top" Loaded="GameWindow_Loaded">
<Canvas x:Name="GameCanvas" Focusable="True" IsEnabled="True" HorizontalAlignment="Left" Height="1000" VerticalAlignment="Top" Width="1000" KeyDown="GameCanvas_KeyDown" KeyUp="GameCanvas_KeyUp">
<Canvas.Background>
<ImageBrush ImageSource="D:\CPIT\BCPR283\Asteroids\Asteroids\AsteroidsAttempt2\Resources\SpaceBackground.jpg" Stretch="Fill"/>
</Canvas.Background>
</Canvas>

In each call of the drawAsteroid method you're adding all Polygons from the asteroidCollection to the Canvas, regardless if they have already been added or not. But you can't add the same object twice to the Children collection of a WPF Panel. That's why you get the exception (it has nothing to do with the Name).
Change your code like this:
if (!gameCanvas.Children.Contains(theAsteroid.entityShape))
{
gameCanvas.Children.Add(theAsteroid.entityShape);
}
Of course the code still lacks logic to remove Polygons from the Canvas that are no longer contained in the asteroidCollection. You will also have to add that.
And you don't need to set the Name of the Polygon objects at all, unless you want to access them by their name later.

Related

WinUI3: How to change the size of a rectangle at runtime

The Goal
I want to build an audio visualizer based on 1/3 octaves like this one in an WinUI3 app:
I already have the sampling part, I only need to deal with the rendering and visual part.
What Have I tried?
While looking at the WinUI3 Gallery App, I've found that you can modify rectangles in a nice way using either a translation or a scale modifier(and it's also animated):
<!-- Automatically animate changes to Scale -->
<Rectangle x:Name="rectangle" Width="50" Height="50" Scale="1,1,1" >
<Rectangle.ScaleTransition>
<Vector3Transition />
</Rectangle.ScaleTransition>
<Rectangle.TranslationTransition>
<Vector3Transition />
</Rectangle.TranslationTransition>
</Rectangle>
using System.Numerics;
private void button_Click(object sender, RoutedEventArgs e)
{
rectangle.Scale = new Vector3(1, 1, 1);
rectangle.Translation = new Vector3(1, 1, 1);
}
Firstly I tried to have 11 very long rectangles and move them upwards using the value. The problem with this approach is that the rectangles cannot be bigger than the container. You can have a default scale that actually overrides this rule but it lead to blurry corners.
So I've decided to try to scale the Y value of the rectangles by the respective value for each octave.
What I have
Right now I have 11 rectangles in a StackPanel named "rectangles", each rectangle looking like this
<Rectangle
Width="50"
Height="40"
Margin="7,10,7,0"
Fill="{ThemeResource SystemAccentColor}"
RadiusX="7"
RadiusY="3"
Scale="1,0,1">
<Rectangle.ScaleTransition>
<Vector3Transition />
</Rectangle.ScaleTransition>
<Rectangle.TranslationTransition>
<Vector3Transition />
</Rectangle.TranslationTransition>
</Rectangle>
And a timer that runs this code every 10ms:
private void FetchOctaves(object state)
{
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
if (!Audio.IsPlaying()) return;
var octaves = Audio.GetFrequencyData().ToList();
for (var i = 0; i < octaves.Count; i++)
{
// Map() normalizes a value from a source min and max to a target min and max
var yScale = Map(octaves[i], -60f, 20f, 1f, 12f);
rectangles.Children[i].Scale = new Vector3(1, yScale, 1);
}
}
);
}
And the result looks like this:
The Problem(s)
As you can figure, my solution is quite hardcoded and it leads to some problems:
1. I cannot find a maximum scale value in order to appeal to all window sizes.
Bigger scale values make my rectangles go out of the container's bounds.
One solution to this would be to have a property MaxScale that adapts whenever the size of the container changes(I already modify the rectangle's width whenever this happens). But this doesn't solve the next issue:
2. When met with big Y-scale values my rectangle's rounded corners start to look blurry.
This probably happens because of how the app handles scale(by zooming in).
What I want
I want to know if there's a way to resize the actual height of the rectangles at runtime.
This would solve both of my problems and would allow me to make it responsive to different window sizes.
If that is not possible, I would appreciate if you would point me towards another solution to achieve this goal using WinUI3.
Thanks in advance!

WPF Touch - Choosing Manipulation Container based on number of manipulators

I have a WPF Touch app (MS Surface, VS 2017, .NET 4.7) that shows a number of simple shapes (lines, rects, circles) above an image, all on on a canvas. I'm having a trouble with selecting my ManipulationContainer for touch.
Roughly what I have is this:
<ScrollViewer x:Name="MyViewer"
ManipulationStarting="Viewer_ManipulationStarting">
<Canvas x:Name="MyCanvas">
<Image x:Name="MyImage" Source={Binding MyImageSource}"/>
<ItemsControl x:Name="MyShapes" ItemsSource="{Binding MyShapes}"/>
</Canvas>
</ScrollViewer>
I want let my user do two different things with touch.
With one finger they can draw new shapes (relative to the canvas)
With two fingers they can zoom the entire canvas (relative to the scrollviewer)
So if the user is drawing a new shape, then ManipulationContainer must be "MyCanvas". But if the user is zooming the whole scene, then the ManipulationContainer must be "MyViewer" (because in that case I'm changing the LayoutTransform of the whole canvas).
So I need (it would seem) to select from one of two different manipulation containers depending on which of these operations is happening. But I can't seem to figure out how. Below is what I have (which doesn't work) where I choose the container. in the ManipulationStarting handler.
private void Scene_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
// If zooming, container is the parent viewer. Otherwise it's
// drawing a shape relative to the canvas.
if (e.Manipulators.Count() >= 2)
e.ManipulationContainer = MyViewer; // Zooming whole canvas
else
e.ManipulationContainer = MyCanvas; // Drawing on canvas
e.Handled = true;
}
As you likely guessed, when I first get this, I always get just one manipulator; The user is extremely unlikely to have his very first touch be with two fingers exactly simultaneously. So my code always thinks I'm drawing a shape.
It's only later on, in the ManipulationDelta that I start getting more than one manipulator. But it's too late then to choose a container. It's already been chosen. And if I check the number of manipulators then, my coordinates, delta, and origin are all relative to the very thing I'm trying to move. It makes the zooming jump all over the place.
I don't know what to do about this. I have to let my user zoom at any time so I do not have the option of forcing them to choose a "zoom tool".
I'm sure I'm missing something simple but I don't know what it is and I'd like to avoid blind alleys. Any suggestions?

How do I reference a rectangle created programmatically, with XAML?

I am working with WPF for the first time. I am creating a rectangle object & adding it as a child of a canvas.
How do I reference it in XAML?
I want to be able to rotate it over time but don't know how to access it from the MainWindow.xaml code...
I haven't been able to find an answer to this anywhere (maybe you can't do it this way?)
Edit:
I tried setting the Name property of the rectangle to Test (in C# code) and then doing
<Rectangle x:Name="Test">
<Rectangle.LayoutTransform>
<RotateTransform Angle="-45"/>
</Rectangle.LayoutTransform>
</Rectangle>
(This didn't work)
If you create a control in C#, you cannot access it in XAML. I think you must create the necessary animation in C# too.
Applying your rotation in C# could look like this:
var rect = new Rectangle();
rect.LayoutTransform = new RotateTransform() { Angle = -45 };
parentPanel.Children.Add(rect);
The better way would be to generate the Rectangle in XAML and apply there the animation. But this depends on your exact situation. e.g. you can create a single Rectangle in XAML and use this one or you can bind an ItemsControl and create a Rectangle in the ItemTemplate for each entry in the binded list.

Unexpected behaviour in a game example from a book

I just got the book Head First C# 3rd Edition. In the first part of the book it gets you started creating a simple game where you make bouncing circles that you must avoid while clicking and dragging a "human" to safety. The coding involves C# as well as XAML for the graphics side. Everything works except for one thing. Instead of clicking and dragging the "human" if I click on it it will "follow" the cursor around and the game behaves as if the cursor itself is the human since that is where game over happens when I hit one of the bouncing circles. I cannot figure out what I am doing wrong. I have looked over the code line by line next to the book.....The only thing I can figure out is that this block of code.
private void playArea_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (humanCaptured)
{
Point pointerPosition = e.GetCurrentPoint(null).Position;
Point relativePosition = grid.TransformToVisual(playArea).TransformPoint(pointerPosition);
if ((Math.Abs(relativePosition.X - Canvas.GetLeft(human)) > human.ActualWidth * 20)
|| (Math.Abs(relativePosition.Y - Canvas.GetTop(human)) > human.ActualHeight * 20))
{
humanCaptured = false;
human.IsHitTestVisible = true;
}
else
{
Canvas.SetLeft(human, relativePosition.X - human.ActualWidth / 2);
Canvas.SetTop(human, relativePosition.Y - human.ActualHeight / 2);
}
}
}
The book says to put the human actual width and height to 3. But if I do that and try to click on the person in the actual game it won't even register the cursor at all. but if I put that at 20 or above and I click on the human as I said earlier it starts following my cursor around and the rest of the game behaves normally.
Here is the code that is supposed to make the cursor be able to "click" and "drag" the human.
private void human_PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (enemyTimer.IsEnabled)
{
humanCaptured = true;
human.IsHitTestVisible = false;
}
}
And here is the related XAML code this is supposed to be working with to make it click and drag but instead is making it follow my cursor around the area:
<StackPanel x:Name="human" Orientation="Vertical" Canvas.Left="17" Canvas.Top="15"
PointerPressed="human_PointerPressed" >
<Ellipse Fill="White" Height="10" Stroke="White" Width="10"/>
<Rectangle Fill="White" Height="25" Stroke="White" Width="10" />
</StackPanel>
Everything else seems to be working right. I have checked the code in the book over and over and gone to their site. Nothing I did is different as far as I can tell. If anyone can help it will be greatly appreciated.
After reading the Book "Head First C#, 3rd Edition"
I would say the problem isn't your code...
It's more about how he detects if a Human get captured, because he does not check if "human" collide with a "enemy" he just did some Pointerevent's which will get triggert if a Pointer (your Mouse) will enter an "enemy" and not if your "human" collide with him...

Dynamically changing properties of a Storyboard (created in XAML) in the C# code-behind

I admit I'm new to Silverlight, but you have to start somewhere.
Here's my problem: I have XAML code which creates a Canvas to be used on my web page. I dynamiclly (in the code behind) create 24 smaller canvas objects (called tiles) and can correctly position and move these tiles inside the larger canvas. I want to now animate the movement of the tiles rather than have them just "jump" from one location to the next. Inside the XAML I created a Storyboard and a DoubleAnimation for one of the tiles. Clicking on the specific tile named in the DoubleAnimation/Storyboard produces the correct animation. Now I want to be able to change the properties of the animation in the XAML dynamically by the code in the code-behind. Specifically, I want to change the "From", "To", "Storyboard.TargetName", and "Storyboard.TargetProperty" values. This would allow me to use the single animation to control the movement of all 24 tiles (one at a time). Below is the XAML and below that is the code I've been attempting to get to work correctly.
XAML
<Canvas x:Name="LayoutRoot" Background="BlanchedAlmond" Height="700" Width="1405">
<Image Source="bkcolor.png" Canvas.Left="600" Height="500" Width="500" Stretch="UniformToFill" Canvas.Top="100"></Image>
<Canvas x:Name="myContainer" Canvas.Left = "50" Canvas.Top="100">
<!---->
<Canvas.Resources>
<Storyboard x:Name="MoveTileAnimation">
<DoubleAnimation x:Name="myDoubleAnimation"
From="400" To="300"
Duration="00:00:1"
Storyboard.TargetName="Tile23"
Storyboard.TargetProperty="(Canvas.Top)">
<DoubleAnimation.EasingFunction>
<PowerEase Power="3" EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Canvas.Resources>
<!---->
</Canvas>
</Canvas>
C# Code in Code Behind
private void MainPage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Canvas c = sender as Canvas;
// New location is specified by nx, ny
int nx = 200;
int ny = 300;
// Old Location
int ox = 200;
int oy = 200;
// "Tile Moves Up" -- Other case removed for clarity
//----------------------------------------------
// Code below is known to work correctly
//----------------------------------------------
// Set the "To" and "From" properties
myDoubleAnimation.From = Convert.ToDouble(oy);
myDoubleAnimation.To = Convert.ToDouble(ny);
//----------------------------------------------
// Code below does not function correctly
//----------------------------------------------
MoveTileAnimation.SetValue(Storyboard.TargetNameProperty, c.Name); // c.Name is the
// name of the tile
// that was clicked
MoveTileAnimation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath(Canvas.TopProperty)); // Need to switch between Top and Left
//----------------------------------------------
MoveTileAnimation.Begin(); // This works if the TargetNameProperty in
// the XAML matches the Tile Name
//----------------------------------------------
// Code below is known to work correctly
//----------------------------------------------
// Move the Tile to its new position
tileCanvas[nCanvasID].SetValue(Canvas.TopProperty, Convert.ToDouble(ny));
tileCanvas[nCanvasID].SetValue(Canvas.LeftProperty, Convert.ToDouble(nx));
}
I don't want to have to create 96 storyboards to be able to move 24 tiles in each of 4 directions. If I can get changing the "TargetNameProperty" working, that reduces to 4 storyboards. If I can also get changing the "TargetPropertyProperty" working, I'm done to a single storyboard.
Thanks in advance,
John
This link points to an article with code for reusable storyboards. It's an old article, but still applies today - maybe these classes will help you. http://www.codeproject.com/Articles/24543/Creating-and-Reusing-Dynamic-Animations-in-Silverl

Categories

Resources