Quantcast
Channel: Category Name
Viewing all articles
Browse latest Browse all 10804

How to make a Windows Store game with C# and XAML, part 3

$
0
0

This guest blog post is the final post of a 3-part series written by Windows developer Jani Nevalainen. Please see the original 3rd post on Jani’s blog here, and refer to Part 1 here and Part 2 here

Moving the ship
When we’re doing a universal app, we have to take in account that there are several possible ways for users to control the game. Phones and tablets have touch screens, but on desktop most of us still prefer to use a keyboard to play games.

For doing the movements, we need to be aware of the screen dimensions. Add these member variables to App.xaml.cs class:

public static double ScreenWidth { get; set; }
public static double ScreenHeight { get; set; }
public static int Highscore { get; set; }

We set these values in StartPage.xaml.cs, at the end of the constructor, after the Loaded -lambda:

#if WINDOWS_PHONE_APP
            App.ScreenWidth = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Bounds.Width;
            App.ScreenHeight = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Bounds.Height;
#endif
#if WINDOWS_APP
            App.ScreenWidth = Window.Current.Bounds.Width;
            App.ScreenHeight = Window.Current.Bounds.Height;
#endif

Here we used #if statements to run different code depending on the platform we’re running, the code will be compiled to include just the platform specific code for each build for Phone and Windows.

Now we open the GamePage.xaml.cs and add the following member variables to keep track of the ship position:

private double shipPosition;
private readonly double shipHorizontalPosition = App.ScreenHeight - 50;

Now we’re ready to create the actual method to move the ship. We add first this to the top of the GamePage.xaml.cs:

using Windows.System;

And then this to the end of the GamePage.xaml.cs:

private void MoveShip(int amount)
{
    shipPosition += amount;
    // Let's make sure that the ship stays in the screen
    if (shipPosition > LayoutRoot.ActualWidth - 30)
    {
        shipPosition = LayoutRoot.ActualWidth - 30;
    }
    else if (shipPosition < 0)
    {
        shipPosition = 0;
    }
    Rocket.Margin = new Thickness(shipPosition, shipHorizontalPosition, 0, 0);
}

The method moves the ship the amount specified in the calling function (in pixels), and checks that it doesn’t go over the boundaries from left or right.

Next we’ll capture the keyboard in case the user is playing with a desktop machine. Let’s create the event handler for key events. Open GamePage.xaml.cs constructor, and add the following after InitializeComponent():

Loaded += (sender, args) =>
{
    // Resize move controls to fit the area
    LeftCanvas.Width = LeftCanvas.Height = (LeftArea.ActualWidth / 2) - 10;
    RightCanvas.Width = RightCanvas.Height = (LeftArea.ActualWidth / 2) - 10;

    // Position the ship to the bottom center of the screen
    shipPosition = LayoutRoot.ActualWidth / 2;
    Rocket.Margin = new Thickness(shipPosition, shipHorizontalPosition, 0, 0);
 
    Window.Current.CoreWindow.KeyDown += CoreWindow_KeyDown;
};

Next we add the actual code to handle the key presses by adding the following method:

private void CoreWindow_KeyDown(Windows.UI.Core.CoreWindow sender, Windows.UI.Core.KeyEventArgs args)
{
    switch (args.VirtualKey)
    {
        case VirtualKey.Left:
            MoveShip(-5);
            break;
        case VirtualKey.Right:
            MoveShip(5);
            break;
        case VirtualKey.Space:
            OnFire(null, null);
            break;
        default:
            break;
    }
}

Star field background
We had one type of particle engine on the start screen, but I think we need something of a more traditional kind to create sense of moving for the ship. First we need to add one using –statement to the top of the file:

using Windows.UI;

Then we need to add some more member variables for the starfield:

private const int StarCount = 200
private List stars = new List(StarCount);
private Random randomizer = new Random();

Here’s altered versions of CreateStar and MoveStars methods, which we add to the GamePage.xaml.cs:

