Sunday, June 5, 2011

XNA Tutorial - Basic Event Handling

One of the most important aspects of any program that is intended to be used by people (i.e. Application Software) is the concept of event handlers. An event is simply some intended user input, like a button push or a mouse-click on something. The programmer must specifically define what actions s/he wants a program to perform and what events those actions should be triggered by. Unless specifically coded somewhere else (such as the default actions for pressing the ‘Windows/Home’ button on a Windows Phone), a programmer must specifically tell the program what events (user actions) to listen for, and what to do for each type of event. If a user performs some input that the programmer wanted, then that triggers some action response from the program.

To recap: a user performs some specific action, if that action is one of the events wired by the programmer, then an appropriate action is performed by the program's event handler. In programmer speak, we say that an event handler 'listens' for events, then 'triggers' an appropriate action once its event is detected. Again this event is just some specific user input.

In this tutorial we will be wiring a bare-bones event handler to listen for user input in the form of a finger tap on an icon. We will create a new WP7 Game that displays that icon, listens for a tap Gesture event occurring on that icon, and then performs a Game exit.



Prerequisites: Be able to create a new Project in Microsoft Visual Studio.

Now that we have a good idea of what we want to do, we can take a look at the default classes that are generated whenever you create a new project.
  • Go ahead and launch MSVS 2010,
  • and select "File > New Project."
  • Select a Windows Phone 7 Game (4.0) project from the XNA Game Studio 4.0 Templates, name the Project "EventHandlerTutorial", and click OK.
  • Double click on the EventHandlerTutorial > Game1.cs class in the Solution Explorer. (If you can't see the Solution Explorer, select "View > Solution Explorer")
If you remove all of the extraneous comments you should have something like this:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

namespace WindowsPhoneGame1
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            base.Draw(gameTime);
        }
    }
}


In the Solution Explorer,
  • right-click the Game1.cs and select the 'Rename' option.
  • Then rename the file to EventHandlerTutorialGame.cs".
  • When prompted, answer 'yes' to wanting to rename all occurrences of the old class name in the solution.
This is optional, and I will be referring to this class as either 'the main Game class', or as 'EventHandlerTutorial.cs', whichever I feel like at the time.

If you select the correct setting in the "XNA Game Studio Deployment Device" selector (either WP7 Device or Emulator), and start debugging (F5) you will see a solid blue screen. If you press the 'back' button on the phone, the Game will exit back out to the main screen. We will be replicating this effect with an event handler.

  • Right-click on the EventHandlerTutorial project in the Solutions Explorer, and select Add > Class.
  • Name this class "InputState.cs" and click the Add button.

Inside this class we will be tracking the state of the phone's touch screen. We will need a TouchCollection class instance from the Microsoft.Xna.Framework.Input.Touch library, and we will also need a GestureSample class instance from that same library.

Now to the code:
  • Make the InputState class public.
public class InputState
    {
    }
  • Bring the library into scope with a using statement so that we can use the TouchCollection and GestureSample classes.
using Microsoft.Xna.Framework.Input.Touch;


  • Now create a 'Fields' region and
  • declare a public TouchCollection TouchState,
  • and a public readonly List<GestureSample> Gestures,
as below:
#region Fields

        public TouchCollection TouchState;
        public readonly List<GestureSample> Gestures = new List<GestureSample>();
        
        #endregion


  • Create another region and call it "Initialization".
  • Inside this region create the default constructor for the InputState class
It takes no parameters, and it doesn't do anything besides allow us to create instances of this class. It should look as follows:
#region Initialization

        public InputState()
        {
        }
        #endregion

  • Now create yet another region for this class’s Public Methods,
  • and define a public void Update method therein.
This Update method will be what the Game uses to store user input for use in handling events.
  • Inside of it we will need to get the current state of the TouchPanel and store all the Gestures held in that state.
We do this like so:
#region Public Methods

        public void Update()
        {
            // Get user input from the touch panel since last update.
            TouchState = TouchPanel.GetState();

            // Clear the Gesture buffer from last update, so we don't act on it again.
            Gestures.Clear();

            // Add all the stored gestures from the TouchPanel in the Gestures List.
            while (TouchPanel.IsGestureAvailable)
            {
                Gestures.Add(TouchPanel.ReadGesture());
            }
        }
        #endregion

