Статьи

Приключения в необработанной обработке исключений для XNA / Silverlight

Одна вещь, которая может пометить ваших пользователей / игроков, заключается в том, что неизбежно в течение ряда случайных событий происходит сбой, он не обрабатывается и вызывает просто сбой приложения / игры. Взрыв их идет всем моим прогрессом и спасает, очень плохой опыт

Даже с лучшими в своем роде приложениями и играми это может произойти только потому, что мы люди и не можем учесть каждую возможность или потенциально хитроумную вещь, которую могут сделать пользователи / игроки, будь то умышленное, случайное или просто глупое, или это может быть даже Вы виноваты в том, что думали, что игроки / пользователи захотят что-то сделать таким образом.

Все это может помешать вашему приложению / игре опубликоваться, но даже если люди будут играть в нее, это просто плохой опыт, если вы не уверены, что когда / если это произойдет, это произойдет обработанным способом (обработка необработанного) и информирует пользователя и, возможно, позволяет ему помочь вам и внести свой вклад.

Существует несколько механизмов для этого в XNA и Silverlight в зависимости от вашей платформы, здесь мы просто сосредоточимся на XBOX / PC для необработанного XNA и Silverlight + XNA на телефоне.

Образец для этого учебного пособия был размещен с источником серии пособий для начинающих здесь, на codeplex — http://bit.ly/JmuXTE


Нормальный путь для XNA

XNA действует как любая обычная программа, когда она запускается на Windows или XBOX, есть главная точка запуска «Программы», где вы можете обернуть логику, чтобы поймать упавшую игру в случае ее сбоя, когда это происходит, мы что-то делаем с этим, мы можем либо :

    Сохраните ошибку и покажите ее для использования при следующем запуске игры (или сразу же покажите ее).

    Дайте пользователю возможность отправлять по электронной почте или отправлять отчет об ошибке через веб-службу (или даже делать это молча, но я бы не рекомендовал этого делать). ) — Обратите внимание, что XBOX не имеет подключения, если вы не XBLA, поэтому этот параметр ограничен для XBOX

Принцип довольно прост, просто поместите цикл Try / Catch в класс Program:

static class Program
{
	/// 
	/// The main entry point for the application.
	/// 
	static void Main(string[] args)
	{
		try
		{
			using (Game1 game = new Game1())
			{
				game.Run();
			}
		}
		catch (Exception)
		{

		}
	}
}

Так что теперь мы обрабатываем ошибки, но мы все еще сбоим, это не выглядит так плохо для системы, в которой они находятся, и даст нам возможность посмотреть на сбой в отладчике.

Стандартный способ, которым я обрабатывал сбои, подобные этому в прошлом, — это запуск второго игрового проекта, который запускается в случае сбоя игры. Это дает мне представление об ошибке на экране, как это происходит, это особенно хорошо при попытке отладить игру, запущенную на XBOX без отладчика.

Для этого просто:

    Создайте новый проект XNA в решении, например «UnhandledExceptionReporter».
    Переименуйте Game.cs в «MyErrorHandler» (или что-то в этом роде) и переименуйте класс внутри.

Обязательно обновите весь код, который использует класс, например Program.cs в проекте обработчика ошибок и конструктор в игровом классе.

     Добавьте SpriteFont в проект ContentHandler Content (не ваш игровой проект).
     Добавьте свойство в класс ErrorHandler Game, чтобы сохранить текст ошибки
     в содержимом загрузки. Загрузите шрифт Sprite
    в цикле рисования. Нарисуйте текст ошибки.

Ваш класс Game теперь должен выглядеть примерно так:

public class MyErrorHandler : Microsoft.Xna.Framework.Game
{
	public string ErrorText = "No Error Found";
	GraphicsDeviceManager graphics;
	SpriteBatch spriteBatch;
	SpriteFont ErrorFont;

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

	/// 
	/// 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.
	/// 
	protected override void Initialize()
	{
		// TODO: Add your initialization logic here

		base.Initialize();
	}

	/// 
	/// LoadContent will be called once per game and is the place to load
	/// all of your content.
	/// 
	protected override void LoadContent()
	{
		// Create a new SpriteBatch, which can be used to draw textures.
		spriteBatch = new SpriteBatch(GraphicsDevice);
		ErrorFont = Content.Load("ErrorFont");
		// TODO: use this.Content to load your game content here
	}

	/// 
	/// UnloadContent will be called once per game and is the place to unload
	/// all content.
	/// 
	protected override void UnloadContent()
	{
		// TODO: Unload any non ContentManager content here
	}

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

