Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Оценка производительности

XNA Game Studio: Оценка производительности игр




Для успешного написания компьютерных игр необходимо с особым вниманием относиться к объёмам системных ресурсов, затрачиваемых на выполнение игрой тех или иных задач.1 Прежде чем писать код, необходимо наметить цель (goal), к которой будем стремиться. Как ПО-инженерам при разработке кода нам необходимо придерживаться определённых целей. В этой главе мы рассмотрим вопросы быстродействия скомпилированного кода и обсудим некоторые элементы настройки производительности.
Прежде чем начать писать код, необходимо провести т.н. бенчмарк-замеры (benchmark measurement). Затем, по мере добавления в игру нового функционала, мы выясним, как это повлияло на её производительность.
В общих чертах нашей целью является "удержание" числа кадров в секунду (fps) в определённых пределах. В современных играх стандартным считается показатель fps равный около 60 кадров в секунду.

Игровым циклом (game loop) называют цикл, который обновляет объекты и рендерит их на экран, параллельно обсчитывая другие элементы игры (ввод, звук, искусственный интеллект, физика и т.д.). Каждая итерация (=прохождение) игрвого цикла рендеринга и прорисовки представляет собой один кадр. Таким образом наша цель - постоянно вызывать цикл прорисовки (draw loop) с частотой не менее 60 раз за 1 секунду. XNA обладает всеми средствами для организации главного цикла игры.

После выхода т.н. "управляемого" (Managed) DirectX, среди пограммеров возник ряд дискуссий о наиболее удачном способе организации главного цикла игры в среде Windows. К счастью, создатели XNA полностью решили данный вопрос. Игровой цикл XNA-фреймворка предоставляет методы Update и Draw, которые игрокодер может переопределять (override). Когда мы создаём наш клас игры (game class), мы наследуем его от базового класса Microsoft.Xna.Framework.Game, который предоставляет эти и ещё множесто других виртуальных методов.

Правило 80-20