Here is the InputState.cs class in its entirety:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework.Input.Touch;

namespace EventHandlerTutorial
{
    public class InputState
    {
    
        #region Fields

        public TouchCollection TouchState;
        public readonly List<GestureSample> Gestures = new List<GestureSample>();
        
        #endregion

        
        #region Initialization

        public InputState()
        {
        }
        #endregion

        
        #region Public Methods

        public void Update()
        {
            // Get user input from the touch panel since last update.
            TouchState = TouchPanel.GetState();

            // Clear the Gesture buffer from last update, so we don't act on it again.
            Gestures.Clear();

            // Add all the stored gestures from the TouchPanel in the Gestures List.
            while (TouchPanel.IsGestureAvailable)
            {
                Gestures.Add(TouchPanel.ReadGesture());
            }
        }
        #endregion
    }
}



Now that we have a class in which to store our user input, we need to have some means of reading that input and determining if we need to do something because of it. We will do that in the main Game class.
  • Open up the code for the EventHandlerTutorialGame.cs class.
  • Add a field for an InputState class instance
  • and initialize it in the Game's constructor.
#region Fields

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;   
        
        InputState = inputState;

        #endregion
public EventHandlerTutorialGame()
        {
            Content.RootDirectory = "Content";
            graphics = new GraphicsDeviceManager(this);
           
            // Initialize our input storage class.
            inputState = new InputState();                    //<-- HERE
            
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }
  • Now go to the Update(GameTime) method and put a call to inputState.Update in it.
This way the user's input gets stored every update cycle.
protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // Get user input and put all gestures into inputState.Gestures List.
            inputState.Update();                              //<-- HERE
 
            base.Update(gameTime);
        }
Now we need to declare which gestures we want to use as events. Also in the main Game class, go to the Initialize method.
  • Set TouchPanel.EnabledGestures to a Tap gesture.
protected override void Initialize()
        {
            // Tell the TouchPanel to look for Tap type events.
            TouchPanel.EnabledGestures = GestureType.Tap;

            base.Initialize();
        }

So far we are now reading all touch input from the user every update cycle, and we’re listening for Tap gestures. There are really only two things left to do: create an area to tap on, and make the game recognize that it needs to exit when that tap occurs. To create an area to tap on, all we actually need is a Rectangle to perform boundary checks on. We should also add an image so that the user can see where they need to tap. The image is technically optional, as the tap gesture event can be detected without it. Let's create both of these in a single new class.
  • Right-click on the EventHandlerTutorial project and select ‘Add > Class’.
  • Name this class "Button.cs", and make it public.
public class Button
    {
    }
  • Bring the XNA Framework, Framework.Content, and Framework.Graphics libraries into scope with some using statements.
We need them for the Rectangle and Vector2 classes, the ContentManager class, and the SpriteBatch and Texture2D classes respectively.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
In the Fields region
  • put a Rectangle named buttonRectangle,
  • a Texture2D named buttonImage,
  • and a Vector2 named position.
#region Fields

        private Rectangle buttonRectangle;
        private Texture2D buttonImage;
        private Vector2 position;  
        
        #endregion
  • Right-click the buttonRectangle field and select ‘Refactor > Encapsulate Field’ to auto-generate some properties.
  • Repeat the same encapsulation process for the buttonImage
  • and position fields (if you are not using the full version of Visual Studio, you will have to type these out by hand).
  • Put these three properties into a Properties region.
#region Properties

        public Rectangle ButtonRectangle
        {
            get { return buttonRectangle; }
            set { buttonRectangle = value; }
        }
        public Texture2D ButtonImage
        {
            get { return buttonImage; }
            set { buttonImage = value; }
        }
        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }

        #endregion
  • Create an Initialization region, then
  • make a public constructor for the Button class that takes a ContentManager, a string, and a Vector2 as input parameters.
We will have to add an image to the project then load it in when we create the button in our main Game class. But we'll get to that when we're done here. Inside the constructor:
  • load a Texture2D using the passed in string as a reference location and assign it to the buttonImage field,
  • set Position to the passed in Vector2 value,
  • and create a new Rectangle using the X and Y components of Position and the dimensions of buttonRectangle.