		// TODO: Add your update logic here

		base.Update(gameTime);
	}

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

		// TODO: Add your drawing code here
		spriteBatch.Begin();
		spriteBatch.DrawString(ErrorFont, "An Error occured in your game, the error is as follows", Vector2.Zero, Color.White);
		spriteBatch.DrawString(ErrorFont, ErrorText, new Vector2(0,40), Color.White);
		spriteBatch.End();
		base.Draw(gameTime);
	}
}

Конечно, вы можете пойти еще дальше, добавив в класс больше полей и добавив больше данных, очистив вызов отрисовки, чтобы линии обернулись вокруг экрана и выглядели намного чище, но вы получите общую картину.

Вы можете спросить, почему бы просто не добавить новый класс игры в мой существующий проект, и ответ на него будет двойным, сделав его отдельным проектом, вы можете повторно использовать обработчик ошибок для любого проекта, плюс у вас не может быть двух классов Game в проекте XNA это просто запрещено Улыбка с открытым ртом

Чтобы завершить эту реализацию, просто обновите предыдущий код обработки ошибок, чтобы вместо этого вызывать новый обработчик ошибок, таким образом:

static void Main(string[] args)
{
	try
	{
		using (MyGame1 game = new MyGame1())
		{
			game.Run();
		}
	}
	catch (Exception e)
	{
		using (MyErrorHandler errorHandler = new MyErrorHandler())
		{
			errorHandler.ErrorText = e.Message;
			errorHandler.Run();
		}
	}
}

Теперь, когда у вас происходит сбой игры, вы увидите основную причину (или, по крайней мере, очень полезный отчет об исключениях .NET для сбоя Улыбнись языком), я бы порекомендовал расширить его, включив в него собственный текст или тестовые данные для отлова, когда и где произошла ошибка считай это домашним заданием!

образ


Введите Little Watson

Теперь, когда я внедрял свой собственный обработчик ошибок в свои проекты Phone, я наткнулся на твит из небольшого примера класса, который помогает с отчетами об ошибках, называемых «Little Watson». Это написано Энди Пеннеллом в блоге MSDN здесь — http://bit.ly/JhZLKP

Он очень аккуратный, и вместо того, чтобы иметь отдельный проект, он просто сохраняет ошибку и в следующий раз, когда вы запускаете свой проект, он может отправить вам отчет об ошибке по электронной почте (полезно для телефонных проектов, когда вы публикуете, чтобы пользователи давали вам больше информации, чем надоедливый AppHub отчеты об ошибках), это было приятно, но с некоторыми незначительными изменениями можно сделать так, чтобы оно также отображалось на экране и даже могло быть настроено для XBOX.

using System;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;

public static class LittleWatson
{
    static string filename = "LittleWatson.txt";
    static bool checkedForError = false;
    static bool errorFound = false;
    static string ErrorText;
    static SpriteFont ErrorFont;

    public static void ReportException(Exception ex, string extra)
    {
        try
        {
            using (var store = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null))
            {
                SafeDeleteFile(store);
                using (TextWriter output = new StreamWriter(store.CreateFile(filename)))
                {
                    output.WriteLine(extra);
                    output.WriteLine(ex.Message);
                    output.WriteLine(ex.StackTrace);
                }
            }
        }
        catch (Exception)
        {
        }
    }

    public static void LoadContent(ContentManager content)
    {
        ErrorFont = content.Load("ErrorFont");
    }

    public static bool ErrorFound
    {
        get
        {
            if (checkedForError)
            {
                return errorFound;
            }
            using (var store = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null))
            {
                if (store.FileExists(filename))
                {
                    errorFound =  true;
                }
            }
            checkedForError = true;
            return errorFound;
        }
    }

    public static void DrawException(SpriteBatch spriteBatch)
    {
        try
        {
            if (string.IsNullOrEmpty(ErrorText))
            {
                using (var store = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null))
                {
                    if (store.FileExists(filename))
                    {
                        using (TextReader reader = new StreamReader(store.OpenFile(filename, FileMode.Open, FileAccess.Read, FileShare.None)))
                        {
                            ErrorText = reader.ReadToEnd();
                        }
                        SafeDeleteFile(store);
                    }
                }
            }
            if (string.IsNullOrEmpty(ErrorText))
            {
                ErrorText = "An Error occured reading the Error, uh oh";
            }
                spriteBatch.Begin();
                spriteBatch.DrawString(ErrorFont, "An Error occured in your game, the error is as follows", Vector2.Zero, Color.White);
                spriteBatch.DrawString(ErrorFont, ErrorText, new Vector2(0, 40), Color.White);
                spriteBatch.End();
        }
        catch {}
        finally
        {
            SafeDeleteFile(IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null));
        }
    }

    private static void SafeDeleteFile(IsolatedStorageFile store)
    {
        try
        {
            store.DeleteFile(filename);
        }
        catch {}
    }
}