Более 100 лет назад итальянский экономист Вильфредо Парето (Vilfredo Pareto) высказал мысль о том, что 80% всех благ Италии получают лишь 20% её граждан. Позднее он обнаружил ту же законмерность и в других странах. Позднее её назвали Правило 80-20. Существует множество различных вариаций данного правила в самых разных сферах человеческой деятельности. Многие говорят, что, якобы, 20% людей делают 80% всей работы, успешные лидеры тратят 80% своего времени на облагораживание (cultivating) 20% приближённых к ним людей и т.д. Трудно сказать, почему данный принцип работает. Но он работает. Данное правило даже работает при оценки производительности программных приложений: 20% исходного кода требуют оптимизации, т.к. именно он (код) является критическим для быстродействия всего приложения. Т.к. 20% нашего исходного кода делают 80% всей работы, данное правило нельзя списывать со счетов.
В ходе обсуждения в этой главе оценки производительности, важно также помнить про быстродействие кода ещё во время его написания. Но также не следует этим чересчур увлекаться, заморачиваясь на микрооптимизациях на самых ранних этапах. Сэр Тони Хоур (Tony Hoare) стал широко извествен благодаря своему высказыванию: "Преждевременная оптимизация - корень всех зол" ("Premature optimization is the root of all evil."). Буквально оно означает, что заниматься ею всё равно нужно, но не стоит ею чрезмерно увлекаться. Эту фразу можно сравнить с расхожим утверждением, что деньги являются корнем всех зол, в то время как подразумевается, что корнем зол являются не сами деньги, а чрезмерная любовь к ним и жажда наживы.
Так на каком же этапе разработки ПО следует озаботиться его быстродействием? Всегда найдутся те, кто скажет, что преждевременным является всё, что предшествует окончанию цикла разработки. Найдутся и те, кто попадут в ловушку микрооптимизаций с применением микробенчмркинг-тестирования (micro-benchmark testing), которое мы обязательно рассмотрим чуть ниже. Идеальное время заняться оптимизацией находится между этими двумя крайностями.
Ключевым фактором здесь является измерение (measurement). Мы так и не знаем, что и где необходимо оптимизировать, до тех пор, пока не выполним замеры. Не стоит тратить время на оптимизацию метода, который вызывается лишь однажды, например при запуске приложения (если, конечно, время загрузки не превышает допустимых пределов). Мы не начинаем оптимизацию метода до тех пор, пока не замерим его производительность и не выясним, что именно в нём проблема.
В ходе разработки приложения необходимо время от времени проводить проверку производительности с целью сравнить замеры с поставленными целями. Если что-то в коде требует времени чуть больше, чем обычно, но это отклонение в целом укладывается в первоначальные цели, то обычно такой участок просто помечают, чтобы вернуться к нему позднее. Часто такие участки кода вовсе не нуждаются в выделении времени на их оптимизацию. Но когда необходимо повысить быстродействие приложения вцелом, к таким участкам кода можно затем вернуться для внесения изменений.
В время разработки, в случае, когда программер не уверен, где именно кроется "бутылочное горлышко", в ход идут т.н. профайлеры (profiler tools). Один из популярных профайлеров - ANTS Profiler от Redgate Software (http://www.red-gate.com/products/ants_profiler/index.htm(external link)). Но он платный (стоит несколько сотен USD).
Другой, но уже совершенно бесплатный профайлер с открытым кодом называется NProf (http://sourceforge.net/projects/nprof(external link)). Данный программный инструмент показывает объём времени, затрачиваемый на выполнение каждого метода в приложении и в итоге выдаёт суммированное значение (total time). Существует множество других профайлеров. Их нетрудно найти в Интернете. Главное уяснить, что существуют такие инструменты, помогающие обнаружить в исходном коде участки с т.н. "бутылочным горлышком", где процедуры выполняются заметно медленнее. Используя профайлер, ты найдёшь до 20% кода, требующего оптимизации.

Создание бенчмарка


Рис. 1 Выбираем шаблон будущего игрового проекта.
Рис. 1 Выбираем шаблон будущего игрового проекта.


Чтобы организовать главный цикл игры (game loop), мы начнём новый проект игры для ОС Windows. Назовём его PerfomanceBenchmark. В игре будет счётчик числа кадров в секунду (framarate counter), который с помощью функции Update будет выводиться на тайтлбаре окна приложения.

  • Стартуй MSVC#2008, если не сделал этого ранее. Создай новый проект.
Где взять и как установить Microsoft Visual C# 2008 читай здесь: здесь(external link).
Как её подружить с XNA Framework читай здесь(external link).
  • В окне New Project выбери Windows Game (См. Рис.1). Укажи имя проекта PerfomanceBenchmark и нажми OK.
Спустя несколько секунд MSVC#2008 сгенерит шаблон игрового Решения и в правой части, в Solution Explorer, появится древовидная структура файлов будущего игрового проекта. В левой части открыт основной файл исходного кода Game1.cs .
Уже сейчас всё это дело можно отправить на компиляцию, выбрав в главном меню Build -> Build Solution, нажав F6 на клавиатуре (компиляция без запуска приложения) или F5 (компиляция с отладкой и запуском приложения).
  • Жми F5.
Спустя несколько секунд на экране появится окно скомпилированного приложения с тулбаром (toolbar) и голубым фоном.
  • Закрой окно приложения.
Вернёмся к его исходному коду.

  • Найди в исходном коде Game1.cs конструктор главного класса программы:
Фрагмент файла Game1.cs
...
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

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

Добавь в конструктор, после строки Content.RootDirectory = "Content";
следующие строки:
// Не синхронизировать наш метод Draw с вертикальной перерисовкой (retrace) нашего монитора.
graphics.SynchronizeWithVerticalRetrace = false;
// Не вызывать метод Update в каждом кадре на частоте кадров по умолчанию (default rate), т.е. 60 раз в секунду.
IsFixedTimeStep = false;

Автодополнение регулярных слов, свойств, названий имён пространств, методов и т.п. (Code Assistant) в MSVC#2008 работает на ура. Набирать код - одно удовольствие!

В конструкторе главного класса, только что созданного шаблонного проекта, также видим готовый менеджер графических устройств (GraphicsDeviceManager). Уже строкой ниже мы устанавливаем его свойство graphics.SynchronizeWithVerticalRetrace в false. По умолчанию оно установлено в true. Как следует из названия, данное свойство синхронизирует вызов метода Draw из главного цикла (game loop) XNA-фреймворка для того, чтобы периодичность его вызова совпадала (coinside) с частотой обновления (refresh rate) монитора (для современных ЖК-мониторов она обычно составляет 60 Гц). При стандартной частоте обновления в 60 герц, функция Draw по умолчанию перерисовывает изображение на экране каждую 1/60 долю секунды, или 60 раз в секунду. По умолчанию XNA перерисовывает экранное изображение одновременно с обновлением изображения монитора для того, чтобы предотвратить нежелательные эффекты отставания анимации или размазывания. В принципе, это как раз то, что нам необходимо. В то же время, при измерении производительности работы кода с частотой 60 раз в секунду очень сложно определить, помогли ли внесённые в код изменения или нет. Мы просто так и не узнаем, когда что-нибудь пойдёт не так, до тех пор, пока снижение быстродействия не повлияет на способность XNA-фреймворка перерисовывать изображение достаточно быстро.
Также мы установили свойство IsFixedTimeStep в false. По умолчанию оно установлено в true. Данное свойство позволяет XNA знать, должен ли он немедленно вызывать метод Update сразу после вывода изображения на экран (значение false) или только по прошествии определённого промежутка времени (значение true). Как правило этот промежуток также равен 1/60 доле секунды. Данное значение хранится в переменной TargerElapsedTime и его можно позднее изменить. В нашем случае нам необходимо, чтобы фреймворк вызывал метод Update как можно чаще, чтобы функция Draw тоже вызывалась как можно чаще. В XNA вызов функции Draw выполняется после каждого вызова функции (метода) Update.

  • В начале класса Game1, до начала конструктора, добавь следующие определения:
private float fps;
        private float updateInterval = 1.0f;
        private float timeSinceLastUpdate = 0.0f;
        private float framecount = 0;

В результате весь класс Game1 будет выглядеть так:
Фрагмент файла Game1.cs
...
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private float fps;
        private float updateInterval = 1.0f;
        private float timeSinceLastUpdate = 0.0f;
        private float framecount = 0;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            // Не синхронизировать наш метод Draw с вертикальной перерисовкой (retrace) нашего монитора.
            graphics.SynchronizeWithVerticalRetrace = false;
            // Не вызывать метод Update в каждом кадре на частоте кадров по умолчанию (default rate), т.е. 60 раз в секунду.
            IsFixedTimeStep = false;
        }
...


  • Найди в Game1.cs метод Draw:
Фрагмент файла Game1.cs
...
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
...

  • Внутри него между строками GraphicsDevice.Clear(Color.CornflowerBlue); и base.Draw(gameTime); размести следующий код, подсчитывающий кол-во кадров в секунду:
float elapsed = (float)gameTime.ElapsedRealTime.TotalSeconds;
            framecount++;
            timeSinceLastUpdate += elapsed;
            if (timeSinceLastUpdate > updateInterval)
            {
                fps = framecount / timeSinceLastUpdate; // Подразумевается fps, превышающий updateInterval
#if XBOX360
                System.Diagnostics.Debug.WriteLine("FPS: " + fps.ToString() + " - RT: ") +
                gameTime.ElapsedRealTime.TotalSeconds.ToString() + " - GT: " +
                gameTime.ElapsedRealTime.TotalSeconds.ToString());
#else
                Window.Title = "FPS: " + fps.ToString() + " - RT: " +
                    gameTime.ElapsedRealTime.TotalSeconds.ToString() + " - GT: " +
                    gameTime.ElapsedGameTime.TotalSeconds.ToString();
#endif
                framecount = 0;
                timeSinceLastUpdate -= updateInterval;
            }


  • Сохрани Решение.

