You can use Universal Windows Platform (UWP) and its Canvas as your game stage, where you can easily draw, move and animate your game sprites. This works only for 2D scenarios with limited number of objects, so if you’re planning on rendering thousands of objects and flying particles, I suggest you consider using some real game engine. But if your scenario is simple enough, this approach has no extra licenses or dependencies and you have the full power of the .NET Framework UWP stack behind you.

Convinced? Good. Let’s start by creating a new Blank App (C#/Windows Universal) project with the Visual Studio. Then add a new GameTimer class for our game timer, so we can set the pace for our sprite animations. Using CompositionTarget.Rendering event isn’t optimal, but it’s easy to use and more than enough for our simple game.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

using System ;

using System.Diagnostics ;

using Windows.UI.Xaml.Media ;



namespace GameApp

{

public class GameTimer

{

private static TimeSpan updateInterval = TimeSpan . FromSeconds ( 1.0 / 24.0 ) ; // FPS 24

private Stopwatch stopwatch = Stopwatch . StartNew ( ) ;



public GameTimer ( )

{

CompositionTarget . Rendering += CompositionTarget_Rendering ;

}



public event EventHandler < EventArgs > Draw ;



private void CompositionTarget_Rendering ( object sender, object e )

{

if ( Draw != null )

{

// Draw after interval reached, otherwise wait for the next round.

if ( this . stopwatch . Elapsed >= updateInterval )

{

this . stopwatch . Reset ( ) ;

this . stopwatch . Start ( ) ;



Draw ( this , EventArgs . Empty ) ;

}

}

}

}

}

After our timer is set up, we can continue by creating a new UserControl called Pawn, containing Rectangle object for our sprite. This class will represent our moving game object on a Canvas. I’m using my own 96 x 128 size sprite sheet (one frame is 32 x 32 pixels), which you can download here and add to your Visual Studio project.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

< UserControl

x : Class = "GameApp.Pawn"

xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns : x = "http://schemas.microsoft.com/winfx/2006/xaml"

xmlns : local = "using:GameApp"

xmlns : d = "http://schemas.microsoft.com/expression/blend/2008"

xmlns : mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"

mc : Ignorable = "d"

d : DesignHeight = "32"

d : DesignWidth = "32" >



< Rectangle x : Name = "Rectangle" Width = "32" Height = "32" >

< Rectangle . Fill >

< ImageBrush ImageSource = "Pawn.png" Stretch = "None" AlignmentX = "Left" AlignmentY = "Top" >

< ImageBrush . Transform >

< TranslateTransform x : Name = "TranslateTransform" />

</ ImageBrush . Transform >

</ ImageBrush >

</ Rectangle . Fill >

</ Rectangle >



</ UserControl >

Then, in Pawn.xaml.cs code-behind, the FramePhase enumeration handles the sprite sheet columns and the FrameDirection points to the sprite sheet row. SetCanvasLocation method will handle the object repositioning and settings the Z-index (lower objects should be in front of higher objects). The Z-index is divided in 40 pixel rows and every other index is reserved for objects moving between the rows. The actual magic is done in the Timer_Draw event handler, where the next phase, direction and X + Y are calculated until we reach the specified destination. The Pawn can then be moved by using the asynchronous MoveAsync method and specifying the destination point.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

using System ;

using System.Threading.Tasks ;

using Windows.Foundation ;

using Windows.UI.Xaml.Controls ;



namespace GameApp

{

public enum FramePhase

{

Left,

Center,

Right

}



public enum FrameDirection

{

Down,

Left,

Right,

Up

}



public sealed partial class Pawn : UserControl

{

private GameTimer timer ;

private TaskCompletionSource < bool > source ;

private FramePhase activeFoot = FramePhase . Left ;

private FramePhase phase = FramePhase . Center ;

private FrameDirection direction = FrameDirection . Down ;

private Point destination ;



public bool IsSelected

{

get ;

set ;

}



public Pawn ( )

{

this . InitializeComponent ( ) ;

}



public Pawn ( GameTimer timer )

: this ( )

{

this . timer = timer ;

}



public void SetCanvasLocation ( double x, double y )

{

// Using 40x40px grid and saving every other z-index for the movers.

var offsetY = 0 ;

if ( y % 40.0 > 0 )

{

if ( y > Canvas . GetTop ( this ) )

{

// When going from up to down, pawn should be behind the objects.

offsetY = - 1 ;

}

else if ( y < Canvas . GetTop ( this ) )

{

// When going from down to up, pawn should be front of the objects.

offsetY = 1 ;

}

}

var zIndex = ( int ) Math . Round ( y / 40.0 ) * 2 + offsetY ;



Canvas . SetLeft ( this , x ) ;

Canvas . SetTop ( this , y ) ;

Canvas . SetZIndex ( this , zIndex ) ;



// Frame phase equals directly to the sprite sheet column.

this . TranslateTransform . X = ( int ) this . phase * - this . Rectangle . Width ;



// Frame direction equals directly to the sprite sheet row.

this . TranslateTransform . Y = ( int ) this . direction * - this . Rectangle . Height ;

}



public Task MoveAsync ( double x, double y )

{

this . source = < bool > ( ) ;

this . destination = ( x, y ) ;

this . timer . Draw += Timer_Draw ;

return this . source . Task ;

}



private void Timer_Draw ( object sender, EventArgs e )

{

try

{

var x = Canvas . GetLeft ( this ) ;

var y = Canvas . GetTop ( this ) ;



// Animate the walking frames by left-center-right-center-left-...

var nextPhase = FramePhase . Center ;

if ( this . phase == FramePhase . Center )

{

// Remember the previous active foot.

nextPhase = ( this . activeFoot == FramePhase . Left ) ? FramePhase . Right : FramePhase . Left ;

this . activeFoot = nextPhase ;

}

this . phase = nextPhase ;



// Check the pawn facing direction.

var offsetX = x - this . destination . X ;

var offsetY = y - this . destination . Y ;

if ( offsetX < 0 )

{

this . direction = FrameDirection . Right ;

}

else if ( offsetX > 0 )

{

this . direction = FrameDirection . Left ;

}

else

{

if ( offsetY < 0 )

{

this . direction = FrameDirection . Down ;

}

else if ( offsetY > 0 )

{

this . direction = FrameDirection . Up ;

}

}



// Move pawn 4px on canvas until final destination is reached.

var speed = 4.0 ;

if ( x > this . destination . X )

{

x = Math . Max ( x - speed, this . destination . X ) ;

}

else if ( x < this . destination . X )

{

x = Math . Min ( x + speed, this . destination . X ) ;

}

if ( y > this . destination . Y )

{

y = Math . Max ( y - speed, this . destination . Y ) ;

}

else if ( y < this . destination . Y )

{

y = Math . Min ( y + speed, this . destination . Y ) ;

}

SetCanvasLocation ( x, y ) ;

if ( x == this . destination . X && y == this . destination . Y )

{

this . timer . Draw -= Timer_Draw ;

this . source . TrySetResult ( true ) ;

}

}

catch ( Exception ex )

{

this . source . TrySetException ( ex ) ;

}

}

}

} GameAppFramePhaseLeft,Center,RightFrameDirectionDown,Left,Right,UpPawnUserControlGameTimer timerTaskCompletionSourcesourceFramePhase activeFootFramePhaseFramePhase phaseFramePhaseFrameDirection directionFrameDirectionPoint destinationIsSelectedPawnPawnGameTimer timertimerSetCanvasLocationx,offsetYCanvasoffsetYCanvasoffsetYzIndexMathoffsetYCanvas, xCanvas, yCanvas, zIndexTask MoveAsyncx, new TaskCompletionSource new Pointx, yTimer_DrawTimer_Drawsender, EventArgs eCanvasCanvasnextPhaseFramePhaseFramePhasenextPhaseFramePhaseFramePhaseFramePhasenextPhasenextPhaseoffsetXoffsetYoffsetXFrameDirectionoffsetXFrameDirectionoffsetYFrameDirectionoffsetYFrameDirectionspeedMathspeed,Mathspeed,Mathspeed,Mathspeed,SetCanvasLocationx, yTimer_DrawException exex

Now we can proceed to the rendering business by creating Canvas to our MainPage and name it a Map. Adding an event handler to Grid.Tapped event to catch user interaction with the background.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

< Page

x : Class = "GameApp.MainPage"

xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns : x = "http://schemas.microsoft.com/winfx/2006/xaml"

xmlns : local = "using:GameApp"

xmlns : d = "http://schemas.microsoft.com/expression/blend/2008"

xmlns : mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"

mc : Ignorable = "d" >



< Grid Background = "Gray" Tapped = "Map_Tapped" >

< Canvas x : Name = "Map" HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch" />

</ Grid >



</ Page >

Finally, in MainPage.xaml.cs code-behind, we can add some test Pawn objects to the Map. Adding the Pawn_Tapped event handler to flag which Pawn is currently selected and the Map_Tapped event handler will be moving the selected Pawn to the tapped destination (or to the nearest 40 x 40 pixel grid tile).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

using System ;

using System.Linq ;

using Windows.UI.Xaml ;

using Windows.UI.Xaml.Controls ;

using Windows.UI.Xaml.Input ;



namespace GameApp

{

public sealed partial class MainPage : Page

{

private GameTimer timer = ( ) ;



public MainPage ( )

{

this . InitializeComponent ( ) ;

this . Loaded += MainPage_Loaded ;

}



private void MainPage_Loaded ( object sender, RoutedEventArgs e )

{

// Create some test pawns on the map.

var pawn1 = ( this . timer ) ;

var pawn2 = ( this . timer ) ;

var pawn3 = ( this . timer ) ;

pawn1 . Tapped += Pawn_Tapped ;

pawn2 . Tapped += Pawn_Tapped ;

pawn3 . Tapped += Pawn_Tapped ;

pawn1 . SetCanvasLocation ( 120 , 120 ) ;

pawn2 . SetCanvasLocation ( 160 , 160 ) ;

pawn3 . SetCanvasLocation ( 200 , 200 ) ;

this . Map . Children . Add ( pawn1 ) ;

this . Map . Children . Add ( pawn2 ) ;

this . Map . Children . Add ( pawn3 ) ;

}



private void Pawn_Tapped ( object sender, TappedRoutedEventArgs e )

{

// Making sure the Map_Tapped event handler won't run.

e . Handled = true ;



// Selecting the tapped pawn and unselecting the others.

foreach ( Pawn pawn in this . Map . Children )

{

pawn . IsSelected = ( pawn == sender ) ;

}

}



private async void Map_Tapped ( object sender, TappedRoutedEventArgs e )

{

// Get the selected pawn from the map.

if ( this . Map . Children . FirstOrDefault ( x => ( ( Pawn ) x ) . IsSelected ) )

{

// Get nearest 40x40px grid destination and move there.

var position = e . GetPosition ( this . Map ) ;

var x = Math . Floor ( position . X / 40.0 ) * 40.0 ;

var y = Math . Floor ( position . Y / 40.0 ) * 40.0 ;

await pawn . MoveAsync ( x, y ) ;

}

}

}

} GameAppMainPagePageGameTimer timer new GameTimerMainPageMainPage_LoadedMainPage_Loadedsender, RoutedEventArgs epawn1 new Pawnpawn2 new Pawnpawn3 new Pawnpawn1Pawn_Tappedpawn2Pawn_Tappedpawn3Pawn_Tappedpawn1pawn2pawn3pawn1pawn2pawn3Pawn_Tappedsender, TappedRoutedEventArgs ePawn pawnpawnpawnsenderMap_Tappedsender, TappedRoutedEventArgs ePawn is Pawn pawnpositionMathpositionMathpositionpawnx, y

And that’s it! Now you can launch the game, select any Pawn and move it around by tapping on a gray area. There are, of course, some magic numbers and code to clean up, but you should see the basic concepts and jump start your own game with this. Please, comment below what you think. Happy developing!