Если вы добавите вышеупомянутый класс в свой проект и spritefont для использования отчетов об ошибках, то вы можете обновить ваш program.cs так, чтобы

static void Main(string[] args)
{
	try
	{
		using (MyGame1 game = new MyGame1())
		{
			game.Run();
		}
	}
	catch (Exception e)
	{
		LittleWatson.ReportException(e, "A Bad Thing this way comes");
	}
}

Затем добавьте следующее в ваш метод «LoadContent» перед любыми вашими собственными вызовами (но после создания spritebatch и отчета об ошибках это нужно:

protected override void LoadContent()
{
	// Create a new SpriteBatch, which can be used to draw textures.
	spriteBatch = new SpriteBatch(GraphicsDevice);
	if (LittleWatson.ErrorFound)
	{
		LittleWatson.LoadContent(Content);
	}
	else
	{
		// TODO: use this.Content to load your game content here
		throw new Exception("I'm Causing a Ruckas and like to Shout about it!!");
	}
}

И, наконец, измените ваш вызов на ничью, чтобы проверить наличие ошибок и, если они найдены, возьмите их вместо обычного игрового дро:

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

	// TODO: Add your drawing code here
	if (LittleWatson.ErrorFound)
	{
		LittleWatson.DrawException(spriteBatch);
	}
	else
	{
		//Normal Draw Loop
	}

	base.Draw(gameTime);
}
  
 

Очевидно, что есть несколько альтернатив этому подходу, вы можете использовать отдельные методы для Error Draw и Normal Draw, если вы беспокоитесь о производительности и подключаете их в методе LoadContent, как всегда несколько способов достижения одной и той же цели.

Конечным результатом является то, что если ваша игра вылетает, при следующем запуске она отобразит ошибку. полезен как альтернатива отдельному проекту.


Из коробки Silverlight

С Silverlight для Windows Phone он уже был подключен с необработанным обработчиком исключений, готовым к работе, но все же лучше что-то сделать с этим, чтобы вы могли управлять и отслеживать эти неприятные ошибки, когда они происходят. Для примеров AdRotator я использовал модифицированную версию класса Little Watson Энди, так что я могу просто просмотреть ошибку, вместо того, чтобы отправлять ее, на самом деле семантика, но это то, как я работаю, так что последний маленький класс Watson, который я использовал (который отличается от XBOX / Windows выше, потому что у нас больше ресурсов) было:

using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using Microsoft.Phone.Tasks;

public static class LittleWatson
{
    const string filename = "LittleWatson.txt";

    public static void ReportException(Exception ex, string extra)
    {
        try
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                SafeDeleteFile(store);
                using (TextWriter output = new StreamWriter(store.CreateFile(filename)))
                {
                    output.WriteLine(extra);
                    output.WriteLine(ex.Message);
                    output.WriteLine(ex.StackTrace);
                }
            }
        }
        catch (Exception)
        {
        }
    }

    public static void CheckForPreviousException(string _emailTo, string _subject)
    {
        try
        {
            string contents = null;
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (store.FileExists(filename))
                {
                    using (TextReader reader = new StreamReader(store.OpenFile(filename, FileMode.Open, FileAccess.Read, FileShare.None)))
                    {
                        contents = reader.ReadToEnd();
                    }
                    SafeDeleteFile(store);
                }
            }
            if (contents != null)
            {
                if (MessageBox.Show("A problem occurred the last time you ran this application. Would you like to send an email to report it?\nOr click cancel to view it on screen", "Problem Report", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
                {
                    EmailComposeTask email = new EmailComposeTask();
                    email.To = _emailTo;
                    email.Subject = _subject;
                    email.Body = contents;
                    SafeDeleteFile(IsolatedStorageFile.GetUserStoreForApplication()); // line added 1/15/2011
                    email.Show();
                }
                else
                {
                    MessageBox.Show(contents, "Error Detail", MessageBoxButton.OK);
                }
            }
        }
        catch (Exception)
        {
        }
        finally
        {
            SafeDeleteFile(IsolatedStorageFile.GetUserStoreForApplication());
        }
    }

    private static void SafeDeleteFile(IsolatedStorageFile store)
    {
        try
        {
            store.DeleteFile(filename);
        }
        catch (Exception ex)
        {
        }
    }
}