Здесь мы первым делом сохраняем значение времени (elapsed time), прошедшего с момента последнего вызова метода Draw. Затем инкрементируем (= увеличиваем на 1) счётчик кол-ва кадров (framecount), а также суммируем его значение с переменной timeSinceLastUpdate, которая хранит общее значение затраченного времени.
Ниже, с посощью условного оператора if, проверяем, достаточно ли времени прошло для обновления (update) фреймрейта.
В объявлении класса Game1 мы объявили переменную updateInterval, присвоив ей значение 1 секунда. Конечно, позднее это значение можно изменить при необходимости. Как только пройдёт достаточно времени для пересчёта фреймрейта, мы сразу начинаем его путём деления кол-ва кадров на время, затраченное на выполнение условия в операторе if.
Далее обновляем заголовок окна (title), выводя в него актуальное значение fps. Туда же выводим текущие значения ElapsedRealTime и ElapsedGameTime.
Для подсчёта fps мы используем реальное время (real time).

Закрыть
noteОбрати внимание

Свойство SyncronizeWithVerticalRetrace определяет, как часто будет вызываться метод Draw. Свойство IsFixedTimeStamp определяет, как часто будет вызываться метод Update. Свойство ElapsedRealTime ассоциировано со временем, которое требуется для очередного вызова метода Draw, в то время как свойство ElapsedGameTime ассоциировано со временем, которое требуется для очередного вызова метода Update.


В конце фрагмента мы обнуляем (reset) переменную framecount (подсчёт кол-ва кадров) и timeSinceLastUpdate.

  • Сохрани Решение.
  • Скомпилируй Решение, нажав F5.
Окно приложения покажется на экране. На тайтлбаре окна в реальном времени отображаются текущие значения FPS (кол-во кадров в секунду) RT (ElapsedRealTime) и GT (ElapsedGameTime).
Таким образом получаем 3 базовых значения (FPS, RT и GT), которые и возьмём за основу для будущих вычислений производительности. В идеале необходимо вести записи (например в табличном процессоре OpenOffice Calc) с регулярным занесением значений всех трёх свойств, допустим после каждого глобального изменения, загрузки игровой карты и т.д.
Важно проводить тесты (замеры) производительности примерно в тех же условиях, например на том же компьютере и с минимальным количеством одновременно открытых окон других программ.

Источники:


1. Chad Carter. Microsoft XNA Game Studio 3.0 Unleashed. - Sams Publishing. 2009


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Оценка производительности

Contributors to this page: slymentat .
Последнее изменение страницы Четверг 20 / Июнь, 2019 13:00:51 MSK автор slymentat.

Помочь проекту

Яндекс-деньги: 410011791055108