The only tricky part here is that you’ll have to cast the Position.X and Position.Y values to integers when you pass them to the Rectangle constructor (they have the float type originally).
#region Initialization

        public Button(ContentManager content, string imagePath, Vector2 position)
        {
            buttonImage = content.Load<Texture2D>(imagePath);

            Position = position;

            buttonRectangle = new Rectangle((int)position.X,
                                            (int)Position.Y,
                                            buttonImage.Width, 
                                            buttonImage.Height);
        }

        #endregion

The last thing we need for the Button class is a Draw method. We’ll have to specifically call it in the main Game class, and it’s probably a good idea if we use the main Game’s SpriteBatch as well. If we just created a SpriteBatch inside the Button class, we would be creating a separate SpriteBatch for every button we put on the screen! So we’ll just pass the main Game’s SpriteBatch in as a parameter.
  • Create a public void Draw method that takes a SpriteBatch class instance as an input parameter.
  • Inside the method call the SpriteBatch.Draw method and pass in the buttonImage and position fields along with a Color.White value.
The first two are obviously to draw what we want, but the last may be new to you. The Color.White is a tint parameter. If you wanted to make the thing you were drawing tinted another color, you could pass that color in place of the White color. Using Color.White basically implies ‘no tint’.
#region Public Methods

        public void Draw(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(buttonImage, position, Color.White);
        }

        #endregion

The finished Button.cs class should look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace EventHandlerTutorial
{
    public class Button
    {
        #region Fields

        private Rectangle buttonRectangle;
        private Texture2D buttonImage;
        private Vector2 position;  
        
        #endregion


        #region Properties

        public Rectangle ButtonRectangle
        {
            get { return buttonRectangle; }
            set { buttonRectangle = value; }
        }
        public Texture2D ButtonImage
        {
            get { return buttonImage; }
            set { buttonImage = value; }
        }
        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }
        #endregion


        #region Initialization

        public Button(ContentManager content, string imagePath, Vector2 position)
        {
            buttonImage = content.Load<Texture2D>(imagePath);

            Position = position;

            buttonRectangle = new Rectangle((int)position.X,
                                            (int)Position.Y,
                                            buttonImage.Width, 
                                            buttonImage.Height);
        }

        #endregion


        #region Public Methods

        public void Draw(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(buttonImage, position, Color.White);
        }

        #endregion        
    }
}


Up to this point we have an InputState class to get and store all user input gestures, and we have a Button class that displays an image and contains a Rectangle for use in a basic tap-type event. All that remains is to use the Button class in the main Game, and to create a couple methods that handle the user’s input and setup the screen. Before we get to that, we need to add the image we’re going to use as a button icon. Right-click on the EventHandlerTutorialContent(Content) project and select ‘Add > Folder’. Name this folder “Buttons”. You can use any image you want for this really, but I’m going to use this one.
  • Save it to your hard-drive,
  • then right-click the Content Project’s "Buttons" folder and select ‘Add > Existing Item’.
  • Navigate to wherever you saved the button_close.png file, and press the Add button.
Now we can use the image in our project. Inside the EventHandlerTutorial.cs class,
  • add a Button field named button.
  • In the LoadContent method, initialize the Button to a new Button instance and pass in the Game’s ContentManager, the string ‘@”Buttons\button_close”’ (without the single-quotes), and a new Vector2(5f,5f).
Like this:
#region Fields

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;        
        
        InputState inputState;
        Button button;                              //<-- HERE

        #endregion
// Initialize the button, passing in the location for the image file.
            button = new Button(Content, @"Buttons\button_close", new Vector2(5f, 5f));
    
Now in the Draw method, we need to
  • start up the SpriteBatch,
  • Draw the button,
  • then shut the SpriteBatch back down.
protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Draw our close button
            spriteBatch.Begin();
            button.Draw(spriteBatch);
            spriteBatch.End();

            base.Draw(gameTime);
        }