void MoveStars(object sender, object e)
{
    if (stars.Count < StarCount)
    {
        CreateStar();
    }

    foreach (Dot star in stars)
    {
        Canvas.SetLeft(star.Shape, Canvas.GetLeft(star.Shape) + star.Velocity.X);
        Canvas.SetTop(star.Shape, Canvas.GetTop(star.Shape) + star.Velocity.Y);

        if (Canvas.GetTop(star.Shape) > LayoutRoot.ActualHeight)
        {
            int left = randomizer.Next(0, (int)LayoutRoot.ActualWidth);
            Canvas.SetLeft(star.Shape, left);
            Canvas.SetTop(star.Shape, 0);
        }
    }
    Move.Begin();
}
private void CreateStar()
{
    var star = new Dot()
    {
        Shape = new Ellipse() { Height = 2, Width = 2 },
        Velocity = new Point(0, randomizer.Next(1, 5))
    };

    int left = randomizer.Next(0, (int)LayoutRoot.ActualWidth);
    Canvas.SetLeft(star.Shape, left);
    Canvas.SetTop(star.Shape, 0);
    Canvas.SetZIndex(star.Shape, 1);
            
    // Set color
    byte c = (byte)randomizer.Next(10,255);
    star.Shape.Fill = new SolidColorBrush(Color.FromArgb(c, c, c, c));

    stars.Add(star);
    LayoutRoot.Children.Add(star.Shape);
}

Now we go to the constructor of the same class, and add inside the end of the Loaded lambda the following code:

// Starfield background
CreateStar();
Move.Completed += MoveStars;
Move.Begin();

Now if you run the game, you see how stars are falling in different speeds from the top of the screen, creating sense of depth and speed.

Adding enemies
What would a shoot’em up be without any enemies to shoot? Next we’ll add some enemies to the screen to get some action to the screen.

Let’s create a new UserControl, and call it Bobo by right clicking the Shared project, and Add, New Item, User Control. You could give it some nice bitmap images, but for this exercise I’m using XAML to draw the creature. Open Bobo.xaml and copy/paste the following on top the XAML:

Open the Bobo.xaml.cs and add the following member variables to the class:

public int AreaWidth { get; set; }
public Point Location { get; set; }
public bool Dead { get; set; }
public int Worth { get; set; } // amount of score for kill
public int Type; // 1 - green, 2 - blue, 3 - mega
private readonly Random randomizer = new Random();
public double Velocity;
private int direction;
private int directionCount = 0; // don't change direction on every loop

Add also the velocity and type randomizer to the constructor, just after the InitializeComponent –call:

Velocity = randomizer.Next(1, 3);
Type = randomizer.Next(1, 4);
if (Type == 3)
{
    Velocity = 4;
}
SetType();

Now we have three different types of enemies, and one of them moves faster than the others. For the enemy to move on the screen, we’ll add the Move method for it:

public void Move()
 {
     int move;
     // Randomize the move direction
     if (directionCount == 0)
     {
         direction = randomizer.Next(1, 3);
     }
     if (direction == 1)
     {
         move = -1;
     }
     else
     {
         move = 1;
     }
     directionCount++;
     // Change direction every 30 count
     if (directionCount > 30)
     {
         directionCount = 0;
     }
     // Check that the bobo doesn't go through the game area walls
     if (Location.X + direction < 0)
     {
         move = 0;
     }
     if (Location.X + direction > AreaWidth)
     {
         move = AreaWidth;
     }
     // Set the new location
     Location = new Point(Location.X + move, Location.Y + Velocity);
 }

We need to add also method to set the color according to the type of the enemy and give them unique kill score. Add the following method to Bobo.xaml.cs:

private void SetType()
{
    switch (Type)
    {
        case 1:
            SetFill(Color.FromArgb(0xFF, 0x00, 0xA2, 0x07), Color.FromArgb(0xFF, 0x3A, 0xFF, 0x00));
            Worth = 10;
            break;
        case 2:
            SetFill(Color.FromArgb(0xFF, 0x00, 0x00, 0xa0), Color.FromArgb(0xFF, 0x00, 0x0F, 0xff));
            Worth = 20;
            break;
        case 3:
            SetFill(Color.FromArgb(0xFF, 0xaf, 0x00, 0x00), Color.FromArgb(0xFF, 0xff, 0x0F, 0x00));
            Worth = 50;
            break;
    }
}