Чтобы использовать это, просто обновите метод App.XAML.cs «Application_UnhandledException» из:

private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
	if (System.Diagnostics.Debugger.IsAttached)
	{
		// An unhandled exception has occurred; break into the debugger
		System.Diagnostics.Debugger.Break();
	}
}

Для того, чтобы:

private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
	if (System.Diagnostics.Debugger.IsAttached)
	{
		// An unhandled exception has occurred; break into the debugger
		System.Diagnostics.Debugger.Break();
	}
	else
	{
		try
		{
			LittleWatson.ReportException(e.ExceptionObject, "Starter3D SilverXNA Error");
		}
		catch { }
	}

}

Очевидно, что вы изменили бы текст отчета на что-то более подходящее для вашего проекта, это позволяет записать отчет об ошибке на диск, чтобы прочитать его обратно, просто добавив следующее в методы запуска и активации приложения:

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
	try
	{
		LittleWatson.CheckForPreviousException("[email protected]", "Starter3D SilverXNA Example Error Report");
	}
	catch { }
}

// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
	try
	{
		LittleWatson.CheckForPreviousException("[email protected]", "Starter3D SilverXNA Example Error Report");
	}
	catch { }
}

Первый параметр — куда отправлять отчет, если он найден (если пользователь с радостью отправит его по электронной почте, второй — тема). Именно поэтому ваш проект Silverlight защищен от гремлинов.

XNA на Windows Phone, другая история

Когда дело доходит до XNA на Windows Phone, ни один из перечисленных выше вариантов не доступен, XNA на Windows Phone не использует класс Program (он все еще существует, но его так же легко удалить, потому что он не используется), и у нас нет готовых настроек Доступны необработанные отчеты об исключениях, так что же нужно сделать.

В глубине библиотеки «System.Windows» скрыт объект приложения, настроенный для Windows Phone, и, как это происходит, в нем скрыто событие «UnHandledException», которое (начиная с Mango) теперь доступно для использования, поэтому все, что нам нужно сделать, это подключиться к нему.

Сначала добавьте ссылки на « System.Windows » и « Microsoft.Phone » (последний нужен для Little Watson), а затем скопируйте в классе Little Watson ваш проект Phone XNA (обратите внимание, что проекты SilverXNA по-прежнему являются проектами Silverlight, поэтому используйте предыдущий раздел) ,

Теперь подключите следующее в конструкторе вашего игрового класса:

Application.Current.UnhandledException += new EventHandler(Current_UnhandledException);

И добавьте соответствующую функцию для обработки следующим образом:

void Current_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
	if (System.Diagnostics.Debugger.IsAttached)
	{
		// An unhandled exception has occurred; break into the debugger
		System.Diagnostics.Debugger.Break();
	}
	else
	{
		try
		{
			LittleWatson.ReportException((Exception)e.ExceptionObject, "Example XNA Error caught");
		}
		catch { }
	}
}

Это заботится об отчетности, любые необработанные исключения будут отслеживаться и записываться, затем вам просто нужно проверить ошибки при запуске, где вы разместите это, зависит от вас, это должно быть достаточно скоро в вашей игре, чтобы сообщить ошибка до того, как это произойдет, но достаточно поздно, чтобы телефон был инициализирован достаточно, чтобы иметь возможность работать.  * Обратите внимание, что конструктор игры слишком раноУлыбка с открытым ртом

Таким образом, у вас остается либо функция «Initialized», либо «LoadContent», это на ваше усмотрение, просто добавьте следующий код там, где вы считаете, что он больше подходит для вашей игры (я предпочитаю инициализированную функцию BTW):

try
{
	LittleWatson.CheckForPreviousException("[email protected]", "Sample SL Example Error Report");
}
catch { }

Вот и все!


Хватит значит хватит

Что ж, я надеюсь, что вы получили что-то из этой статьи, было приятно отвлечься, когда я занимаюсь другими делами, нужно, чтобы старые клетки мозга тикали.

Далее я обновляю инструкции по внедрению AdRotator в XNA и Silverlight, более подробное объяснение. Эта статья была просто отрывком (о котором я бы подумал, что он намного короче, но просто так не закончился) с добавлением обработки ошибок во все примеры проектов.