|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionWhile it has not happened yet, Silverlight will change the development world much in the way that .NET did years ago. The fusion of .NET power with a lightweight web footprint is truly going to take your everyday, run of the mill ASP.NET site to new heights. Not to mention the powerful implications on desktop software, as well. (By this, I mean that with some tweaking, Silverlight can also be distributed as a desktop application.) Now, I love all the fancy animations and super transformations of which I’ve seen plenty of samples. Lord knows I enjoy watching colors fade, and circles grow and shrink, and all that general interactive design goodness. But, that’s not what really excites me about the technology. Call me crazy, but it’s the fact that I have a standardized, .NET based interface for delivering applications to the client. Yes, the web is standardized, but browsers don’t seem to be. Yes, you can use JavaScript, or VBScript, or whatever else is out there, but I like C#, and I like .NET, and I love the fact that I can now deliver a solution entirely developed in that space. We’ve had C# in the database for some time now (even in Oracle!), and for a while, we had those ActiveX documents… oops, I mean XBAPs that brought the power of WPF to the web. It’s not been since the advent of SL2, however, that we’ve really seen a technology that has such far reaching scenarios, yet is still highly lightweight (2MB). Years ago, I wrote a Tetris clone - Blox - that illustrated the power of GDI+ and some of what can be accomplished with it. I thought it would be cool to rewrite the entire app from scratch using Silverlight 2. The Rules of the GameFor those of you who spent the Eighties and Nineties in a cave, Tetris is a puzzle video game, originally designed and programmed by Alexey Pajitnov, in June of 1985, while working for the Dorodnicyn Computing Centre of the Academy of Science of the USSR in Moscow. (I would suggest reading an article on Wikipedia to gather more information on the subject, as describing it in detail is beyond the scope of this article.) It features a configuration of four blocks into seven shapes: I, L, J, S, Z, T, O. These shapes fall from the sky, and must be placed in such a way as to fill a line completely, leaving no gaps. The shapes may also be rotated to assist in attaining the best placement. When a level is filled using the variously shaped falling blocks, the line disappears and points are awarded. That’s basically it. Developing BloxAs with any game, before actually diving into the semantics of points and even forms, it is important to understand the physics of the game. These are the factors we must consider that affect the artifacts on the screen. They typically do not require user interaction, but can. In the case of our Blox API for building Tetris games, there are five primary considerations:
There are obviously more things to consider, but these five will do to explain the basic functionality. Key ComponentsThe primary classes of the Blox API are
The _blocks = new BlockCollection (this.GameWidth, this.GameHeight);
//populate width
foreach (int game_width in Enumerable.Range(0, this.GameWidth))
{
ColumnDefinition col_def = new ColumnDefinition();
//col_def.Width = new GridLength(25);
LayoutRoot.ColumnDefinitions.Add(col_def);
}
//populate height
foreach (int game_height in Enumerable.Range(0, this.GameHeight))
{
RowDefinition row_def = new RowDefinition();
//row_def.Height = new GridLength(25);
LayoutRoot.RowDefinitions.Add(row_def);
}
////populate controls
foreach (int game_height in Enumerable.Range(0, this.GameHeight))
{
LineManagers.Add(new LineManager(this.GameWidth, game_height));
foreach (int game_width in Enumerable.Range(0, this.GameWidth))
{
//add a block to that area
Block block = new Block();
block.SetValue(Grid.ColumnProperty, game_width);
block.SetValue(Grid.RowProperty, game_height);
LayoutRoot.Children.Add(block);
Blocks.Add(block, game_width, game_height);
}
}
As you can see from the sample, public class BlockCollection
{
Block[,] _blocks;
int _width, _height;
public BlockCollection(int width, int height)
{
this._height = height;
this._width = width;
_blocks = new Block[width, height];
}
public Block this[int left, int top]
{
get
{
//dont throw error, just return
if(left >= _width )
left = _width - 1;
if(top >= _height)
top = _height -1;
return _blocks[left, top];
}
}
internal void Add(Block block, int left, int top)
{
_blocks[left, top] = block;
}
}
The Blox API does not use WPF animation – it did not seem necessary; rather, the appearance of motion is achieved by turning individual public void Occupy(Shape shape)
{
LayoutRoot.Background = shape.Background;
_isoccupied = true;
}
public void Occupy(Shape shape, Thickness borders, CornerRadius corners)
{
LayoutRoot.Background = shape.Background;
LayoutRoot.BorderThickness = borders;
LayoutRoot.CornerRadius = corners;
_isoccupied = true;
}
public void Occupy(Brush background)
{
LayoutRoot.Background = background;
_isoccupied = true;
}
public void Clear()
{
LayoutRoot.Background = GameField.Singleton.FieldBackground;
_isoccupied = false;
}
This process is managed in two ways. The Shape
SquareShape square = new SquareShape();
square.Background = new SolidColorBrush(Colors.Purple);
square.Left = 10;
square.Top = 0;
control_gamefield.AddShape(square);
Inside the public void AddShape(Shape shape)
{
if (Blocks[shape.Left, shape.Top].IsOccupied)
{
if (GameOver != null)
GameOver();
}
else
{
ActiveShape = shape;
ActiveShape.Wedged += (target_shape) =>
{
ActiveShape = null;
if (ShapeWedged != null)
ShapeWedged();
};
shape.Draw(this);
shape.Initialize(this);
}
}
From this sample, you can see some of the key mechanics of the game. First is the condition by which the game ends; this is when there is no space for a newly added shape to be placed (indicated be checking the Draw/ClearDraw is one of the abstract methods of the public override void Draw(IShapeRenderer field)
{
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left + 1, Top].Occupy(this);
field.Blocks[Left, Bottom].Occupy(this);
field.Blocks[Left + 1, Bottom].Occupy(this);
}
As you can see, the general idea here is to call field.Blocks[Left, Top].Clear();
field.Blocks[Left + 1, Top].Clear();
field.Blocks[Left, Bottom].Clear();
field.Blocks[Left + 1, Bottom].Clear();
Given that some shapes will occupy different blocks when rotated, for any shape other than switch (this.ShapeAxis)
{
case 0: case 180:
field.Blocks[Left , Top].Occupy(this);
field.Blocks[Left - 1, Top].Occupy(this);
field.Blocks[Left + 1, Top].Occupy(this);
field.Blocks[Left + 2, Top].Occupy(this);
break;
case 90: case 270:
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left, Top + 1].Occupy(this);
field.Blocks[Left, Top + 2].Occupy(this);
field.Blocks[Left, Top + 3].Occupy(this);
break;
}
For the L shape which has a representation for each of its axes, switch (this.ShapeAxis)
{
case 0:
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left - 1, Top].Occupy(this);
field.Blocks[Left + 1, Top].Occupy(this);
field.Blocks[Left + 1, Top - 1].Occupy(this);
break;
case 90:
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left, Top - 1].Occupy(this);
field.Blocks[Left, Top + 1].Occupy(this);
field.Blocks[Left + 1, Top + 1].Occupy(this);
break;
case 180:
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left - 1, Top ].Occupy(this);
field.Blocks[Left - 1, Top + 1 ].Occupy(this);
field.Blocks[Left + 1, Top].Occupy(this);
break;
case 270:
field.Blocks[Left, Top].Occupy(this);
field.Blocks[Left , Top - 1].Occupy(this);
field.Blocks[Left - 1, Top - 1].Occupy(this);
field.Blocks[Left, Top + 1].Occupy(this);
break;
}
InitializeInitialize on the base abstract class looks like this: public virtual void Initialize(GameField field)
{
_timer_descent = new DispatcherTimer();
_timer_descent.Interval = TimeSpan.FromSeconds(field.GameSpeed);
_timer_descent.Tick += (sender, args) =>
{
Decend(field);
};
_timer_descent.Start();
}
This means that the primary purpose of public virtual void Descend(GameField field)
{
if (CanDescend(field))
{
ClearShape(field);
Top += 1;
DrawShape(field);
}
else
{
//stop the decent timer for now
_timer_descent.Stop();
Wedge(field);
if (Wedged != null)
Wedged(this);
}
}
As you can see from the sample above, this is where the if (_timer == null)
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(GameField.Singleton.GameSpeed);
_timer.Tick += (A, B) =>
{
for (int line = GameHeight - 1; line >= 0; line--)
{
LineManager manager = LineManagers[line];
if (manager.IsFull())
{
manager.ClearBlocks();
if (Score != null)
Score(ScoreIncrement);
manager.ShiftDown();
}
}
};
_timer.Start();
}
Line ManagersIf you re-examine the if (_timer == null)
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(GameField.Singleton.GameSpeed);
_timer.Tick += (A, B) =>
{
for (int line = GameHeight - 1; line >= 0; line--)
{
LineManager manager = LineManagers[line];
if (manager.IsFull())
{
manager.ClearBlocks();
if (Score != null)
Score(ScoreIncrement);
manager.ShiftDown();
}
}
};
_timer.Start();
}
Besides initializing the boundaries of the game surface, public bool ShiftDown()
{
if (_blocks.Count == 0)
return false;
//copy the line above me
if (LineManagers[_line_number - 1]._blocks.Count > 0)
{
ClearBlocks(); //suspect
_blocks = new List<BlockInfo>(LineManagers[_line_number - 1]._blocks);
//populate this line with the item above's blocks
foreach (BlockInfo block_info in _blocks)
{
GameField.Singleton.Blocks[block_info.Left,
_line_number].Occupy(block_info.BlockColor);
}
}
else
{
//clear all blocks
foreach (BlockInfo block_info in _blocks)
{
GameField.Singleton.Blocks[block_info.Left, _line_number].Clear();
}
_blocks = new List<BlockInfo>( LineManagers[_line_number - 1]._blocks);
}
return LineManagers[_line_number - 1].ShiftDown();
}
As you can see from the sample, this is recursive, starting from the cleared line, and moving upwards. Registering Blocks to LineManagersThe last thing to note is the abstract //stop the decent timer for now
_timer_descent.Stop();
Wedge(field);
if (Wedged != null)
Wedged(this);
As you can see, before the field.LineManagers[Top].AddBlock(new BlockInfo
{
Left = this.Left,
BlockColor = this.Background,
});
field.LineManagers[Top].AddBlock(new BlockInfo
{
Left = this.Right,
BlockColor = this.Background,
});
field.LineManagers[Bottom].AddBlock(new BlockInfo
{
Left = this.Left,
BlockColor = this.Background,
});
field.LineManagers[Bottom].AddBlock(new BlockInfo
{
Left = this.Right,
BlockColor = this.Background,
});
Putting it all TogetherThere are really two ways to get started relatively quickly with this library. The first is very simple. Open a Silverlight application project in Visual Studio (see the References for how to get access to Visual Studio and get the Silverlight toolkit). Once you have it open, add the library from this article, Block.Silverlight.Library, to your project. Now, open Page.xaml. At this point, it should look like this: <UserControl x:Class="Blox.Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid x:Name="LayoutRoot" Background="White">
</Grid>
</UserControl>
Next, add a XAML reference to the Page.xaml markup: xmlns:blox="clr-namespace:Blox.Silverlight;assembly=Blox.Silverlight"
I use Now, add the following into the Grid: <blox:SimpleTetrisGame />
Your final listing should look like this: <UserControl x:Class="Blox.Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:blox="clr-namespace:Blox.Silverlight;assembly=Blox.Silverlight.Library"
>
<Grid x:Name="LayoutRoot" Background="White">
<blox:SimpleTetrisGame />
</Grid>
</UserControl>
With this completed, build and hit F5. You should see a screen that looks like the following: This is the screen for the simple Tetris game. It represents a basic Tetris implementation, complete with levels (achieved by passing multiples of 100). Of course, this is based on my preferences as far as what score you get for filling a line. You might want a different score, or perhaps give more points for consecutive scores, for getting back down to the first line, etc. To get more ‘jiggy’ with it, you can use the <Border BorderThickness="2" BorderBrush="White"
Margin="10,10,10,100" Grid.Column="0" >
<blox:GameField x:Name="control_gamefield" GameHeight="20"
GameWidth="20" GameSpeed=".5" ScoreIncrement="10" >
<blox:GameField.FieldBackground>
<SolidColorBrush Color="Black" Opacity=".85" />
</blox:GameField.FieldBackground>
</blox:GameField>
</Border>
If you choose to do this, you will need to handle the appropriate events to get the basic game functionality working properly. Here is the way the game field is utilized in the control_gamefield.ShapeWedged += () =>
{
//control_gamefield.AddShape(control_shape_preview.NextShape);
LoadShape();
};
control_gamefield.GameOver += () =>
{
txt_game_over_message.Text = "Game Over!";
txt_final_score.Text = "Final Score:" + Score.ToString();
border_game_over.Visibility = Visibility.Visible;
};
control_gamefield.Score += (points) =>
{
Score += points;
txt_score.Text = Score.ToString();
if (Score % 100 == 0)
{
switch (Score){
case 100:
control_gamefield.FieldStopDark.Color = Colors.DarkGray;
break;
case 200:
control_gamefield.FieldStopDark.Color = Colors.LightGray;
break;
case 300:
control_gamefield.FieldStopDark.Color = Colors.Yellow;
break;
}
control_gamefield.GameSpeed -= .1;
}
};
Please explore the included code for further details. ConclusionHey, I hope you enjoy dissecting the code and creating new and exciting Tetris implementations.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||