private void SetFill(Color start, Color end)
{
    var startGradient = new GradientStop();
    var endGradient = new GradientStop();
    startGradient.Color = start;
    startGradient.Offset = 1;
    endGradient.Color = end;
    var collection = new GradientStopCollection();
    collection.Add(startGradient);
    collection.Add(endGradient);

    InsideEllipse.Fill = new LinearGradientBrush(collection, 0);
}

Now we have a nasty looking foe for our ship to shoot at. Next we open the GamePage.xaml.cs and add again some prerequisites for our swarm of enemies. Add the following member variables:

private List enemies = new List();
private int maxEnemies = 20;
private DispatcherTimer timer = new DispatcherTimer();
private int Level { get; set; } // Player level 
private int Score { get; set; } // Game score

You can adjust the game difficulty by increasing maxEnemies at a later level of the game for example.

We have everything set for our enemies to appear on the screen. We need to just add them to the GamePage.xaml.cs. First edit the Loaded–lambda on constructor to include the following at the bottom of it:

timer.Tick += TimerOnTick;
timer.Interval = new TimeSpan(0, 0, 0, 2);
timer.Start();

And then the Tick method to call:

/// 
/// Create a new enemy if not max amount on the screen already
/// 
/// 
/// 
private void TimerOnTick(object sender, object o)
{
    if (enemies.Count < maxEnemies)
    {
        var enemy = new Bobo
        {
            AreaWidth = (int)LayoutRoot.ActualWidth,
            Location = new Point(randomizer.Next(0, (int)LayoutRoot.ActualWidth - 80), 0)
        };
        if (enemy.Type == 3)
        {
            // Make the red enemy smaller and more difficult to hit
            var scaleTransform = new ScaleTransform();
            scaleTransform.ScaleX = scaleTransform.ScaleX * 0.50;
            scaleTransform.ScaleY = scaleTransform.ScaleY * 0.50;
            enemy.RenderTransform = scaleTransform;
            enemy.Width = 30;
            enemy.Height = 30;
        }
        enemy.Velocity = enemy.Velocity * ((Level / (double)10) + 1);
        enemies.Add(enemy);
        Canvas.SetZIndex(enemy, 7);
        LayoutRoot.Children.Add(enemy);
    }
}

All this code will create different types of monsters to the screen, but they’re still sitting static on the top. That’s kind of boring, so let’s add the long awaited game loop to move them:

private void GameLoop(object sender, object e)
{
    if (goingRight)
        MoveShip(5);
    if (goingLeft)
        MoveShip(-5);
    // TODO - collision test

    // TODO - move bullets

    // Move enemies
    for (int i = 0; i < enemies.Count; i++)
    {
        if (enemies[i].Dead == false)
        {
            enemies[i].Move();
            enemies[i].Margin = new Thickness(enemies[i].Location.X, enemies[i].Location.Y, 0, 0);
        }
        if (enemies[i].Margin.Top > App.ScreenHeight || enemies[i].Dead)
        {
            LayoutRoot.Children.Remove(enemies[i]);
            enemies.Remove(enemies[i]);
        }
    }
}

Now we just have to make sure our game loop gets called, so we open the constructor, and add as very last line, after the Loading–lambda has closed, the following line:

CompositionTarget.Rendering += GameLoop;

Go ahead, try the project now, and you should see the star field moving, ship should respond to the keyboard and enemies should move randomly from top to down.

Open fire!
Now our little game seems to be a bit boring, enemies come and sail through your ship and you can’t shoot. We need to add collision detection and possibility to shoot the enemies to make it a bit more interesting. Let’s start with shooting! Add the following member variables to GamePage.xaml.cs:

private List bullets = new List(); // Bullets on the screen
private bool gameRunning = true; // Did we die already

and for ellipses we need to add also this using:

using Windows.UI.Xaml.Shapes;

Now we edit the OnFire–method we added on the second part of the tutorial. Add the following code to it:

