Trivial Physics Simulations (...in Silverlight) - Part 4
Tying it all together has zip to do with the physics simulation, but there are a couple of interesting bits which I'll paste into here. The rest you can grab from the solution that I'll post up; feel free to ask questions.
The Game Loop
The heart of the app is a method call which renders one frame only. The trick is calling that method for each frame, and having that method do a very simple fixed set of operations that updates the particles for that frame only.
For the "call-each-frame" part, I use a very nice method stolen from Bill Reiss:
Each frame, that loop calls my method:
1: void _GameLoop_Update(TimeSpan elapsed)
This method does the following:
- Checks to see if we've got as many particles as the user wants us to have, and if not, adds a few more. I only add a small [random] number each frame, to avoid creating large spurts.
- Updates the physics for all particles by calling each particle's RunFrame method. Because this uses the elapsed time since the last frame, the particles maintain a correct speed on-screen, even if the frame rate drops/increases.
- Renders the particles to the canvas. More on that soon.
- Removes any particles that have died, to prevent the particle list from growing indefinitely.
The AddNewParticles method is where you need some creativity, and is the method you would alter if you wanted to change the nature of the fireworks (say, to create an explosion effect instead of a fountain). This method relies on simple heuristics to generate initial velocities. Here's the relevant bit:
1: // Generate some random starting conditions for this particle
2: Vector2D vel = Vector2D.FromSpeedAndAngle(200 + this.MaxSpeed,
3: ((180 - this.SpreadAngle) / 2) + rnd.Next(this.SpreadAngle));
4: double milliseconds = 200 + rnd.Next(3000);
5: double radius = 2 + rnd.Next(10);
6: Color color = FireworksColours[ rnd.Next(FireworksColours.Length) ];
It's really all in lines 2 & 3: The initial velocity of the particle. We use the user's UI selections as limiters to random values. The more randomness, the more "natural" things look.
In making some effort to separate the logic of the physics from the UI rendering part, I created a FireworksRenderingHelper class that is given a list of FireworksParticle and a Canvas, and knows how to render one to the other.
Pre-Silverlight, I'd reset each frame, clear the background, render all particles, and show the frame. But with Silverlight, it already has internal methods for drawing objects to the screen, buffering, etc. So what I do is add my shapes as the Canvas's children and let it draw.
The catch is that I don't want to remove/add all the children each frame - it seems wasteful. This is why I added the Custom field within FireworksParticle - I use it to track a reference to the child of the canvas. At render time, if the field is null, I create a new Ellipse as a child and add it to the canvas. If it's not null, then I retrieve it and modify its position. Here are the loop internals ("p" is the current particle):
1: UIElement canvasChild;
2: if (p.Custom != null)
4: canvasChild = (UIElement)p.Custom;
8: canvasChild = new Ellipse()
10: Width = p.Radius,
11: Height = p.Radius,
12: Fill = new SolidColorBrush(p.GlowColor)
14: p.Custom = canvasChild;
18: canvasChild.Opacity = 1-p.Opacity;
19: canvasChild.SetValue(Canvas.LeftProperty, p.Position.X);
20: canvasChild.SetValue(Canvas.TopProperty, p.Position.Y);
Removing Dead Particles
The one non-trivial thing about this is that you can't use a simple foreach loop and remove any particles where Alive==false. Doing so would be modifying the collection while enumerating it, and you'd get slapped by the runtime. Normally, you'd get around this by separating things into two steps (get the list of dead particles, then remove them individually).
It seems that LINQ allows you to do this in one line, and I think it's due to the ToList() in there:
1: private void RemoveDeadParticles()
3: _Particles.Where(p => !p.Alive).ToList().ForEach(p => _Particles.Remove(p));
In the final installment, I'll post the zipped up solution (and if I can work out how, the actual Silverlight app), and I'll explain how you can take this further.