Reputation: 31
Intro:
So what I have built is a Projectile Motion simulator of sorts where given an initial velocity and angle, every time the "Fire" button is clicked a new bullet is spawned and animated along a calculated path with MatrixAnimationUsingPath. The path is only calculated once and is used for all projectiles.
The Problem:
The problem I have run into is the number of projectiles that can be animated just fine(ex: 30) before I get application performance issues seems relatively low and it can vary widely depending on the initial velocity or angle. When the issue occurs the responsiveness of the "Fire" button is greatly slowed as well as the projectiles seem to be queued and fired in bursts rather than at time of button click.
My Attempts To Resolve The Issue:
this.Dispatcher.Invoke((Action)(() => { /*Code for creating and starting animation*/ }));
It worked fine for creating and animating a bullet but it did not solve or even improve the the main issue. Perhaps it is not the proper way to go about it.Question:
What other options might there be or is there another approach I should use with the threads or is it just the nature of the matrix animation I'm using and should I consider using another type of animation?
A possible correlation I noticed is the greater the initial velocity or distance the projectile needs to cover will decrease the number of smoothly animated bullets or inversely increase that number(e.g. slower velocity or less of a horizontal distance to cover increases the number).
I have run the application on my laptop as well as my high end desktop and have gotten similar results.
Below I have the main bit of code responsible for creating a new bullet and then animating it without multi-threading. I would have included screenshots to aid in my explanation, but I am currently unable to do so.
I thank you in advance for any and all contributions!
Here is a Drop Box link to a zipped Demo Project along with screenshots: Projectile Motion Demo
public partial class MainWindow : Window
{
Projectile bullet;
ProjectilePathMovement projMove;
private void spawnAndFireBullet()
{
// Create new bullet with: Velocity, Initial Angle, Damage
bullet = new Projectile(100, 45, 0);
bullet.Template = Resources["BulletTemplate"] as ControlTemplate;
canvas.Children.Add(bullet);
// Position the bullet at it's starting location.
Canvas.SetLeft(bullet, 50);
Canvas.SetTop(bullet, canvas.ActualHeight - 10);
projMove.animateProjectile(bullet, mainWindow);
}
}
public class ProjectilePathMovement
{
Storyboard pathAnimationStoryboard;
MatrixAnimationUsingPath projectileAnimation;
MatrixTransform projectileMatrixTransform;
ProjectileMotion pMotion = new ProjectileMotion();
public void animateProjectile(Projectile projectile, Window window)
{
NameScope.SetNameScope(window, new NameScope());
projectileMatrixTransform = new MatrixTransform();
projectile.RenderTransform = projectileMatrixTransform;
window.RegisterName("ProjectileTransform", projectileMatrixTransform);
projectileAnimation = new MatrixAnimationUsingPath();
projectileAnimation.PathGeometry = pMotion.getProjectilePath(projectile); // Get the path of the projectile.
projectileAnimation.Duration = TimeSpan.FromSeconds(pMotion.flightTime);
projectileAnimation.DoesRotateWithTangent = true;
Storyboard.SetTargetName(projectileAnimation, "ProjectileTransform");
Storyboard.SetTargetProperty(projectileAnimation, new PropertyPath(MatrixTransform.MatrixProperty));
pathAnimationStoryboard = new Storyboard();
pathAnimationStoryboard.Children.Add(projectileAnimation);
pathAnimationStoryboard.Begin(window);
}
}
class ProjectileMotion
{
// Trajectory variables.
public double trajRange = 0.0, trajHeight = 0.0, trajTime = 0.0;
private double gravity = 9.81; // m/s^2
private double velocity = 0.0; // m/s
private double angle = 0.0; // In radians
private double cosine, secant, tangent;
private double deltaX, deltaY;
private double x_component, y_component;
private double t_maxHeight;
private double start_x, start_y, current_x, current_y;
private double previousAngle = 0.0, previousVelocity = 0.0;
private PathGeometry projectilePath, previousProjectilePath; // The actual path of the object/projectile.
private PathFigure pFigure; // projectilePath is comprised of pFigure.
private PolyLineSegment polyLine; // polyLine is comprised of points.
private PointCollection points; // points is comprised of a list of all points in the path
/// <summary>
/// Returns the path the projectile would take given its initial velocity, initial angle, and starting point.
/// Pass the angle in Degrees.
/// </summary>
/// <param name="projectile"></param>
/// <param name="vel"></param>
/// <param name="ang"></param>
/// <param name="startPoint"></param>
/// <returns></returns>
public PathGeometry getProjectilePath(UIElement projectile, double vel, double ang, System.Windows.Point startPoint)
{
// Calculate the necessary values.
calculateValues(projectile, ang, vel);
// Derive the object's/projectile's path.
return deriveProjectilePath(startPoint);
}
public double getGravity()
{
return gravity;
}
private void calculateValues(UIElement projectile, double ang, double vel)
{
// Convert the angle from Degrees to Radians.
angle = ang * (Math.PI / 180);
velocity = vel;
cosine = Math.Cos(angle);
secant = 1 / cosine;
tangent = Math.Tan(angle);
deltaX = Math.Cos(angle);
deltaY = Math.Sin(angle);
// Get current coordinates.
start_x = Canvas.GetLeft(projectile);
start_y = Canvas.GetTop(projectile);
current_y = start_y;
current_x = start_x;
// Calculate the horizontal and vertical components of initial velocity.
// Xvo = Vo * Cos(angle)
// Yvo = Vo * Sin(angle)
x_component = velocity * Math.Cos(angle);
y_component = velocity * Math.Sin(angle);
// Calculate time to reach max height. t max = Vyo / 9.8
t_maxHeight = y_component / gravity;
// Calculate max height of projectile. h = Yo + Vyo·t - 0.5·g·t^2
trajHeight = 0 + (y_component * t_maxHeight) - (.5 * gravity * t_maxHeight * t_maxHeight);
//Calulate max range of projectile.
trajRange = (2 * (velocity * velocity) * Math.Sin(angle) * Math.Cos(angle)) / gravity;
// Calculate flight time.
trajTime = 2 * t_maxHeight;
}
private PathGeometry deriveProjectilePath(System.Windows.Point pt)
{
projectilePath = new PathGeometry();
pFigure = new PathFigure();
start_x = pt.X;
start_y = pt.Y;
current_y = start_y;
pFigure.StartPoint = pt;
polyLine = new PolyLineSegment();
points = new PointCollection();
// Checks if the angle and velocity for this projectile is the same as last time. If it is the same there is no need to recalculate the path of the projectile, just use the same one from before.
if (previousAngle != angle && previousVelocity != velocity)
{
// Loop to obtain every point in the trajectory's path.
for (current_x = start_x; current_x <= trajRange; current_x++)
{
current_y = start_y - current_x * tangent + ((gravity * current_x * current_x) / (2 * (velocity * velocity * cosine * cosine))); // Y = Yo + X*tan - ( (g*X^2) / 2(v*cos)^2 ) Trajectory Formula to find the 'y' value at a given 'x' value.
points.Add(new System.Windows.Point(current_x, current_y));
}
// If the last x-coordinate value exceeds the actual range of projectile set x = to actual range to
// obtain actual y-coordinate value for final trajectory point.
if (current_x > trajRange)
{
current_x = trajRange;
current_y = start_y - current_x * tangent + ((gravity * current_x * current_x) / (2 * (velocity * velocity * cosine * cosine))); // Y = Yo + X*tan - ( (g*X^2) / 2(v*cos)^2 ) Trajectory Formula to find the 'y' coord given an 'x' value.
points.Add(new System.Windows.Point(current_x, current_y));
}
polyLine.Points = points;
pFigure.Segments.Add(polyLine);
projectilePath.Figures.Add(pFigure);
}
else
{
projectilePath = previousProjectilePath;
}
// Freeze the PathGeometry for performance benefits?
projectilePath.Freeze();
previousVelocity = velocity;
previousAngle = angle;
previousProjectilePath = projectilePath;
return projectilePath;
}
}
Upvotes: 3
Views: 75
Reputation: 9827
I have seen your code and recommending following solution. Following solution will bring needed improvement in no time without any big code changes.
Add namespace : System.Windows.Threading;
Comment out your fireButton_Click function completely. And copy paste following code fragment as it is :
Queue<FireBulletDelegate> bulletQueue = new Queue<FireBulletDelegate>();
delegate void FireBulletDelegate();
DispatcherTimer bulletQueueChecker;
const int threshold = 100;
private void fireButton_Click(object sender, RoutedEventArgs e)
{
if (bulletQueue.Count > threshold) return;
FireBulletDelegate d = new FireBulletDelegate(spawnAndFireBullet);
bulletQueue.Enqueue(d);
if (bulletQueueChecker == null)
{
bulletQueueChecker = new DispatcherTimer(
TimeSpan.FromSeconds(0.2),
DispatcherPriority.Render,
(s1, e1) =>
{
if (bulletQueue.Count > 0)
(bulletQueue.Dequeue())();
//spawnAndFireBullet();
},
fireButton.Dispatcher);
}
else if (!bulletQueueChecker.IsEnabled)
{
bulletQueueChecker.Start();
}
}
This solves your problem of bursting bunch of bullets.
Problem was occuring because of many button click messages which were probably exploding the message queue, system will process message queue at its own pace. So, we need a check at those click events. This we are achieving using a threshold value, and processing clicks with 0.2 seconds interval. More improvement can be done in your code. I am using FileBulletDelegate, we can use Bullet too for queue item, but that will bring more code changes. One benefit of using delegates could be Asynchronous invocation.
Upvotes: 1