Click here to Skip to main content
Click here to Skip to main content

Snail Quest

, 7 Apr 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
A maze game using WPF, A* Search algorithm, C# Midi Toolkit and Irrklang audio engine

SnailQuest

Table of Contents

Introduction

In the eighties, when I was still a young boy, I used to play a nice videogame where a yellow, ball-shaped character moved around inside a maze, eating little squares and escaping from 4 color ghosts.

Many years later, I finally came up with a game that doesn't intend to be as brilliant as the original Pac Man game, but rather inspired on it.

As for the game name, I thought it would be funny, since it sounds like some classic PC games, such as "Space Quest", "King's Quest".

Despite the fun atmosphere behind the game concept, this article and the game here are intended to show how to mix up different technologies together to provide an attractive visual and auditive experience.

System Requirements

To use the Snail Quest game provided with this article, if you already have Visual Studio 2010, that's fine. If you don't, you can download the following 100% free development tool directly from Microsoft:

  • Visual C# 2010 Express

    Also, you must download and install Rx for .Net Framework 4.0 by clicking the button with the same name from the DevLabs page:

  • DevLabs: Reactive Extensions for .NET (Rx)

    YouTube Video

    Before proceeding with reading the rest of the article, you might at this point be somehow curious about how is game in action, so I uploaded a video for you:

    The Game Rules

    The game rules are quite simple: you must walk (or crawl) your Snail around the maze and collect all pearls there, while getting away from the four squids.

    Once you get all pearls of the maze, you move on to the next level, with a different maze where you'll be collecting those pearls too.

    When you finish the last level, you get a congratulations message and the game ends.

    Splash Screen: Bubbles Animation

    The game intro shows the game title with the cool font "Jokerman". At first I thought of using the "Comic Sans" on it, but I believe Jokerman fits better.

    This screen shows some bubbles at tbe background, as you can see. Those bubbles are dinamically generated by a function, and animated using WPF animation. There are 10 fixed bubbles, which are animated vertically, each one with a different diameter and speed. A second animation applies to the opacity of the bubbles, and makes them appear suddenly and vanish in the deep ocean when they reach the top of the screen.

    The animation goes on and on, lasting forever. This is achieved by setting the RepeatBehavior of Storyboard class to RepeatBehavior.Forever value.

            private void CreateBubbles()
            {
                Storyboard sbPressSpace = this.FindResource("sbPressSpace") as Storyboard;
                sbPressSpace.Begin();
    
                var linearBubbleBrush = new LinearGradientBrush() 
    			{ StartPoint = new Point(1, 0), EndPoint = new Point(0, 1) };
                linearBubbleBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x00, 0x20, 0x40), 0.0));
                linearBubbleBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
    
                var radialBubbleBrush = new RadialGradientBrush() 
    			{ Center = new Point(0.25, 0.75), RadiusX = .3, RadiusY = .2, GradientOrigin = new Point(0.35, 0.75) };
                radialBubbleBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0));
                radialBubbleBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
    
                for (var i = 0; i < 10; i++)
                {
                    var diameter = 10 + (i % 4) * 10;
                    var ms = 1000 + i % 7 * 500;
    
                    var ellBubble = new Ellipse()
                    {
                        Width = diameter,
                        Height = diameter,
                        Stroke = linearBubbleBrush,
                        Fill = radialBubbleBrush,
                        StrokeThickness = 3
                    };
    
                    ellBubble.SetValue(Canvas.LeftProperty, i * (40.0 + 40.0 - diameter / 2));
                    ellBubble.SetValue(Canvas.TopProperty, 0.0 + 40.0 - diameter / 2);
    
                    cnvBubbles.Children.Add(ellBubble);
    
                    var leftAnimation = new DoubleAnimation()
                    {
                        From = 40.0 * i,
                        To = 40.0 * i,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    var topAnimation = new DoubleAnimation()
                    {
                        From = 200,
                        To = 0,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    var opacityAnimation = new DoubleAnimation()
                    {
                        From = 1.0,
                        To = 0.0,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    Storyboard.SetTarget(leftAnimation, ellBubble);
                    Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
                    Storyboard.SetTarget(topAnimation, ellBubble);
                    Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
                    Storyboard.SetTarget(opacityAnimation, ellBubble);
                    Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
                    leftAnimation.EasingFunction = new BackEase() 
    				{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
                    topAnimation.EasingFunction = new BackEase() 
    				{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
    
                    var sb = new Storyboard();
                    sb.Children.Add(leftAnimation);
                    sb.Children.Add(topAnimation);
                    sb.Children.Add(opacityAnimation);
                    sb.RepeatBehavior = RepeatBehavior.Forever;
    
                    bubbles.Add(ellBubble);
                    storyBoards.Add(sb);
    
                    sb.Begin();
                }
            }
    

    Creating The Maze: From Text File To WPF

    The mazes are created dinamically, from plain text files. Each text file must rectangular, having 15 x 10 characters, where different characters have different meanings:

    • 1 - Indicates the blocks of glass that makes up the walls in the maze. Each block might have different shapes, based on the values of the neighboring cells.
    • [blank space] - A blank space allows snail and squids to walk freely inside the maze corridors.
    • A, B, C and D - The initial positions of the red, yellow, white and blue squids, respectively.
    • o - The position of each pearl.
    • * - The position of each starfish.
    • S - The initial position of the snail.

    I would have been nice to create a level editor, but I think it's not the focus of the game. Instead, you can use an ordinary text editor to do it.

            private void LoadMaze(int level)
            {
                collectedPearls.Clear();
                collectedStarfishes.Clear();
                grdMaze.Children.Clear();
    
                for (var i = 0; i < starfishes.Count(); i++)
                {
                    cnvMain.Children.Remove(starfishes[i]);
                }
    
                starfishes.Clear();
    
                for (var i = 0; i < pearls.Count(); i++)
                {
                    cnvMain.Children.Remove(pearls[i]);
                }
    
                pearls.Clear();
    
                for (var i = 0; i < mazeGlasses.GetLength(0); i++)
                {
                    for (var j = 0; j < mazeGlasses.GetLength(1); j++)
                    {
                        mazeGlasses[i, j] = null;
                    }
                }
    
                for (var i = 0; i < mazeValues.GetLength(0); i++)
                {
                    for (var j = 0; j < mazeValues.GetLength(1); j++)
                    {
                        mazeValues[i, j] = ' ';
                    }
                }
    
                var fileName = string.Format(@"Mazes\Level{0}.txt", level);
    
                using (var sr = new StreamReader(fileName))
                {
                    var l = 0;
                    while (!sr.EndOfStream)
                    {
                        string line = sr.ReadLine();
    
                        for (var c = 0; c < line.Length; c++)
                        {
                            mazeValues[c, l] = line[c];
    
                            if (mazeValues[c, l] == '1')
                            {
                                var glass = new Glass();
                                glass.SetValue(Grid.ColumnProperty, c);
                                glass.SetValue(Grid.RowProperty, l);
                                grdMaze.Children.Add(glass);
                                mazeGlasses[c, l] = glass;
                            }
                            else if (mazeValues[c, l] == '*')
                            {
                                var starfish = new Starfish();
                                starfish.SetValue(Canvas.LeftProperty, 0.0);
                                starfish.SetValue(Canvas.TopProperty, 0.0);
                                starfish.SetValue(Canvas.ZIndexProperty, -1);
                                cnvMain.Children.Add(starfish);
                                starfish.Throw(new Point(c, l), new Point(c, l), 
    							TimeSpan.FromMilliseconds(50), null);
                                starfishes.Add(starfish);
                            }
                            else if (mazeValues[c, l] == 'o')
                            {
                                var pearl = new Pearl()
                                {
                                    Width = 30,
                                    Height = 30
                                };
                                pearl.SetValue(Canvas.LeftProperty, 0.0);
                                pearl.SetValue(Canvas.TopProperty, 0.0);
                                pearl.SetValue(Canvas.ZIndexProperty, -1);
    
                                cnvMain.Children.Add(pearl);
                                pearl.PlaceAt(new Point(c, l));
                                pearls.Add(pearl);
                            }
                            else if (mazeValues[c, l] == 'S')
                            {
                                snail.OriginalCellPoint = new Point(c, l);
                            }
                            else if ("ABCD".Contains(mazeValues[c, l]))
                            {
                                var index = "ABCD".IndexOf(mazeValues[c, l]);
    
                                squids[index].OriginalCellPoint = new Point(c, l);
                            }
                        }
                        l++;
                    }
                }
    
                for (var c = 0; c < mazeWidth; c++)
                {
                    for (var l = 0; l < mazeHeight; l++)
                    {
                        var topValue = ' ';
                        var bottomValue = ' ';
                        var leftValue = ' ';
                        var rightValue = ' ';
    
                        if (l > 0)
                            topValue = mazeValues[c, l - 1];
                        if (l < mazeHeight - 1)
                            bottomValue = mazeValues[c, l + 1];
                        if (c > 0)
                            leftValue = mazeValues[c - 1, l];
                        if (c < mazeWidth - 1)
                            rightValue = mazeValues[c + 1, l];
    
                        var glass = mazeGlasses[c, l];
                        if (glass != null)
                        {
                            glass.LeftValue = leftValue;
                            glass.RightValue = rightValue;
                            glass.TopValue = topValue;
                            glass.BottomValue = bottomValue;
                        }
                    }
                }
            }
    

    Each block of the maze is made up of glass, hence the Glass UserControl. This user control has 9 sections, and the central sections of it can be filled or not, depending on wether there are neighboring blocks. We do it by assigning dependency properties to the user control:

        public partial class Glass : UserControl
        {
            #region DPs
            private static DependencyProperty LeftValueProperty = 
    		DependencyProperty.Register("Left", typeof(char), typeof(Glass), new PropertyMetadata(LeftValueChanged));
    
            public char LeftValue
            {
                get { return (char)this.GetValue(LeftValueProperty); }
                set {this.SetValue(LeftValueProperty, value);}
            }
    
            static void LeftValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var glass = (Glass)d;
                glass.rct01.Visibility = (char)e.NewValue == '1' ? Visibility.Visible : Visibility.Hidden;
            }
    
            private static DependencyProperty RightValueProperty = 
    		DependencyProperty.Register("Right", typeof(char), typeof(Glass), new PropertyMetadata(RightValueChanged));
    
            public char RightValue
            {
                get { return (char)this.GetValue(RightValueProperty); }
                set { this.SetValue(RightValueProperty, value); }
            }
    
            static void RightValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var glass = (Glass)d;
                glass.rct21.Visibility = (char)e.NewValue == '1' ? Visibility.Visible : Visibility.Hidden;
            }
    
            private static DependencyProperty TopValueProperty = 
    		DependencyProperty.Register("Top", typeof(char), typeof(Glass), new PropertyMetadata(TopValueChanged));
    
            public char TopValue
            {
                get { return (char)this.GetValue(TopValueProperty); }
                set { this.SetValue(TopValueProperty, value); }
            }
    
            static void TopValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var glass = (Glass)d;
                glass.rct10.Visibility = (char)e.NewValue == '1' ? Visibility.Visible : Visibility.Hidden;
            }
    
            private static DependencyProperty BottomValueProperty = 
    		DependencyProperty.Register("Bottom", typeof(char), typeof(Glass), new PropertyMetadata(BottomValueChanged));
    
            public char BottomValue
            {
                get { return (char)this.GetValue(BottomValueProperty); }
                set { this.SetValue(BottomValueProperty, value); }
            }
    
            static void BottomValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var glass = (Glass)d;
                glass.rct12.Visibility = (char)e.NewValue == '1' ? Visibility.Visible : Visibility.Hidden;
            }
    
            #endregion DPs
    

    Just to clarify a little bit how it works: if you have a block alone in the middle of the maze, it appears as just a little block on the screen. But if you put a block at its side, you won't see 2 blocks, but rather a single wall, extending from one block to the other. Depending on how the neighboring blocks, you can also have "L" or "T" or cross-shaped layouts.

    The Snail Character

    The snail is our hero, and, for some strange reason, the squids don't like him (sorry, I don't know sea animals behavior quite well). Also, it happens our hero must collect all pearls in the maze to complete each level.

    Most of the videogame heroes, like Mario and Sonic, are quite charismatic. Our Snail here is not different. It blinks and smiles, blinks and smiles. Also, when it dies, it fades in and out, and shows a screaming face. These emotions are intended to generate compassion on the player and identification with our hero.

            <Storyboard x:Name="sbBlink" x:Key="sbBlink" Duration="0:0:3" 
    		RepeatBehavior="Forever" AutoReverse="True" FillBehavior="HoldEnd">
                <DoubleAnimation Storyboard.TargetName="leftPupil" 
    			Storyboard.TargetProperty="Height" From="7" To="1" Duration="0:0:0.200" 
    			BeginTime="0:0:0.000" FillBehavior="HoldEnd"/>
                <DoubleAnimation Storyboard.TargetName="leftPupil" 
    			Storyboard.TargetProperty="Height" From="1" To="7" Duration="0:0:0.200" 
    			BeginTime="0:0:0.200" FillBehavior="HoldEnd"/>
                <DoubleAnimation Storyboard.TargetName="rightPupil" 
    			Storyboard.TargetProperty="Height" From="7" To="1" Duration="0:0:0.200" 
    			BeginTime="0:0:2.000" FillBehavior="HoldEnd"/>
                <DoubleAnimation Storyboard.TargetName="rightPupil" 
    			Storyboard.TargetProperty="Height" From="1" To="7" Duration="0:0:0.200" 
    			BeginTime="0:0:2.200" FillBehavior="HoldEnd"/>
            </Storyboard>
            <Storyboard x:Name="sbDie" x:Key="sbDie" Duration="0:0:3" FillBehavior="HoldEnd">
                <DoubleAnimation Storyboard.TargetName="grdMain" 
    			Storyboard.TargetProperty="Opacity" From="0" To="1" RepeatBehavior="3" 
    			Duration="0:0:0.200" BeginTime="0:0:0.000" FillBehavior="HoldEnd"/>
            </Storyboard>
            <Storyboard x:Name="sbBorn" x:Key="sbBorn" Duration="0:0:3" FillBehavior="HoldEnd">
                <DoubleAnimation Storyboard.TargetName="grdMain" 
    			Storyboard.TargetProperty="Opacity" From="1" To="1" RepeatBehavior="3" 
    			Duration="0:0:0.200" BeginTime="0:0:0.000" FillBehavior="HoldEnd"/>
            </Storyboard>
    
            public void Die(AnimationCompleted endAnimationCallback)
            {
                rotateEyeBrow1.Angle = -15;
                rotateEyeBrow2.Angle = 15;
                pthTeeth.Visibility =
                pthMouth.Visibility = System.Windows.Visibility.Hidden;
                pthMouth2.Visibility = System.Windows.Visibility.Visible;
                Storyboard sbDie = this.FindResource("sbDie") as Storyboard;
    
                sbDie.Completed += (s, e) =>
                    {
                        if (endAnimationCallback != null)
                            endAnimationCallback();
                    };
    
                IsDying = true;
                sbDie.Begin();
            }
    

    The Snail moves according with the player gestures (the arrow keys, as we are going to see later in the article). Each movement is done by a vertical or horizontal animation (depending on the movement direction), and the snail can move only one cell at a time. The movement is only started if the intended new position falls between the boundaries of the maze, and if it doesn't collide with the maze walls:

            private void ProcessNextAnimation(Queue<Point> queue, Storyboard sb)
            {
                if (queue.Count > 0)
                {
                    var deltaPoint = queue.Dequeue();
    
                    AnimateTopLeft(deltaPoint, this, sb);
                }
            }
    
            private void AnimateTopLeft(Point deltaPoint, FrameworkElement animal, Storyboard sb)
            {
                if (deltaPoint.X > 0)
                    SnailDirection = Controls.SnailDirection.Right;
                else if (deltaPoint.X < 0)
                    SnailDirection = Controls.SnailDirection.Left;
                if (deltaPoint.Y > 0)
                    SnailDirection = Controls.SnailDirection.Down;
                else if (deltaPoint.Y < 0)
                    SnailDirection = Controls.SnailDirection.Up;
    
                var left1 = (double)animal.GetValue(Canvas.LeftProperty);
                var top1 = (double)animal.GetValue(Canvas.TopProperty);
                var left2 = left1 + deltaPoint.X * cellWidth;
                var top2 = top1 + deltaPoint.Y * cellWidth;
    
    
    
                var newX = (int)(left2) / cellWidth;
                var newY = (int)(top2) / cellWidth;
    
                var ms = animationMs;
    
                var badMove = false;
    
                if (left2 < 0 || left2 > mazeWidth * cellWidth ||
                    top2 < 0 || top2 > mazeHeight * cellWidth)
                {
                    left2 = left1;
                    top2 = top1;
                    ms = 100;
                    badMove = true;
                }
                else if (MazeValues[newX, newY] == '1')
                {
                    left2 = left1;
                    top2 = top1;
                    ms = 100;
                    badMove = true;
                }
    
                var leftAnimation = new DoubleAnimation()
                {
                    From = left1,
                    To = left2,
                    Duration = TimeSpan.FromMilliseconds(ms),
                };
                var topAnimation = new DoubleAnimation()
                {
                    From = top1,
                    To = top2,
                    Duration = TimeSpan.FromMilliseconds(ms),
                };
                Storyboard.SetTarget(leftAnimation, animal);
                Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
                Storyboard.SetTarget(topAnimation, animal);
                Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
    
                sb.Children.Add(leftAnimation);
                sb.Children.Add(topAnimation);
    
                sb.Begin();
            }
    

    The Squid Characters

    Maybe this is the part that more resembles Pac Man: Just like the ghosts in the classic game, the squids are the bad guys (at least in this game, no offense intended to real squids) and their role is to chase our hero wherever he goes.

    Unlike our snail hero, the squids have fixed eyes and cold behavior (like villains such as Darth Vader and Jason Vorhees) and the only animations that apply to them are the eyes that follow their walking direction (just like Pac Man ghosts) and the tentacles movement.

    For the tentacles movement, I don't use traditional WPF animations. Instead, I switch between 2 possible tentacle configurations, applying different drawing data to the Path of the squids, in a timely fashion:

            #region events
            void timer_Tick(object sender, EventArgs e)
            {
                if (feetState == -1)
                {
                    pthBottom.Data = PathGeometry.Parse(@"
                    M0,0 
                    C00,0 05,20 10,10 
                    C10,10 15,0 20,10 
                    C20,10 25,20 30,10 
                    C30,10 35,0 40,10 
                    C40,10 45,20 50,0");
                }
                else
                {
                    pthBottom.Data = PathGeometry.Parse(@"
                    M0,0 
                    C00,00 05,20 10,10 
                    C10,10 15,00 20,10 
                    C20,10 25,20 30,10 
                    C30,10 35,00 40,10 
                    C40,10 45,20 50,10
                    C50,10 55,00 60,10
                    C60,10 65,20 70,0");
                }
    
                feetState *= -1;
            }
            #endregion events
    

    Game Control Keys

    The keyboard arrow keys move the snail, while the space bar starts the game and throws starfishes (this sounds a bit strange, but I'll explain this later).

    Each arrow key pressed generates a different DeltaPoint [deltaX, deltaY] that corresponds to moves measured in cell coordinates.

            private void Window_KeyDown(object sender, KeyEventArgs e)
            {
                var deltaX = 0;
                var deltaY = 0;
                var spacePressed = false;
                switch (e.Key)
                {
                    case Key.Right:
                        deltaX = 1;
                        break;
                    case Key.Left:
                        deltaX = -1;
                        break;
                    case Key.Up:
                        deltaY = -1;
                        break;
                    case Key.Down:
                        deltaY = 1;
                        break;
                    case Key.Space:
                        spacePressed = true;
                        break;
                }
    

    When the space bar is pressed in the beginning of the game, the game is started:

                if (spacePressed)
                {
                    if (splashScreen.Opacity == 1.0)
                    {
                        levelScreen.LevelNumber = level;
                        Storyboard sbStart = this.FindResource("sbStart") as Storyboard;
                        sbStart.Begin();
    
                        Storyboard sbLevel = this.FindResource("sbLevel") as Storyboard;
                        sbLevel.Begin();
    
                        midiHelper.StopAll();
                        midiHelper.Play("km_start",
                            () =>
                            {
                                movementHalted = false;
                                PlayStage1Music();
                            });
                    }
                    else
                    {
    

    Collecting Starfishes

    As stated before, the snail can use starfishes as shurikens (ninja stars) to kill the approaching squids. But these starfishes are only available as weapons after they are picked up. The snail can collect as many starfishes as possible.

    To collect a starfish, the snail must reach the cell the starfish is in. We know that a starfish has been collected when the snail rectangle intersects with the starfish rectangle:

    	if (starfish.Visibility == System.Windows.Visibility.Visible)
    	{
    		var rectStarfish = starfish.GetRect(cnvMain);
    		var starfishCellPoint = starfish.GetCellPoint();
    
    		if (rectSnail.IntersectsWith(rectStarfish))
    		{
    			irrKlangEngine.Play2D(@"Sounds\Reload.wav");
    
    			gotStarfish = true;
    
    			starfish.Visibility = System.Windows.Visibility.Hidden;
    
    			AddStarfish(starfish);
    			mazeValues[(int)starfishCellPoint.X, (int)starfishCellPoint.Y] = ' ';
    			break;
    		}
    

    Besides, there are also functions to control the starfish count and display the remaining number in the score panel:

            private void AddStarfish(Starfish starfish)
            {
                cnvMain.Children.Remove(starfish);
                collectedStarfishes.Push(starfish);
                txtStarfishes2.Text = string.Format("x{0}", collectedStarfishes.Count());
            }
    
            private Starfish RemoveStarfish()
            {
                var starfish = collectedStarfishes.Pop();
                txtStarfishes2.Text = string.Format("x{0}", collectedStarfishes.Count());
                cnvMain.Children.Add(starfish);
                return starfish;
            }
    

    Throwing Starfishes

    During the game play, the space bar is pressed to throw the starfishes (actually, our snail has ninja skills that allow him to handle starfishes like deadly shurikens). The starfish thrown follows the direction the snail is pointing to.

                        if (collectedStarfishes.Count() > 0)
                        {
                            var starfishPoint1 = snail.GetCellPoint();
    
                            var starfish = RemoveStarfish();
                            var xDirection = 0;
                            var yDirection = 0;
    
                            switch (snail.SnailDirection)
                            {
                                case SnailDirection.Right:
                                    xDirection = 1;
                                    yDirection = 0;
                                    break;
                                case SnailDirection.Left:
                                    xDirection = -1;
                                    yDirection = 0;
                                    break;
                                case SnailDirection.Down:
                                    xDirection = 0;
                                    yDirection = 1;
                                    break;
                                case SnailDirection.Up:
                                    xDirection = 0;
                                    yDirection = -1;
                                    break;
                            }
    

    Once thrown, the starfish can move up to 3 cells away from the snail. But like other things in the game, the starfish movement must respect the maze boundaries. We can ensure this restrictions by testing if there is no boundary or wall blocking the starfish way, step by step:

                            var targetX = (int)starfishPoint1.X;
                            var targetY = (int)starfishPoint1.Y;
                            var starfishPoint2 = new Point(targetX, targetY);
                            var length = 0;
                            for (var i = 1; i <= 3; i++)
                            {
                                targetX = (int)starfishPoint1.X + i * xDirection;
                                targetY = (int)starfishPoint1.Y + i * yDirection;
                                if (targetX >= 0 & targetX < mazeWidth &
                                    targetY >= 0 & targetY < mazeHeight)
                                {
                                    if (mazeValues[targetX, targetY] == '1')
                                    {
                                        break;
                                    }
                                    else
                                    {
                                        starfishPoint2 = new Point(targetX, targetY);
                                        length = i;
                                    }
                                }
                            }
    

    When the starfish is thrown, it makes an interesting boomerang sound. We make use of the IrrKlang framework (we'll talk more on this subjecat later).

    After some calculations, we define the starting point and ending point of the starfish, and the Throw on the Starfish class does the animation.

                            irrKlangEngine.Play2D(@"Sounds\boomerang.wav");
                            starfish.Throw(starfishPoint1, starfishPoint2, 
    						TimeSpan.FromMilliseconds((animationMs / 3.0) * length),
                                    () =>
                                    {
                                        var starfishPoint = starfish.GetCellPoint();
    
                                        foreach (var squid in squids)
                                        {
                                            var squidPoint = squid.GetCellPoint();
    
                                            var x1 = (starfishPoint1.X < starfishPoint2.X) 
    										? starfishPoint1.X : starfishPoint2.X;
                                            var x2 = (starfishPoint1.X < starfishPoint2.X) 
    										? starfishPoint2.X : starfishPoint1.X;
                                            var y1 = (starfishPoint1.Y < starfishPoint2.Y) 
    										? starfishPoint1.Y : starfishPoint2.Y;
                                            var y2 = (starfishPoint1.Y < starfishPoint2.Y) 
    										? starfishPoint2.Y : starfishPoint1.Y;
    
                                            if ((x1 <= squidPoint.X & squidPoint.X <= x2 
    										& squidPoint.Y == starfishPoint.Y) ||
                                                (y1 <= squidPoint.Y & squidPoint.Y <= y2 
    											& squidPoint.X == starfishPoint.X))
                                            {
                                                irrKlangEngine.Play2D(@"Sounds\bulle.wav", false);
                                                squid.Die(() =>
                                                {
                                                    squid.IsDying = true;
                                                    squid.Born(null);
                                                }
                                                );
    
                                                break;
                                            }
                                        }
                                    });
                        }
                    }
                }
                else
                {
                    if (!movementHalted)
                    {
                        snail.TryMoveXY(new Point(deltaX, deltaY));
                    }
                }
            }
    

    The Throw method, on the other side, set up the animation needed to make the starfish rotate and fly, according to the parameters provided:

            public void Throw(Point fromCellPoint, Point toCellPoint, TimeSpan duration, AnimationCompleted animationCompleted)
            {
                this.Visibility = System.Windows.Visibility.Visible;
                var leftAnimation = new DoubleAnimation()
                {
                    From = fromCellPoint.X * cellWidth + 15,
                    To = toCellPoint.X * cellWidth + 15,
                    Duration = duration,
                };
                var topAnimation = new DoubleAnimation()
                {
                    From = fromCellPoint.Y * cellWidth + 15,
                    To = toCellPoint.Y * cellWidth + 15,
                    Duration = duration,
                };
                Storyboard.SetTarget(leftAnimation, this);
                Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
                Storyboard.SetTarget(topAnimation, this);
                Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
    
                var sb = new Storyboard();
                sb.Children.Add(leftAnimation);
                sb.Children.Add(topAnimation);
                sb.Completed += (s, e) =>
                {
                    sbRotate.Stop();
                    IsMoving = false;
    
                    if (animationCompleted != null)
                        animationCompleted();
                };
    
                IsMoving = true;
                sbRotate.Begin();
                sb.Begin();
            }
    

    Killing Squids

    One good thing with videogames it that you can kill without remorse. The squids are trying to kill you anyway, so get all starfishes you can and shoot. The bubbles sound indicates the squid has been killed.

    Since there are WPF animations involved, we must calculate the initial and final points of the starfish, and find out if there are starfishes between this space. In this case, the squids found are killed.

                        foreach (var starfish in starfishes)
                        {
                            if (starfish.IsMoving)
                            {
                                var starfishPoint = starfish.GetCellPoint();
    
                                var x1 = (starfishPoint.X < snailPoint.X) ? starfishPoint.X : snailPoint.X;
                                var x2 = (starfishPoint.X > snailPoint.X) ? starfishPoint.X : snailPoint.X;
                                var y1 = (starfishPoint.Y < snailPoint.Y) ? starfishPoint.Y : snailPoint.Y;
                                var y2 = (starfishPoint.Y > snailPoint.Y) ? starfishPoint.Y : snailPoint.Y;
    
                                if ((x1 <= squidPoint.X & squidPoint.X <= x2 & starfishPoint.Y == squidPoint.Y) ||
                                    (y1 <= squidPoint.Y & squidPoint.Y <= y2 & starfishPoint.X == squidPoint.X))
                                {
                                    AddScore(10);
                                    irrKlangEngine.Play2D(@"Sounds\bulle.wav", false);
                                    squid.Die(() =>
                                    {
                                        squid.Born(() =>
                                            {
                                                squid.ResetAnimations();
                                                squid.IsDying = false;
                                            });
                                    }
                                    );
    
                                    break;
                                }
                            }
                        }
    

    Being Chased By Squids (Using A* Search Algorithm)

    I had a hard time trying to find out the algorithm needed to make the squids chase the snail. Most of the times, the squids got stuck, or, at best, they walked around looking bored and uninterested in the snail.

    But then I remembered our friend Sacha Barber once published a great article on A* Search algorith, dealing with finding the best path between any two stations of the London Underground.

    I wondered myself if the idea could be used in this game, and gave it a try. For my surprise, it worked like a charm. I only had to change the concepts: Sacha's article deals with a person trying to find the optimal path between the current station and the, respecting the geographical connections between those stations. On the other side, in Snail Quest the person is represented by the squid. The desired station is the cell where the snail is found, the stations are the empty cells inside the maze, and instead of connection between stations, we now have connections between empty cells, which are the "corridors" inside the maze.

            public List<MovementType> DoSearch(Point squidPoint, Point snailPoint)
            {
                pathsSolutionsFound = new List<List<Point>>();
                pathsAgenda = new List<List<Point>>();
    
                List<Point> pathStart = new List<Point>();
                pathStart.Add(squidPoint);
                pathsAgenda.Add(pathStart);
    
                while (pathsAgenda.Count() > 0)
                {
                    List<Point> currPath = pathsAgenda[0];
                    pathsAgenda.RemoveAt(0);
                    if (currPath.Count(
                        x => x.Equals(snailPoint)) > 0)
                    {
                        pathsSolutionsFound.Add(currPath);
                        break;
                    }
                    else
                    {
                        Point currPoint = currPath.Last();
                        List<Point> successorPoints =
                            GetSuccessorsForPoint(currPoint);
    
                        foreach (var successorPoint in successorPoints)
                        {
                            if (!currPath.Contains(successorPoint) &
                                pathsSolutionsFound.Count(x => x.Contains(successorPoint)) == 0)
                            {
                                List<Point> newPath = new List<Point>();
                                foreach (var station in currPath)
                                    newPath.Add(station);
    
                                newPath.Add(successorPoint);
                                pathsAgenda.Add(newPath);
                                //pathsAgenda.Sort();
                            }
                        }
                    }
                }
    
                //Finally, get the best Path, this should be the 1st one found due
                //to the heuristic evaluation performed by the search
                if (pathsSolutionsFound.Count() > 0)
                {
                    var solutionPath = pathsSolutionsFound[0];
    
                    var movementList = new List<MovementType>();
                    var point = solutionPath[0];
    
                    for (var i = 1; i < solutionPath.Count(); i++)
                    {
                        var movement = MovementType.None;
    
                        if (solutionPath[i].X > point.X)
                            movement = MovementType.Right;
                        if (solutionPath[i].X < point.X)
                            movement = MovementType.Left;
                        if (solutionPath[i].Y > point.Y)
                            movement = MovementType.Bottom;
                        if (solutionPath[i].Y < point.Y)
                            movement = MovementType.Top;
    
                        movementList.Add(movement);
    
                        point = solutionPath[i];
                    }
    
                    return movementList;
                }
                return null;
            }
    

    One point of attention: the squids don't just chase the squid all the time. They only go chasing when they have nothing else to do. But once they start chasing the squid, they walk all the path positions, and finally they find out where is the squid and go for the updated position.

    This behavior seems a little stupid, but I did it this way to make the game easier to complete. I'm sure there are better way to do this, and maybe I change this behavior later.

    The code below shows when the squids should look for a new path and when they shouldn't. The way to do this is to enqueue/dequeue from a variable named squidAnimationQueue which holds all the movements needed for the solution path (i.e. the one that leads the squid to the current position of the snail).

            public void ChaseSnail(Point snailPoint)
            {
                var squid = this;
    
                var xSquid = (int)((double)squid.GetValue(Canvas.LeftProperty) / cellWidth);
                var ySquid = (int)((double)squid.GetValue(Canvas.TopProperty) / cellWidth);
    
                var squidPoint = new Point(xSquid, ySquid);
    
                if (squidAnimationQueue.Count() == 0)
                {
                    var solutionPath = DoSearch(squidPoint, snailPoint);
    
                    if (solutionPath != null)
                    {
                        foreach (var movement in solutionPath)
                        {
                            var deltaX = 0;
                            var deltaY = 0;
    
                            switch (movement)
                            {
                                case MovementType.Right:
                                    deltaX = 1;
                                    break;
                                case MovementType.Left:
                                    deltaX = -1;
                                    break;
                                case MovementType.Bottom:
                                    deltaY = 1;
                                    break;
                                case MovementType.Top:
                                    deltaY = -1;
                                    break;
                            }
                            squidAnimationQueue.Enqueue(new Point(deltaX, deltaY));
                        }
                    }
                }
    
                MovementHalted = false;
    
            }
    

    Being Killed By Squids

    When the snail is killed, some actions are taken. First, the snail animation changes, so the snail face looks freightened. Then the whole body begins to blink. In the end, it appears again (that is, if there are at least one life left) at its original position.

            private void RemoveLive()
            {
                if (lives == 0)
                {
                    snail.Die(ResetPositions);
                    snail.ResetAnimations();
    
                    gameOverScreen.Text = "Game Over";
                    Storyboard sbGameOver = this.FindResource("sbGameOver") as Storyboard;
                    sbGameOver.Begin();
                    midiHelper.Play("km_gameover",
                        () =>
                        {
                            this.Dispatcher.Invoke((Action)delegate
                            {
                                LoadMaze(level);
                                AddLive();
                                AddLive();
                                AddLive();
                                Storyboard sbSplashScreen = this.FindResource("sbSplashScreen") as Storyboard;
                                sbSplashScreen.Begin();
                            });
                        });
                }
                else
                {
                    lives--;
                    txtLives2.Text = string.Format("x{0}", lives);
    
                    snail.Die(ResetPositions);
                    snail.ResetAnimations();
    
                    var osWaitBeforeReborn = Observable.Interval(TimeSpan.FromMilliseconds(3000)).Take(1);
                    osWaitBeforeReborn.Subscribe(e =>
                    {
                        PlayStage1Music();
    
                        movementHalted = false;
                    }
                    );
                }
            }
    

    Collecting Pearls

    To collect a pearl, the snail must reach the pearl position. We test this kind of collision by discovering if the snail's rectangle has intersected with the pearl's rectangle:

                foreach (var pearl in pearls)
                {
                    if (pearl.Visibility == System.Windows.Visibility.Visible)
                    {
                        var rectPearl = pearl.GetRect(cnvMain);
                        var pearlCellPoint = pearl.GetCellPoint();
    
                        if (rectSnail.IntersectsWith(rectPearl))
                        {
                            AddScore(100);
                            midiHelper.Play("km_crystal", null);
                            AddPearl(pearl);
                            pearl.Visibility = System.Windows.Visibility.Hidden;
                            mazeValues[(int)pearlCellPoint.X, (int)pearlCellPoint.Y] = ' ';
                            break;
                        }
                    }
                }
    

    Game Scoring

    Every time you kill a squid, collect a pearl or complete a level, your score on the screen is raised. This is done by a simple function:

    		private void AddScore(int points)
            {
                score += points;
    
                txtScore1.Text =
                txtScore2.Text = score.ToString("00000");
            }
    

    Moving On To The Next Level

    When all pearls in a given level have been collected, the game moves on to the next level. Each level has its own .txt file, and if this file doesn't exist the game ends with a different music.

    Notice that the midiHelper.Play method receives an anonimous method, which is executed only after the whole music has been played:

            private void GoNextLevel()
            {
                movementHalted = true;
                midiHelper.StopAll();
    
                var fileName = string.Format(@"Mazes\Level{0}.txt", level + 1);
    
                if (!File.Exists(fileName))
                {
                    midiHelper.StopAll();
                    gameOverScreen.Text = "Congratulations!";
                    Storyboard sbGameOver = this.FindResource("sbGameOver") as Storyboard;
                    sbGameOver.Begin();
                    midiHelper.Play("km_ending",
                        () =>
                        {
                            this.Dispatcher.Invoke((Action)delegate
                            {
                                Storyboard sbSplashScreen = 
    							this.FindResource("sbSplashScreen") as Storyboard;
                                sbSplashScreen.Begin();
                            });
                        });
                }
                else
                {
                    irrKlangEngine.Play2D(@"Sounds\bjp.wav", false);
                    AddScore(200);
                    var osWaitBeforeNextLevel = 
    				Observable.Interval(TimeSpan.FromMilliseconds(3000)).Take(1);
                    osWaitBeforeNextLevel.Subscribe(e =>
                    {
                        this.Dispatcher.Invoke((Action)delegate
                        {
                            level++;
                            LoadMaze(level);
    
                            ResetPositions();
                            levelScreen.LevelNumber = level;
                            Storyboard sbLevel = this.FindResource("sbLevel") as Storyboard;
                            sbLevel.Begin();
    
                            midiHelper.Play("km_start",
                            () =>
                            {
                                movementHalted = false;
                                PlayStage2Music();
                            });
                        });
                    });
                }
            }
    

    Playing Game Music With C# Midi Toolkit

    Leslie Sanford's C# Midi Toolkit is a great article contribution here in The Code Project. If you have time to dig into his code, you will find out that Leslie's work is really awesome. I use it in Snail Quest solution as compiled dll's, so if you are interested in C# Midi Toolkit code, please download it from Leslie's article.

    As you have seen, the game use midi music in some situations: the game opening, at the start of the levels, at game over event, and at the end of the game.

    There are situations when we have to use overlapping midi executions. If you are using C# Midi toolkit, usually you have only one sequencer. It turns out that when you are playing a midi file with one sequencer, you can't play another midi at the same time. To overcome this problem, I creted a MidiHelper that can hold a dicionary of Sequencer objects, which in turn can be played concurrently.

        public class MidiHelper
        {
            private int outDeviceID = 0;
            private OutputDevice outDevice;
            private Dictionary<string, Sequence> dicSequence = new Dictionary<string,Sequence>();
            private Dictionary<string, Sequencer> dicSequencer = new Dictionary<string,Sequencer>();
            private Dictionary<string, NoArgDelegate> dicPlayingCompleteDelegate = new Dictionary<string, NoArgDelegate>();
            private Dictionary<string, int> dicSequencerMessageCount = new Dictionary<string, int>();
            private Dictionary<string, bool> dicSequencerInitialized = new Dictionary<string, bool>();
    
            private bool playing = false;
            private bool closing = false;
            public delegate void NoArgDelegate();
            NoArgDelegate loadCompleted;
            NoArgDelegate playingCompleted;
    
            #region ctor
            public MidiHelper()
            {
                if (outDevice == null)
                    outDevice = new OutputDevice(outDeviceID);
            }
            #endregion ctor
    
            #region methods
    
            public void InitializeSequencer(string midiKey)
            {
                var sequence = dicSequence[midiKey];
                var sequencer = dicSequencer[midiKey];
    
                sequencer.Stop();
                playing = false;
    
                sequence.Format = 1;
                sequencer.Position = 0;
                sequencer.Sequence = sequence;
                sequencer.ChannelMessagePlayed += 
    			new System.EventHandler<Sanford.Multimedia.Midi.ChannelMessageEventArgs>(this.HandleChannelMessagePlayed);
                sequencer.Stopped += 
    			new System.EventHandler<Sanford.Multimedia.Midi.StoppedEventArgs>(this.HandleStopped);
                sequencer.SysExMessagePlayed += 
    			new System.EventHandler<Sanford.Multimedia.Midi.SysExMessageEventArgs>(this.HandleSysExMessagePlayed);
                sequencer.Chased += 
    			new System.EventHandler<Sanford.Multimedia.Midi.ChasedEventArgs>(this.HandleChased);
                sequence.LoadCompleted += HandleLoadCompleted;
            }
    
            public void Load(string midiKey, string midiFile)
            {
                dicSequence.Add(midiKey, new Sequence());
                dicSequencer.Add(midiKey, new Sequencer(midiKey));
                dicPlayingCompleteDelegate.Add(midiKey, null);
                dicSequencerMessageCount.Add(midiKey, 0);
                dicSequencerInitialized.Add(midiKey, false);
    
                InitializeSequencer(midiKey);
    
                dicSequencer[midiKey].Stop();
    
                dicSequencer[midiKey].ChannelMessagePlayed += (s, e) =>
                {
                    dicSequencerMessageCount[midiKey]++;
                };
    
                dicSequencer[midiKey].PlayingCompleted += (s, e) =>
                {
                    if (dicSequencerMessageCount[midiKey] > 0)
                    {
                        var playingCompleted = dicPlayingCompleteDelegate[midiKey];
    
                        if (playingCompleted != null)
                            playingCompleted();
                        dicSequencer[midiKey].Stop();
                        dicSequencerMessageCount[midiKey] = 0;
                    }
                };
    
                playing = false;
                dicSequence[midiKey].LoadAsync(midiFile);
            }
    
            public void Play(string midiKey, NoArgDelegate playingCompleted)
            {
                playing = true;
    
                dicPlayingCompleteDelegate[midiKey] = playingCompleted;
    
                if (!dicSequencerInitialized[midiKey])
                {
                    dicSequencerInitialized[midiKey] = true;
                    dicSequencer[midiKey].GetTracks();
                }
                dicSequencer[midiKey].Stop();
                dicSequencer[midiKey].Start();
            }
    
            public void Continue(string midiKey)
            {
                playing = true;
                dicSequencer[midiKey].Continue();
            }
    
            public void Stop(string midiKey)
            {
                playing = false;
                dicSequencer[midiKey].Stop();
            }
    
            public void StopAll()
            {
                foreach (var kv in dicSequencer)
                {
                    kv.Value.Stop();
                }
            }
    
            #endregion methods
    
            #region events
            private void HandleChannelMessagePlayed(object sender, ChannelMessageEventArgs e)
            {
                if (closing)
                {
                    return;
                }
    
                outDevice.Send(e.Message);
            }
    
            private void HandleChased(object sender, ChasedEventArgs e)
            {
                foreach (ChannelMessage message in e.Messages)
                {
                    outDevice.Send(message);
                }
            }
    
            private void HandleSysExMessagePlayed(object sender, SysExMessageEventArgs e)
            {
                outDevice.Send(e.Message); //Sometimes causes an exception to be thrown because the output device is overloaded.
            }
    
            private void HandleStopped(object sender, StoppedEventArgs e)
            {
                foreach (ChannelMessage message in e.Messages)
                {
                    outDevice.Send(message);
                }
            }
    
            private void HandleLoadCompleted(object sender, AsyncCompletedEventArgs e)
            {
                if (loadCompleted != null)
                    loadCompleted();
            }
    
            #endregion events
        }
    

    Playing Sound Effects With IrrKlang Engine

    IrrKlang is an awesome cross platform audio library, very easy to use and it's free to use for non-commercial purposes. For commercial applications, you should purchase a licence.

    But fortunately (for you) I'm not going to make money with Snail Quest, so I can share it with Code Project readers.

    You might be wondering why I bothered to use 2 sound engines (C# Midi Toolkit and IrrKlang) in the game. Of course .mp3 and .wav files have better quality than midi files. But there's a size payoff in using .mp3 or .wav files, because 2 minutes of music could mean some megabytes, while a complex midi music can be stored in much less space. Anyway, I find it a good coding exercise, and also it's more convenient for the Code Project readers, since they have to download fewer megabytes.

    It's really easy to use. See below how many lines of code I needed to play a sound:

            IrrKlang.ISoundEngine irrKlangEngine;
            IrrKlang.ISound currentlyPlayingSound;
    ...
    		irrKlangEngine = new IrrKlang.ISoundEngine();
    ...
    		irrKlangEngine.Play2D(@"Sounds\boomerang.wav");
    

    Final Considerations

    That's it. I had a lot of fun in writing the code and the article, and hope you like it. If you liked the article, please leave a comment below. And if you don't like it, leave a comment too. Your feedback is very important for me.

    History

    • 2011-03-29: Initial version.
    • 2011-04-05: Minor corrections.
  • License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

    Share

    About the Author

    Marcelo Ricardo de Oliveira
    Software Developer
    Brazil Brazil
    Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
     
    He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
     
    Published Windows Phone apps:
     
     
    Awards:
     
    CodeProject MVP 2012
    CodeProject MVP 2011
     
    Best Web Dev article of March 2013
    Best Web Dev article of August 2012
    Best Web Dev article of May 2012
    Best Mobile article of January 2012
    Best Mobile article of December 2011
    Best Mobile article of October 2011
    Best Web Dev article of September 2011
    Best Web Dev article of August 2011
    HTML5 / CSS3 Competition - Second Prize
    Best ASP.NET article of June 2011
    Best ASP.NET article of May 2011
    Best ASP.NET article of April 2011
    Best C# article of November 2010
    Best overall article of November 2010
    Best C# article of October 2010
    Best C# article of September 2010
    Best overall article of September 2010
    Best overall article of February 2010
    Best C# article of November 2009

    Comments and Discussions

     
    GeneralMy vote of 5 PinmvpMika Wendelius20-Oct-12 1:46 
    QuestionGood job man! [modified] Pinmemberkartalyildirim4-Mar-12 1:39 
    GeneralMy vote of 5 Pinmemberpaulot213-Jul-11 21:47 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira8-Jul-11 15:26 
    GeneralMy vote of 5 PinmemberHimanshuJoshi19-Apr-11 15:37 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira20-Apr-11 12:26 
    GeneralMy vote of 5 PinmvpSandeep Mewara16-Apr-11 20:34 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira17-Apr-11 7:42 
    GeneralMy vote of 5 Pinmemberezekjh14-Apr-11 10:52 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira14-Apr-11 15:47 
    GeneralMy vote of 5 PinmemberDr.Luiji11-Apr-11 1:15 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira11-Apr-11 3:13 
    GeneralMy vote of 5 PinmvpAbhinav S9-Apr-11 5:03 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira9-Apr-11 10:47 
    GeneralMy vote of 5 PinmemberAlain Rist8-Apr-11 21:37 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira9-Apr-11 10:47 
    GeneralMy vote of 5 PinmemberSunasara Imdadhusen8-Apr-11 20:41 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira9-Apr-11 10:47 
    GeneralExcellent article / Artigo Excelente Pinmembersasusk37-Apr-11 19:41 
    GeneralRe: Excellent article / Artigo Excelente PinmvpMarcelo Ricardo de Oliveira8-Apr-11 2:27 
    GeneralMy vote of 5 PinmemberDr. Jones DK7-Apr-11 10:31 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira7-Apr-11 10:42 
    GeneralVery nice PinmemberCadu Sa Motta7-Apr-11 7:24 
    GeneralRe: Very nice PinmvpMarcelo Ricardo de Oliveira7-Apr-11 7:40 
    GeneralPhenomenal! My 5! PinmvpNishant Sivakumar6-Apr-11 13:02 
    GeneralRe: Phenomenal! My 5! PinmvpMarcelo Ricardo de Oliveira6-Apr-11 13:32 
    GeneralWOW PinmemberPetr Pechovic5-Apr-11 2:25 
    GeneralRe: WOW PinmvpMarcelo Ricardo de Oliveira5-Apr-11 5:08 
    GeneralMy vote of 5 PinmemberHalil ibrahim Kalkan4-Apr-11 21:33 
    GeneralRe: My vote of 5 PinmvpSacha Barber4-Apr-11 22:39 
    GeneralRe: My vote of 5 PinmemberHalil ibrahim Kalkan5-Apr-11 1:38 
    GeneralRe: My vote of 5 PinmvpSacha Barber5-Apr-11 1:39 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira5-Apr-11 5:08 
    GeneralMy vote of 5 PinmemberIlka Guigova4-Apr-11 18:39 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira5-Apr-11 5:07 
    GeneralMy vote of 5 PinmemberJF20153-Apr-11 23:42 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira4-Apr-11 4:01 
    GeneralBeautiful! PinmemberMark McE2-Apr-11 17:27 
    GeneralRe: Beautiful! PinmvpMarcelo Ricardo de Oliveira3-Apr-11 15:35 
    GeneralMy vote of 5 Pinmembervankaandreev2-Apr-11 5:41 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira2-Apr-11 7:27 
    GeneralMy vote of 5 PinmvpBrij31-Mar-11 4:38 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira31-Mar-11 5:02 
    GeneralMy vote of 5 Pinmvpdefwebserver31-Mar-11 3:40 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira31-Mar-11 3:53 
    GeneralRe: My vote of 5 Pinmvpdefwebserver6-Apr-11 13:22 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira6-Apr-11 13:30 
    GeneralRe: My vote of 5 Pinmvpdefwebserver6-Apr-11 13:45 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira6-Apr-11 14:04 
    GeneralMy vote of 5 PinmemberBigWCat30-Mar-11 7:28 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    | Advertise | Privacy | Terms of Use | Mobile
    Web01 | 2.8.1411023.1 | Last Updated 7 Apr 2011
    Article Copyright 2011 by Marcelo Ricardo de Oliveira
    Everything else Copyright © CodeProject, 1999-2014
    Layout: fixed | fluid