If you debug the game now, you should see the icon in the top left corner of the screen (in landscape view), but pressing it still doesn’t do anything. I’m going to set the screen to display in portrait mode, because I like it better for this (and because that's the orientation I created all of the example images from :P Back inside the main Game class,
  • create a public void method named InitializePortraitView.
  • Inside the method set the GraphicsDevice’s PreferredBackBufferHeight and PreferredBackBufferWidth properties to 800 and 480 respectively.
  • Add a call to this method in the class constructor.
public void InitializePortraitView()
        {
            graphics.PreferredBackBufferHeight = 800;
            graphics.PreferredBackBufferWidth = 480;
        }
public EventHandlerTutorialGame()
        {
            Content.RootDirectory = "Content";
            graphics = new GraphicsDeviceManager(this);
            InitializePortraitView();                      //<-- HERE
          
            // Initialize our input storage class.
            inputState = new InputState();
            
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }
Now when the Game runs, it should look like this:




Still inside the main Game class,
  • create a new public void method HandleInput that takes an Input State as a parameter.
Inside the method we’re finally going to get to the meat of the event handler!
  • Create a foreach loop to iterate through the GestureSample List from the InputState class.
  • Inside this foreach loop check whether the current GestureSample is of the GestureType Tap.
  • If it is, then create a Point using the GestureSample’s X and Y coordinates,
  • and check if the button’s Rectangle contains that point.
  • If it does, then exit the Game.
public void HandleInput(InputState input)
        {
            foreach (GestureSample gesture in input.Gestures)
            {
                if (gesture.GestureType == GestureType.Tap)
                {
                    Point tapLocation = new Point((int)gesture.Position.X, (int)gesture.Position.Y);
                    
                    if (button.ButtonRectangle.Contains(tapLocation))
                        this.Exit();
                }
            }
        }
The very last thing we need to do is
  • call this HandleInput method inside the Update method,
specifically after we have updated the InputState for this cycle.
protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // Get user input and put all gestures into inputState.Gestures List.
            inputState.Update();

            // Handle some events!
            HandleInput(inputState);
            
            base.Update(gameTime);
        }
The final code for the EventHandlerTutorial.cs class is here:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

namespace EventHandlerTutorial
{
    public class EventHandlerTutorialGame : Microsoft.Xna.Framework.Game
    {
        #region Fields

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;        
        
        InputState inputState;
        Button button;

        #endregion


        public EventHandlerTutorialGame()
        {
            Content.RootDirectory = "Content";
            graphics = new GraphicsDeviceManager(this);
            InitializePortraitView();
          
            // Initialize our input storage class.
            inputState = new InputState();
            
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }

        protected override void Initialize()
        {
            // Tell the TouchPanel to look for Tap type events.
            TouchPanel.EnabledGestures = GestureType.Tap;

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Initialize the button, passing in the location for the image file.
            button = new Button(Content, @"Buttons\button_close", new Vector2(5f, 5f));

        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // Get user input and put all gestures into inputState.Gestures List.
            inputState.Update();

            // Handle some events!
            HandleInput(inputState);
            
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Draw our close button
            spriteBatch.Begin();
            button.Draw(spriteBatch);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        public void HandleInput(InputState input)
        {
            foreach (GestureSample gesture in input.Gestures)
            {
                if (gesture.GestureType == GestureType.Tap)
                {
                    Point tapLocation = new Point((int)gesture.Position.X, (int)gesture.Position.Y);
                    
                    if (button.ButtonRectangle.Contains(tapLocation))
                        this.Exit();
                }
            }
        }

        public void InitializePortraitView()
        {
            graphics.PreferredBackBufferHeight = 800;
            graphics.PreferredBackBufferWidth = 480;
        }
    }
}

That’s it! Launch the debugger and check it out. You can now tap the icon and the Game exits, just as if you had pressed the ‘back’ button on your Windows Phone 7 device. That was a bit longer than I was anticipating, but I think it gets the point across. There are other things we could do to spruce this up a bit. We could add a ‘pressed’ condition to the button, that would change the currently drawn image to a darker one while the user is pushing it or for just a moment after the user taps it. We could also create a Screen class that could contain several buttons, each doing something different. But these are going to be the topics for later tutorials. If anything’s not clear, or just seems messed up, leave me a comment and I’ll try to fix it.
I hope this helps someone,
-H

2 comments:

  1. Just found this via Google. I am currently learning XNA with the goal of porting an app I have made in Flash.
    This is a great article and exactly what I needed to learn about events in XNA. Thank you very much! :-)

    ReplyDelete
  2. Ok, but in this example you are not using C# Events, and the term Event is being translated to the user action. Then, you are actively polling the input class for gestures.

    Not wrong, just that by the article title I thought I would find something about how to use C# Events / delegates with XNA.

    :-)

    ReplyDelete