if (gameRunning)
{
    var bullet = new Ellipse
    {
        Width = 5, Height = 5, Fill = new SolidColorBrush(Colors.Red)
    };
    bullet.Margin = new Thickness(shipPosition + (Rocket.Width/2) - (bullet.Width/2),
        shipHorizontalPosition + 2, 0, 0);
    LayoutRoot.Children.Add(bullet);
    bullets.Add(bullet);
}

The code above checks that the player is still alive, and if yes, creates a new ellipse as a bullet and adds it to the bullet list, so we can later easily check if any of the bullets hit anything on the screen. At this point we have static bullets in the screen, so logical step is to add moving functionality to the game by adding this:

private void MoveBullet(Ellipse ellipse)
{
    if ((ellipse.Margin.Top - 10) > 0)
    {
        ellipse.Margin = new Thickness(ellipse.Margin.Left, ellipse.Margin.Top - 10, 0, 0);
        HitTest(ellipse);
    }
    else
    {
        bullets.Remove(ellipse);
        LayoutRoot.Children.Remove(ellipse);
    }
}

The code above moves the bullet 10 pixels upwards until it goes off the screen. At that point it is removed from the bullet list. Let’s add the HitTest method to see if our bullets actually hit anything:

private void HitTest(Ellipse ellipse)
{
    for (int i = 0; i < enemies.Count; i++)
    {
        var enemyInFire = new Rect(enemies[i].Location.X, enemies[i].Location.Y, enemies[i].ActualWidth, enemies[i].ActualHeight);
        if (enemyInFire.Contains(new Point(ellipse.Margin.Left, ellipse.Margin.Top)))
        {
            Score += enemies[i].Worth;
            ScoreBoard.Text = Score.ToString();
            if (Score > App.Highscore)
            {
                App.Highscore = Score;
                HighscoreBoard.Text = Score.ToString();
            }
            LayoutRoot.Children.Remove(ellipse);
            bullets.Remove(ellipse);
            enemies[i].Dead = true;
            return;
        }
    }
}

To move the bullets, add this to the GameLoop -method, over the // TODO – move bullets text:

for (int i = 0; i < bullets.Count; i++)
{
    MoveBullet(bullets[i]);
}

At this point, you’re ready to shoot some aliens from outer space! But it kind of gets boring as there’s no way you can die yet, as we’re not testing if the aliens hit you. Let’s add few more things and it starts to come together. First we need the famous game over notification. Add this member variable to GamePage.xaml.cs:

private TextBlock GameOver = new TextBlock();

After that we need a crash test method:

private void CrashTest()
{
    for (int i = 0; i < enemies.Count; i++)
    {
        var enemyCreature = new Rect(enemies[i].Location.X, enemies[i].Location.Y, enemies[i].ActualWidth, enemies[i].ActualHeight);
        enemyCreature.Intersect(new Rect(Rocket.Margin.Left, Rocket.Margin.Top, Rocket.ActualWidth,
            Rocket.Margin.Top));
        if (!enemyCreature.IsEmpty)
        {
            CompositionTarget.Rendering -= GameLoop;
            Move.Completed -= MoveStars;
            GameOver.Text = "Game Over!";
            GameOver.FontSize = 48;
            GameOver.VerticalAlignment = VerticalAlignment.Center;
            GameOver.HorizontalAlignment = HorizontalAlignment.Center;
            Grid.SetColumn(GameOver, 1);
            MainGrid.Children.Add(GameOver);
            gameRunning = false;
            if (App.Highscore < Score)
            {
                App.Highscore = Score;
            }
        }
    }
}

Finally we need to add to the GameLoop call to the CrashTest, overwriting the // TODO – collision test with call to our method: CrashTest();

If you try the game, you soon realize that if the enemies crash to your ship, that’s the end of you! This is the end of part three of Universal Games for Windows. On the next post we’ll continue to improve the game by adding universal high score system, levels, navigation and other relevant things to finish up the game.

Download the solution so far from here.


Viewing all articles
Browse latest Browse all 10804

Trending Articles