Source code: http://www.bluerosegames.com/spacerocks/spacerocks_9_rocks.zip
Everything we’ve done so far has to do with the ship, but there is some good plumbing in place to get the asteroids themselves going, and that’s going to be our next step. For now we’ll just get some asteroids on the screen. In later steps we’ll worry about placement, movement, and collisions.
Just like with the ship, we can create a Silverlight Templated Control for the asteroids and then change it to inherit from Sprite. By inheriting from Sprite we’ll get support for movement based on a velocity and wrapping at the edges of the screen. So create a new Silverlight Templated Control from the new item wizard and call it Rock.cs. As we previously saw with the ship, this creates a new entry in Generic.xaml which contains the control template for the Rock sprite. You can edit this in Blend or by hand to make it look how you want. The asteroids are a bit different from the ship since there are 3 different shapes for the asteroids. Even the smaller versions (asteroids come in large, medium, and small) have those same 3 shapes. You could create a different Silverlight Templated Control for each variation and have them all inherit from Rock (which inherits from Sprite) but since they are so similar we can do a little visual magic instead.
What we can do is define the visuals for each of the asteroid shapes and put them all into the Rock template. Then in OnApplyTemplate we can show one and hide the others. First the Rock XAML template:
<Style TargetType="local:Rock">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Rock">
<Canvas x:Name="LayoutRoot" Width="60" Height="60" RenderTransformOrigin=".5,.5">
<Path x:Name="rock0"
Data="M-4.125,9.75 L9.375,-4.25 L24.645,2.67 L37.875,-1.75 L49.375,10.75 L40.875,16.75 L50.875,27.25 L38.625,46 L14.125,41 L5.875,45.5 L-2.875,37 L2.375,24.25 z" Stretch="Fill" Stroke="White" UseLayoutRounding="False" Margin="3,3,1,5.75" StrokeThickness="2"/>
<Path x:Name="rock1"
Data="M-6.75,19.75 L9.375,-4.25 L31.75,-5 L49.375,10.75 L49.75,29.25 L32.5,49.75 L19.25,51.5 L19.35429,30 L5.875,45.5 L-5.5,30.25 L6,24 z" Stretch="Fill" Stroke="White" UseLayoutRounding="False" StrokeThickness="{Binding StrokeThickness, ElementName=rock0}" Margin="0.375,2.25,2.125,0.25"/>
<Path x:Name="rock2"
Data="M-4.125,9.75 L11.5,9.5 L4.75,0 L26,-5.75 L49.375,10.75 L51,19 L32.75,26.25 L46.75,36 L40,45.75 L31,38.5 L5.75,46.25 L-6.5,27 z" Stretch="Fill" Stroke="White" UseLayoutRounding="False" StrokeThickness="{Binding StrokeThickness, ElementName=rock0}" Margin="0.625,1.5,0.875,5.5"/>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
By naming each of the paths we can find the one we want to show in the OnApplyTemplate method. Note that rock0 specifies a StrokeThickness and the other two use element to element binding to reference the rock0 StrokeThickness. This means that if we want to set the StrokeThickness we only have to set it one place. The reason we need this will become evident soon.
Now for the code for the Rock, we can modify it as follows:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SpaceRocks
{
public class Rock : Sprite
{
int rockType = 0;
public double Scale { get; private set; }
public Rock() : this(1, 0)
{
}
public Rock(double scale, int rockType)
{
this.DefaultStyleKey = typeof(Rock);
Scale = scale;
this.rockType = rockType;
}
public override void OnApplyTemplate()
{
var rock0 = this.GetTemplateChild("rock0") as Path;
var LayoutRoot = this.GetTemplateChild("LayoutRoot") as Panel;
LayoutRoot.Width = 60 * Scale;
LayoutRoot.Height = 60 * Scale;
ScaleTransform scaleTransform = new ScaleTransform();
scaleTransform.ScaleX = scaleTransform.ScaleY = Scale;
LayoutRoot.RenderTransform = scaleTransform;
rock0.StrokeThickness = 2 / Scale;
var currentRock = this.GetTemplateChild(string.Format("rock{0}", rockType)) as FrameworkElement;
foreach (FrameworkElement element in LayoutRoot.Children)
{
if (element == currentRock)
{
element.Visibility = Visibility.Visible;
}
else
{
element.Visibility = Visibility.Collapsed;
}
}
base.OnApplyTemplate();
}
}
}
The scale value allows us to handle the 3 sizes of rocks. The large rock will have a scale of 1, the medium a scale of .5, and the small a scale of .25. So for each smaller size of rock the scale changes by a factor of 2. We need to set the width and height of the root element so that the base Sprite class centers the sprite properly at different scales. Then we can actually scale the visuals using a ScaleTransform.
If we just used a ScaleTransform to make the rock smaller, the StrokeThickness would also be made narrower based on the scale. To make the StrokeThickness consistent at all scales we need to divide the desired thickness by the current scale. This will make the asteroid always have a final stroke thickness of 2 no matter what the scale. By setting the StrokeThickness on rock0 the other two shapes will also get that value through data binding.
The rockType value can be 0, 1, or 2 and will be passed in when the asteroid is created. Based on this, we’ll find the desired element to show in the template and hide the others. Finally we need to call the OnApplyTemplate method of the base class because it does some important things related to centering the sprites based on the width and height of the sprite.
To test out the rock types and sizes, let’s just add some test code to the Game constructor for now. This code will draw all combinations of sizes and shapes, 9 in total.
for (int i = 0; i < 3; i++)
{
double scale=1;
for (int j = 0; j < 3; j++)
{
Rock rock = new Rock(scale, i);
rock.Position = new Vector2(30 + i * 60, 30 + j * 60);
LayoutRoot.Children.Add(rock);
scale /= 2;
}
}
And this is the resulting output:
In the next step we’ll work on generating the asteroids needed for the start of a level.