Friday, January 04, 2013

MonoGame, MonoDroid and threads

The holy grail of all programs, from games to utilities, is to provide a user interface which is useful and responsive while real work gets done in the background.

A classic example of this is the MonoGame content loading process which, on a small machine can be lengthy and give the appearance of the target device having crashed completely unless some indication of activity is provided.

The XNA Game system which is the basis for MonoGame defines a nice neat structure for game operations. The Game class is instantiated, The Initialize routine is called which in-turn calls LoadContent where we can load in all the textures, sounds, fonts and whatever else we need to run the game and finally a loop is entered where Update and Draw are called repeatedly in a loop for the life of the game. It stands to reason that if there is a lengthy delay between the call of the LoadContent method and the beginning of the Update-Draw loop then our user may be left staring at an uninterestingly blank screen.

This code shows how to use the .net BackgroundWorker to load content while still running the game loop to show a progress loading screen. As soon as the content is loaded, the game swaps over to normal operations and your users won't be left looking at a blank screen for fifteen seconds.


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.ComponentModel;
using System.Threading;

namespace BackgroundLoad
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont font;

        //Used to determine if we are ready to go
        bool _isInitialized;

        int _loadingProgress;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);

            Content.RootDirectory = "Content";

            graphics.IsFullScreen = true;
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 480;
            graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            //Here we can load anything that is absolutely necessary
            //to the basic operation of the game, such as a font to display progress
            font = Content.Load<SpriteFont>("spriteFont1");

            //For all other content we can use a BackgroundWorker
            BackgroundWorker bgw = new BackgroundWorker();
            bgw.WorkerReportsProgress = true;
            //Give the background worker something to do:
            bgw.DoWork += bgw_DoWork;
            //Handle progress updates:
            bgw.ProgressChanged += (s, e) => _loadingProgress = e.ProgressPercentage;
            //Handle completion:
            bgw.RunWorkerCompleted += (s, e) => _isInitialized = true;
            //Kick off the loading process
            bgw.RunWorkerAsync();
        }

        void bgw_DoWork(object sender, DoWorkEventArgs e)
        { //This happens on a thread not associated with the
          //rest of the game
           
            BackgroundWorker bgw = (BackgroundWorker)sender;

            for (int i = 0; i < 100; i++)
            {
                bgw.ReportProgress(i);
                Thread.Sleep(30);
            }

            /*
             *
            //Really we may do something like this...
            
            int p = 0;
            _gameTexture1 = Content.Load<Texture2D>("gametexture1");
            bgw.ReportProgress(p++);
            _gameTexture2 = Content.Load<Texture2D>("gametexture2");
            bgw.ReportProgress(p++);
            _gameTexture3 = Content.Load<Texture2D>("gametexture3");
            bgw.ReportProgress(p++);
            _gameTexture4 = Content.Load<Texture2D>("gametexture4");
            bgw.ReportProgress(p++);
            _gameTexture5 = Content.Load<Texture2D>("gametexture5");
            bgw.ReportProgress(p++);
            _gameTexture6 = Content.Load<Texture2D>("gametexture6");
            bgw.ReportProgress(p++);
            //.............
            //.............
            _gameTextureN = Content.Load<Texture2D>("gametextureN");
            bgw.ReportProgress(p++);
            
             */

        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            {
                Exit();
            }

            // TODO: Add your update logic here
            if (!_isInitialized)
            {
                //Do things here that you can do without the main resources
            }
            else
            {
                //Do all the normal game stuff here
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {

            if (!_isInitialized)
            {
                graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
                spriteBatch.Begin();
                spriteBatch.DrawString(font, string.Format("Loading...{0}% complete",_loadingProgress), new Vector2(16, 16), Color.White);
                spriteBatch.End();
            }
            else
            {
                graphics.GraphicsDevice.Clear(Color.Green);
                //Place all your normal game-drawing stuff here
                spriteBatch.Begin();
                spriteBatch.DrawString(font, "Hello from MonoGame!", new Vector2(16, 16), Color.White);
                spriteBatch.End();
           }

            base.Draw(gameTime);
        }
    }
}




1 comment:

Angus Cheng said...

Thanks a lot for this, I will try and implement this in my MonoDroid Game.

Angus