Classic games are so much fun to play, and they are even more fun to try to recreate. A few months ago, I decided to build an asteriods clone in Silverlight in order to familiarize myself with the technology. I had so much fun with the project that I decided to build a Frogger clone this time. This article will walk you through the steps that I used to recreate this arcade classic.
Since I am by no means a graphic designer, I needed to find some pre-made images for my game. I figured that I would be able to at least find a few screenshots of an older Frogger game using Google image search. Luckily, I was able to find the Xbox live arcade screenshots which gave me the base images I needed. Now that I had something to work with, I opened the screenshot in GIMP and started cropping out images. This process took a while because I needed to crop out each unique image and make the background transparent. I ended up with five different vehicles, three different size logs, three different turtles, a frog, the frog head, and finally a plain background image. Here are some examples of the images that were cropped out:
froghead,
tractor, and
frog (our main character).
Now that I have the images out of the way, it is time to start coding. For my project I used Visual Studio 2008 with the Silverlight Tools Add-on. This add-on allows you to create Silverlight projects directly from Visual Studio, which means you do not have to download any of the Expression products.
Once you get the add-on installed, it is time to fire up Visual Studio and create a new project using the 3.5 Framework. Then, select either VB or C# as your language. Finally, choose to create a new Silverlight Application. Follow the prompts, and when the Add Silverlight Application dialog appears, choose the option to Automatically generate a test page to host Silverlight at build time. I like this option because when you run your project from Visual Studio, an Internet Explorer instance will appear so you can test out your application.
Now that your project has been created, you can navigate to the Solution Explorer and open up the design surface for Page.xaml. My first step was to take the background image that I created from the original and set it as the background image for the page. This was done by adding an Image
element to the Canvas
.
<Image Source="media/background.png" Stretch="None" Canvas.Top="50"/>
I also wanted to add some space to the top and bottom of the page for displaying the score and some other information that is relevant to the game. Therefore, I changed the width and the height of the canvas to be large enough to contain the background image and also have enough margin for the footer and header. Now that the basic elements were on the page, I needed to think about how I was going to program the game. Just for some background information, the whole idea of the game is for the frog to safely make it to the “homes” shown at the very top of the screen. In order to get home, the frog has to make it through the five lane highway by avoiding contact with the cars. Then, the frog must hop on the logs to safely make it to home. So, with that bit of information, I figured I needed a way to define “lanes”. The lanes would basically create physical boundaries for the sprites to move in. I ended up using Rectangle
s (these are represented by the red outlines you see on the image to the left) to achieve this affect. After creating all the Rectangle
s, here is what my screen ended up looking like. Also, you will notice that there are some additional rectangles at the top which define the goals where you want the frog to end up once he crosses all the obstacles.
Now, it is time to discuss the mechanics of the game engine. Let me start by giving you the definition of a Sprite (from Wikipedia):
In computer graphics, a sprite is a two-dimensional/three-dimensional image or animation that is integrated into a larger scene.
Sprites were originally invented as a method of quickly compositing several images together in two-dimensional video games using special hardware. As computer performance improved, this optimization became unnecessary, and the term evolved to refer specifically to the two dimensional images themselves that were integrated into a scene. That is, figures generated by either custom hardware or by software alone were all referred to as sprites. As three-dimensional graphics became more prevalent, the term was used to describe a technique whereby flat images are seamlessly integrated into complicated three-dimensional scenes.
More specifically, in my game, the logs, vehicles, and turtles are all considered sprites. In order to make my things simpler to program, I leveraged a common base class called SpriteBase
. Using a common base class allows you to leverage code reuse and more importantly polymorphism. This is an important factor when you are trying to move around large amounts of objects and applying collision detection algorithms to each one. For example, if you want to move a sprite around on the screen, you need to do it by changing the X and Y coordinates of that object. Therefore, the SpriteBase
class has an X
and Y
property:
/// <summary>
/// The Y position of the sprite
/// </summary>
public virtual double Y
{
get { return (double)this.GetValue(Canvas.TopProperty); }
set { this.SetValue(Canvas.TopProperty, value); }
}
Moving on, we have now established that the sprites are the basic building blocks of the game. So, the first step is to get the sprites on to the screen. Since we want the game to be challenging, we do not want to hard code the locations of each sprite; therefore, we will have to use a randomizer. The randomizer will be used to make the cars to move at different speeds, determine if items move left to right or right to left. In addition, we want a variety of different cars, logs, and turtles on the screen.
Previously, we discussed how I used Rectangle
s to create logical boundaries for how the various sprites would move within the game. Basically, what I did was overlay Rectangle
s on the background image to create “lanes” for the road and water. Each Rectangle
was defined in the XAML file and has a unique name. I reference each unique name in my code and add it to an array inside my code. Now, I realize that I could have just dynamically created these items in code, but I like being able to see the layout of the game in design mode. Below is the section of the XAML file which creates the lanes on the road. You will notice that the lanes all have a red outline around them. This is so I can see the lines at design time. When the application loads, I loop over the Rectangle
s and set the Stroke
to a transparent color.
<Rectangle x:Name="StreetLane5" Stroke="Red"
StrokeThickness="1" Canvas.Left="0"
Canvas.Top="440" Width="600" Height="50"/>
<Rectangle x:Name="StreetLane4" Stroke="Red"
StrokeThickness="1" Canvas.Left="0"
Canvas.Top="490" Width="600" Height="50"/>
<Rectangle x:Name="StreetLane3" Stroke="Red"
StrokeThickness="1" Canvas.Left="0"
Canvas.Top="540" Width="600" Height="50"/>
<Rectangle x:Name="StreetLane2" Stroke="Red"
StrokeThickness="1" Canvas.Left="0" Canvas.Top="590"
Width="600" Height="50"/>
<Rectangle x:Name="StreetLane1" Stroke="Red"
StrokeThickness="1" Canvas.Left="0"
Canvas.Top="640" Width="600" Height="50"/>
Now that I have an array with the Rectangle
s added to it, I can rely on the X and Y coordinates of those Rectangle
s to control the placement of each sprite. Now, I create a nested for
loop that iterates over each Rectangle
and adds a random number of vehicles to each “lane”. Each lane is assigned a random speed and direction for the vehicles to move in. Also, the cars in each lane are randomly assigned. Here is the code:
private void CreateVehicles()
{
//add some vehicles to each lane using some random logic
for (int i = 1; i <= 5; i++)
{
Boolean rightToLeft = (_randomizer.Next(0, 11) % 2 == 0);
Double startX = _randomizer.Next(0, 150);
Double speed = (double)_randomizer.Next(5, 20) * .1;
for (int j = 0; j < 4; j++)
{
VehicleType vType = (VehicleType)_randomizer.Next(0, 5);
Vehicle vehicle = new Vehicle(vType, rightToLeft);
if ((startX + vehicle.ActualWidth) >= this.Width) break;
vehicle.Lane = i;
vehicle.X = startX;
vehicle.XSpeed = speed;
vehicle.Y = GetNewYLocation(vehicle, i);
this.LayoutRoot.Children.Add(vehicle);
_sprites.Add(vehicle);
int spacing = _randomizer.Next((int)(_frog.ActualWidth * 3),
(int)(_frog.ActualWidth * 4));
startX += (vehicle.ActualWidth + spacing);
}
}
}
You will notice that the cars are randomly spaced apart. I used the width of the frog as a basic unit of measure for this. After all, the frog needs to be able to fit between the cars if it wants to make across the road. Also, note that each new sprite is assigned a lane. This is important because it helps me determine what items are in which lane. Since the frog moves forwards or backwards only one lane at a time, I can easily determine which objects the frog can potentially collide with. The logs and turtles are created in a very similar fashion. The newly created sprites are all added to a private list named _sprites
. Having the sprites in a collection allows me to easily iterate over them for animation and collision detection purposes.
In order to animate the sprites, I create a new System.Windows.Threading.DispatcherTimer
class and implement the Tick
event. In the Tick
event, I call a method called MoveSprites()
. This method iterates over the list of sprites that were added to the screen and updates their X and/or Y coordinates. In addition, it also detects if a sprite moves off of the screen. When a vehicle, log, or turtle moves off the screen, they are replaced by a new random sprite. This makes the game a little more interesting. Finally, if the frog happens to be hopping across the river, I detect which object the frog is sitting on and move the frog at the same speed as that object. Let’s take a look at the code:
private void MoveSprites()
{
for (int i = 0; i < _sprites.Count; i++)
{
Boolean remove = false;
SpriteBase sprite = _sprites[i];
double newX = sprite.X;
//check the direction of the sprite and modify accordingly
if (sprite.RightToLeft) {
newX -= sprite.XSpeed;
remove = (newX + sprite.ActualWidth < 0);
}
else {
newX += sprite.XSpeed;
remove = (newX > this.Width);
}
//when items go off the screen we replace them with a new random sprite
if (remove == true){
LayoutRoot.Children.Remove(sprite);
SpriteBase replacement;
if (sprite.GetType() == typeof(Vehicle))
replacement = new Vehicle((VehicleType)_randomizer.Next(0, 5),
sprite.RightToLeft);
else if (sprite.GetType() == typeof(Log))
replacement = new Log((LogSize)_randomizer.Next(0, 3), sprite.RightToLeft);
else
replacement = new Turtle((TurtleType)_randomizer.Next(0, 3),
sprite.RightToLeft);
//find the min or max X position of the sprite in the same lane
var query = from x in _sprites
where
x.Lane == sprite.Lane
orderby
x.X ascending
select
x;
SpriteBase lastSprite;
//right to left means you want the max because
//when the item wraps around the screen
//it will appear in the higher range of X values
if (sprite.RightToLeft){
lastSprite = query.Last();
if ((lastSprite.X + lastSprite.ActualWidth) >= this.Width)
newX = (lastSprite.X + lastSprite.ActualWidth) + _randomizer.Next(50, 150);
else
newX = this.Width;
}
else{
lastSprite = query.First();
if (lastSprite.X <= 0)
newX = (lastSprite.X) - _randomizer.Next(50, 150) - replacement.ActualWidth;
else
newX = 0 - replacement.ActualWidth;
}
replacement.XSpeed = sprite.XSpeed;
replacement.Lane = sprite.Lane;
replacement.Y = GetNewYLocation(replacement, sprite.Lane);
_sprites[i] = replacement;
sprite = replacement;
LayoutRoot.Children.Add(replacement);
}
//when items start to move off the screen we clip part of the object so we do
//not see it hanging off the screen
if ((newX + sprite.ActualWidth) >= this.Width){
if (sprite.X < this.Width) {
RectangleGeometry rg = new RectangleGeometry();
rg.Rect = new Rect(0, 0, this.Width - sprite.X, sprite.ActualHeight);
sprite.Clip = rg;
sprite.Visibility = Visibility.Visible; //forces a repaint
}
}
//if the frog is on a object in the river then move it at the same rate
if (_frog.WaterObject == sprite){
double frogX = _frog.X - (sprite.X - newX);
Point p = new Point(frogX, _frog.Y);
MoveFrog(p);
}
sprite.X = newX;
}
}
Since objects really only move in a horizontal direction, I never recalculate the Y position of a sprite after it is placed on the screen. I am only recalculating the X position. Towards the middle of the function, you see some LINQ code. The LINQ code is used to figure out where to position the “replacement” sprite when something moves off the screen. Because the sprites have different lengths, I have to dynamically determine how far to the left or right an object needs to be placed when it is added to the screen. If the objects in a lane are moving right-to-left, we need to find the maximum X value; if left-to-right, we will look for the minimum X value. The diagram below will help clarify this logic:
So, now that you understand how the objects move around the screen, lets talk about collision detection. Collision detection in this game is very simple. Since the frog can only be in one lane at a time, I only perform collision detection on a particular group of objects at a time. Once again, I use LINQ to simplify the task:
private bool CheckForCollisions()
{
//check only the current lane to see if the frog is being hit by any vehicles.
//rely on the X coordinates only since the frog basically sits in the middle of the lane.
var query = from x in _sprites
where
x.Lane == _currentRow &&
((_frog.X >= x.X) &&
(_frog.X <= (x.X + x.ActualWidth)) ||
((_frog.X + _frog.ActualWidth) >= x.X) &&
((_frog.X + _frog.ActualWidth) <= (x.X + x.ActualWidth)))
select
x;
return (query.Count() > 0);
}
My collision detection algorithm is primary concerned only with X coordinates. Because the frog sits in the middle of a lane, I can rely on the fact that objects in the same lane are already within the same Y range of values. As I mentioned before, when the frog is on the road, he will die if he is hit by a vehicle. Therefore, when the frog is in lanes 1 through 5 (the road) and the result of the CheckForCollisions()
method is true
, the frog is road kill. However, when the frog is in lanes 6 through 10 (the water) and a collision occurs, then the frog is OK because that means he is sitting on top of a log or turtle. For this reason, when the frog is moving through water, I have a method called GetObjectUnderFrog()
which will return a reference to the sprite the frog is on top of. If the frog is not sitting on anything, the method will return null
, which means the frog fell in the water. If a sprite is returned, then a property called WaterObject
is set so I keep a reference to the object the frog is sitting on. This property is used in the MoveSprites()
method (shown above) to help move the frog at the same rate as the object it is sitting on. This gives the appearance that the frog is taking a ride. Here is the code:
void _mainLoop_Tick(object sender, EventArgs e)
{
MoveSprites();
//only check for collisions when the frog is on the road
if (_currentRow > 0 && _currentRow < 6) {
if (CheckForCollisions() == true){
//frog is a pancake
KillFrog();
}
}
//you are in the water
if (_currentRow > 6 && _currentRow < 12) {
_frog.WaterObject = GetObjectUnderFrog();
if (_frog.WaterObject == null) {
KillFrog();
}
if (_frog.X > this.Width || _frog.X < 0)
KillFrog();
}
else
{
_frog.WaterObject = null;
}
}
原文http://www.codeproject.com/KB/silverlight/FroggerInSilverlight.aspx
我基本很多时间都扎在这个网站里面学习,我的浏览器收藏家也收藏了不少这个网站的文章,我把自己看过的都放在博客里面,这样以后我想找文章就直接打开博客。
大家可以打开原文连接进行下载,但是需要注册,为了方便大家下载,我把源文件放在下面了。
建议大家保存这个网站,这个网站对于silverlight爱好者来说真的不错的。
还有大家一定要多看英文文章,英文真是太重要了。就算一开始不习惯也没有关系,还有建议大家装个灵格斯词典。
代码下载: