Загрузка...
 
Печать

Стейты игры и процессы (Game States and Processes)




Intro

Оптимизация программного потока является одной из главных целей любого игрокодера. Исходный код небольшого размера прост в управлении. Но по мере роста приложения работать с ним становится всё сложнее. Особенно когда для изменения небольшой функции приходится переписывать огромные куски кода.
Допустим, разработка игры в процессе и ты решил добавить новую фичу, которая открывает в игре экран инвентори по нажатию клавиши i. Экран инвентори (inventory display screen) может быть активирован только во время игры, а не на главном экране игрового приложения. А значит необходимо внедрить код, определяющий нажатие клавиши i и открывающий по нажатию окно inventory.
Если тупо сделать одну функцию, которая рендерит каждый экран (display screen) независимо от того, что делает игрок в игре, то очень скоро функция рендеринга станет очень большой, сложной и не имеющей возможности отследить, какой из стейтов активен в данный момент.
На помощь придут две методики: Системы управления стейтами и процессами.1

Стейты приложения (Application States)


Image
Рис.1 Стек стейтов позволяет помещать (push) и удалять (pop) из него стейты по мере необходимости


Стейт - это сокращение от "стейт операции" (state of operation), представляющий собой текущее состояние (процесс), которое приложение обрабатывает. Главное меню игры - это стейт. Игровой процесс - это тоже стейт, также как и экран inventroy.
При добавлении в приложение различных стейтов, необходимо также продумать способ определить, как их обрабатывать в зависимости от текущего стейта операции (который постоянно меняется с одного на другой) Процесс определения необходимого стейта может обернуться таким ужасающим кодом:
switch(CurrentState)
{
	case STATE_TITLESCREEN: DoTitleScreen();
		break;
	
	case STATE_MAINMENU: DoMainMenu();
		break;
	
	case STATE_INGAME: DoGameFrame();
		break;
}

Со временем такой список стейтов значительно разрастётся. И если весь его обрабатывать в каждом кадре, то получим значительное снижение производительности.
На практике вместо такого цикла switch...case применяют т.н. программирование на основе стейтов (state-based programming; SBP). Его суть заключается в ветвлении выполнения приложения на основе т.н. стэка стейтов (stack of states). В этом случае каждый стейт представляет собой объект или набор функций, которые добавляются в стек при необходимости. Более неиспользуемые функции удаляются из стека (См. Рис.1).
Стейты добавляются, удаляются и обрабатываются (process) с помощью менеджера стейтов (state manager). Будучи выброшенным (popped) из стека стейтов, самый верхний стейт удаляется (не насовсем), и очередь на обработку сдвигается так, что в обработку идёт следующий за ним стейт. Менеджер стейтов должен получать указатели (pointers) на функции (которые представляют те или иные стейты). При отправке стейта на обработку его указатель добавляется в стек к остальным. Задача игрокодера - своевременно вызывать менеджер стейтов, который будет обрабатывать самый верхний стейт в своём списке. На самом деле с менеджером стейтов работать нетрудно. Рассмотрим пример его реализации:
class cStateManager
{
	// Структура, которая хранит указатель функции и связный список (linked list)
	typedef struct sState
	{
		void (*Function)();
		sState *Next;
	}sState;

protected:
	sState *m_StateParent; // Самый верхний стейт в стеке ("голова" стека)
public:
	cStateManager() {m_StateParent=NULL;}
	
	`cStateManager()
	{
		sState *StatePtr;
	
		// Удаляем все стейты из стека
		while((StatePtr = m_StateParent) != NULL)
		{
			m_StateParent = StatePtr->Next;
			delete StatePtr;
		}

		// Помещаем (push) функцию в стек
		void Push(void (*Function)()
		{
			// Не помещаем нулевое значение
			if(Function != NULL)
			{
				// Выделяем память (allocate) под новый стейт и помещаем (push) его в стек.
				sState *StatePtr = new sState;
				StatePtr -> Next = m_StateParent;
				m_StateParent = StatePtr;
				StatePtr->Function = Function;
			}
		}

		BOOL Pop()
		{
			sState *StatePtr = m_StateParent;
			
			// Удаляем "голову" стека (если есть)
			if(StatePtr != NULL)
			{
				m_StateParent = StatePtr->Next;
				delete StatePtr;
			}
			
			// Возвращаем TRUE если ещё остались стейты и FALSE - если нет.
			if(m_StateParent == NULL)
			{return FALSE;}
			return TRUE;
		}
		
		BOOL Process()
		{
			// Если больше стейтов не осталось, возвращаем ошибку.
			if(m_StateParent == NULL) {return FALSE;}
	
			// Обрабатываем самый верхний стейт в стеке (если есть).
			m_StateParent->Function();
			return TRUE;
		}
	}
};

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

В вышеприведённом коде виден идентификатор Next. Такими снабжается код, оснащённый поддержкой связных списков. Эта технология широко распространена во многих языках программирования. В зависимости от реализации, каждый член связного списка обладает следующими идентификаторами, хранящими информацию о своих "соседях" по последовательности: Next, Previous, First, Last и т.д.

Внедрив класс cStateManager в свой код, ты можешь добавлять в его объект новые стейты по необходимости. И во время вызова функции рендеринга кадра можно вызывать метод Process только на функции нужного стейта. Вот пример применения класса cStateManager:
cStateManager SM;

// Макрос для упрощённого вызова окна сообщения
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Прототипы функций стейтов (должны строго следовать данному прототипу).
void Func1()
{
	MB("1");
	SM.Pop();
} void
	
Func2()
{
	MB("2");
	SM.Pop();
}

void Func3()
{
	MB("3");
	SM.Pop();
}

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPCSTR szCmdLine, int nCmdShow)
{
	SM.Push(Func1);
	SM.Push(Func2);
	SM.Push(Func3);
	while(SM.Process() == TRUE);
}

Вышеприведённая программа позволяет отслеживать состояние трёх стейтов, каждый из которых выдаёт окно сообщения (MessageBox) с определённой цифрой. Каждый стейт выдаёт сообщение прямо из стека и автоматически передаёт очередь другому стейту до тех пор, пока не закончатся все стейты и программа не будет завершена.
А теперь прикинь, если внедрить такой связный список (стек) в покадровую выборку сообщений (per-frame message pump). Допустим, что надо выдать сообщение игроку, но, как на зло, игра находится в стадии прорисовки внутриигровых объектов. В этом случае достаточно просто поместить функцию сообщения в стек и вызвать метод Process при подготовке следующего кадра.

Пример приложения с менеджером стейтов

Перед началом проверь, что у тебя установлены следующие программные компоненты:
    • MS Visual C++ 2010.
Инструкции по её установке ты найдёшь в разделе "Софт" нашего сайта.

Создаём Проект приложения

  • Создай пустой Проект с именем "State01".
Проект автоматически разместится внутри Решения с таким же именем. Весь процесс подробно расписан в статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта State01.
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp. Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 5 State-based processing Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)
**************************************************/

// Include files
#include <windows.h>
#include <stdio.h>

class cStateManager
{
  // A structure that stores a function pointer and linked list
	// Структура, хранящая указатель функции и связный список
  typedef struct sState {
    void  (*Function)();
    sState *Next;
  } sState;

  protected:
    sState *m_StateParent; // The top state in the stack
                           // (the head of the stack)

  public:
    cStateManager() { m_StateParent = NULL; }

    ~cStateManager() 
    {
      sState *StatePtr;

      // Remove all states from the stack
	  // Удалить все стейты из стека
      while((StatePtr = m_StateParent) != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }
    }

    // Push a function on to the stack
	// Поместить функцию в стек
    void Push(void (*Function)())
    {
      // Don't push a NULL value
      if(Function != NULL) {
        // Allocate a new state and push it on stack
		  // Создаём новый стейт и помещаем его в стек
        sState *StatePtr = new sState;
        StatePtr->Next = m_StateParent;
        m_StateParent = StatePtr;
        StatePtr->Function = Function;
      }
    }

    BOOL Pop()
    {
      sState *StatePtr = m_StateParent;

      // Remove the head of stack (if any)
	  // Удаляем "головной" стейт из стека (если есть)
      if(StatePtr != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }

      // return TRUE if more states exist, FALSE otherwise
	  // Если ещё остались стейты, возвращаем TRUE
	  // Иначе - FALSE
      if(m_StateParent == NULL)
        return FALSE;
      return TRUE;
    }

    BOOL Process()
    { 
      // return an error if no more states
		// Если стейтов больше не осталось, возвращаем ошибку
      if(m_StateParent == NULL)
        return FALSE;
      // Process the top-most state (if any)
	  // Обрабатываем самый верхний стейт в стеке
      m_StateParent->Function(); 
      return TRUE;
    }
};

cStateManager g_StateManager;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);

// Macro to ease the use of MessageBox function
// Макром, упрощающий вызов модального окна сообщения
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// State function prototypes - must follow this prototype!
void Func1() { MB("1"); g_StateManager.Pop(); }
void Func2() { MB("2"); g_StateManager.Pop(); }
void Func3() { MB("3"); g_StateManager.Pop(); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  g_StateManager.Push(Func1);
  g_StateManager.Push(Func2);
  g_StateManager.Push(Func3);
  while(g_StateManager.Process() == TRUE);

  return 0;
}

  • Сохрани Решение (Файл -> Сохранить все).
Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). Автор прогал на MS Visual C++ 6.0 (вышла в свет в 1998 г.) У нас IDE совсем другая (MS Visual C++ 2010 Express Edition). Нет, не с целью усложнения задачи. Просто MS Visual C++ 6.0 в своё время стоила 500 USD. Сейчас её, в принципе, можно поискать в торрентах. (За всё, что ты там скачаешь, администрация Igrocoder.ru ответственности не несёт!) Но использование платного/пиратского софта не соответствует концепции сайта Igrocoder.ru . Поэтому наш выбор:
  • Бесплатная IDE MS Visual C++ 2010 Express edition.

Готовим Проект State01 к компиляции

Для успешной компиляции изменим настройки (=свойства) текущего Проекта State01, созданного в MSVC++2010. При этом сам код из книги 2002 года останется нетронутым.
С момента выхода MS Visual C++ 6.0 прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки.

Выбираем многобайтовую кодировку

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, данный Проект мы настроим под многобайтовую кодировку.

Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект State01.
В Обозревателе решений видим: "Решение "State01"", а строкой ниже жирным шрифтом название Проекта (тоже State01).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта State01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

Отключаем инкрементную компоновку (incremental linking)

Инкрементная компоновка призвана сократить время компилирования. Но на деле её присутствие часто вызывает ошибки вроде этой:
Error LNK1123: сбой при преобразовании в COFF: файл недопустим или поврежден


Закрыть
warningОшибка LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt

В MS Visual C++ 2010 даже компиляция консольных приложений нередко завершается неудачей, а вместо исполняемого файла программист видит сообщение:
LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt.

ПРИЧИНА: MS Visual С++ 2010 "не нравится" версия .NET Framework(external link), установленная в операционной системе. В общих чертах, MS Visual С++ 2010 спрограммирована для работы под управлением .NET Framework 4.0 и сильно к нему привязана. Если точнее, к нему сильно привязана система инкрементной компоновки приложений (incremental linking), которая по умолчанию включена для всех создаваемых проектов.
Во время установки MS Visual C++ 2010 пытается установить свой "родной" .NET Framework 4.0, проверяя версию этой программной платформы, установленную в ОС на данный момент. Если версия .NET Framework ниже 4.0, то она обновляется до 4.0 и всё прекрасно компилируется. Если версия .NET Framework выше 4.0, то всё оставляется как есть: IDE успешно завершает установку, но при компиляции ВСЕХ приложений выскакивает данная ошибка. Более того, ошибка была замечена даже при наличии в системе .NET Framework версии 4.0, но отличающейся от "родной" припиской вроде "Beta" или "Release Candidate".

ВАРИАНТЫ РЕШЕНИЙ:
1. Отключить инкрементную линковку в опциях Проекта.
В главном меню MSVC++2010 выбираем: Проект->Свойства->Свойства конфигурации->Компоновщик(Linker)->Включить инкрементное построение (Incremental Linking). Данный пункт по умолчанию включен для всех новых проектов. Выставляем его в Нет (No). И жмём OK. Инкрементное построение заметно сокращает время компилирования больших проектов. Но в нашем случае его отсутствие некритично.
2. Удалить (переместить в другое место) утилиту cvtres.exe из каталога bin установленной MS Visual C++ 2010.
В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe . Официальное название данного приложения - Microsoft® Resource File To COFF Object Conversion Utility (утилита конвертации файлов двоичных ресурсов в Component Object File Format). Опытным путём установлено, что при линковке IDE "спотыкается" именно об него.
3. Удалить из системы все версии .NET Framework (включая языковые пакеты и всякие профайлеры, если есть) и саму MS Visual C++ 2010.
Всё вышеперечисленное можно без труда найти в меню "Программы и компоненты" (MS Windows Vista/7/8). Затем заново установить MS Visual C++ 2010. При этом автоматом установится .NET Framework 4.0, идущий с ней в наборе.

Третий пункт - самый долгий. На деле почти всегда хватает выполнения первых двух. Данным вопросом озадачивались ребята здесь: https://www.cyberforum.ru/cpp-beginners/thread637174.html?ysclid=l3akpmanrq(external link). После этого линковка (=компоновка) проходит идеально. Подобные "костыли" в IDE от Майков - не редкость. Можно предположить, что команда с головой ударилась в тестирование .NET-возможностей MSVC++2010, совсем забыв о Win32-направлении (либо признав его бесперспективным).


Отключим инкрементную компоновку в свойствах открытого Проекта. Для MS Visual C++ 2010 порядок следующий:
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь.
В Обозревателе решений видим: "Решение "..."", а строкой ниже жирным шрифтом название Проекта. Обычно с тем же названием (если не менял вручную).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение Нет (/INCREMENTAL:NO).
  • Жмём ОК.

Удаляем файл cvtres.exe

В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe
В Win32-кодинге он особо ни на что не влияет. Зато без него линковка идёт как по маслу.

Компилируем Проект State01

Наконец, наш тестовый Проект готов к компиляции.
  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 на клавиатуре.
Image
Скомпилированное .exe-приложение в нашем случае (Win7 x64) расположено по пути C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\State01\Debug .
После компилирования приложение Shell01 автоматически запустится и покажет поочерёдно 3 модальных (Popup) окна с кнопкой ОК. По её нажатию одно окно закрывается и тут же появляется другое. При этом отображаемый счётчик стейтов уменьшается на 1. Каждый стейт "выталкивает" (pops) сам себя из стека. Когда стейтов больше не осталось, программа определяет, что стек пуст и завершает работу.

Процессы (Processes)

Если ты используешь отдельные модули для обработки ввода, сети и звука, то, вместо того, чтобы вызывать каждый из них по отдельности, можно создать объект менеджер процессов (ProcessManager), который будет делать это за тебя. Вот пример его реализации:
class cProcessManager
{
	// Структура, хранящая указатель функции и связный список
	typedef struct sProcess
	{
		void (*Function)();
		sProcess *Next;
	} sProcess;

protected:
	sProcess *m_ProcessParent; // Самый верхний стейт в стеке ("голова" стека).
public:
	cProcessManager() {m_ProcessParent=NULL;}
	`cProcessManager()
	{
		sProcess *sProcessPtr;
		
		// Удаляем все процессы из стека
		while((ProcessPtr = m_ProcessParent) != NULL)
		{
			m_ProcessParent = ProcessPtr->Next;
			delete ProcessPtr;
		}
	}
	
	// Добавляем функцию в стек
	void Add(void (*Process)())
	{
		// Не добавляем нулевое значение
		if(Process != NULL)
		{
			// Выделяем память (allocate) под новый процесс и добавляем (push) его в стек.
			sProcess *ProcessPtr = new sProcess;
			ProcessPtr->Next = m_ProcessPtr;
			m_ProcessParent = ProcessPtr;
			ProcessPtr->Function = Process;
		}
	}
	
	// Обрабатываем все функции
	void Process()
	{
		sProcess *ProcessPtr = m_ProcessParent;
		
		while(ProcessPtr != NULL)
		{
			ProcessPtr->Function();
			ProcessPtr = ProcessPtr->Next;
		}
	}
};

Этот пример аналогичен cStateManager, который рассмотрен выше. Единственное отличие - cProcessManager может только добавлять процессы в стек. Он не удаляет их. Вот пример применения менеджера процессов:
cProcessManager PM;

// Макрос для упрощения создания диалоговых окон.
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Прототипы функций процессов (process functions).
void Func1()
{
	MB("1");
}

void Func2()
{
	MB("2");
}

void Func3()
{
	MB("3");
}

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int nCmdShow)
{
	PM.Add(Func1);
	PM.Add(Func2);
	PM.Add(Func3);
	PM.Process();
}


Image
Рис.2 Стек процессов состоит из часто вызываемых функций. Каждая функция, добавленная в менеджер процессов, выполняется при общем вызове cProcessManager::Process


Если заметил, при каждом вызове метода Process вызываются все процессы в стеке (См. Рис.2). Часто бывает очень полезно быстро вызывать определённые функции. Можно создать отдельные менеджеры процессов для разных ситуаций. К примеру, один обрабатывает сеть и пользовательский ввод (input), другой - работает над звуком и ещё чем-нибудь.

Пример приложения с менеджером процессов

Перед началом проверь, что у тебя установлены следующие программные компоненты:
  • MS Visual C++ 2010.
Инструкции по её установке ты найдёшь в разделе "Софт" нашего сайта.

Создаём Проект приложения

  • Создай пустой Проект с именем Process01.
Проект автоматически разместится внутри Решения с таким же именем. Весь процесс подробно расписан в статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Process01.
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp. Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 5 Stack Process Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)
**************************************************/

// Include files
#include <windows.h>
#include <stdio.h>

class cProcessManager
{
  // A structure that stores a function pointer and linked list
	// Структура, хранящая указатель функции и связный список
  typedef struct sProcess {
    void  (*Function)();
    sProcess *Next;
  } sProcess;

  protected:
    sProcess *m_ProcessParent; // The top state in the stack
                               // (the head of the stack)

  public:
    cProcessManager() { m_ProcessParent = NULL; }

    ~cProcessManager() 
    {
      sProcess *ProcessPtr;

      // Remove all processes from the stack
	  // Удаляем все процессы из стека
      while((ProcessPtr = m_ProcessParent) != NULL) {
        m_ProcessParent = ProcessPtr->Next;
        delete ProcessPtr;
      }
    }

    // Add function on to the stack
	// Помещаем функцию в стек
    void Add(void (*Process)())
    {
      // Don't push a NULL value
		// Пропускаем нулевые значения
      if(Process != NULL) {
        // Allocate a new process and push it on stack
		  // Создаём новый процесс и добавляем его в стек
        sProcess *ProcessPtr = new sProcess;
        ProcessPtr->Next = m_ProcessParent;
        m_ProcessParent = ProcessPtr;
        ProcessPtr->Function = Process;
      }
    }

    // Process all functions
	// Выполняем все функции
    void Process()
    { 
      sProcess *ProcessPtr = m_ProcessParent;

      while(ProcessPtr != NULL) {
        ProcessPtr->Function();
        ProcessPtr = ProcessPtr->Next;
      }
    }
};

cProcessManager g_ProcessManager;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);

// Macro to ease the use of MessageBox function
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Processfunction prototypes - must follow this prototype!
void Func1() { MB("1"); }
void Func2() { MB("2"); }
void Func3() { MB("3"); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  g_ProcessManager.Add(Func1);
  g_ProcessManager.Add(Func2);
  g_ProcessManager.Add(Func3);
  g_ProcessManager.Process();
  g_ProcessManager.Process();

  return 0;
}

  • Сохрани Решение (Файл -> Сохранить все).
Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). Автор прогал на MS Visual C++ 6.0 (вышла в свет в 1998 г.) У нас IDE совсем другая (MS Visual C++ 2010 Express Edition). Нет, не с целью усложнения задачи. Просто MS Visual C++ 6.0 в своё время стоила 500 USD. Сейчас её, в принципе, можно поискать в торрентах. (За всё, что ты там скачаешь, администрация Igrocoder.ru ответственности не несёт!) Но использование платного/пиратского софта не соответствует концепции сайта Igrocoder.ru . Поэтому наш выбор:
  • Бесплатная IDE MS Visual C++ 2010 Express edition.

Готовим Проект Process01 к компиляции

Для успешной компиляции изменим настройки (=свойства) текущего Проекта Shell01, созданного в MSVC++2010. При этом сам код из книги 2002 года останется нетронутым.
С момента выхода MS Visual C++ 6.0 прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки.

Выбираем многобайтовую кодировку

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, данный Проект мы настроим под многобайтовую кодировку.

Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект Process01.
В Обозревателе решений видим: "Решение "Process01"", а строкой ниже жирным шрифтом название Проекта (тоже Process01).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта Process01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

Отключаем инкрементную компоновку (incremental linking)

Инкрементная компоновка призвана сократить время компилирования. Но на деле её присутствие часто вызывает ошибки вроде этой:
Error LNK1123: сбой при преобразовании в COFF: файл недопустим или поврежден


Закрыть
warningОшибка LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt

В MS Visual C++ 2010 даже компиляция консольных приложений нередко завершается неудачей, а вместо исполняемого файла программист видит сообщение:
LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt.

ПРИЧИНА: MS Visual С++ 2010 "не нравится" версия .NET Framework(external link), установленная в операционной системе. В общих чертах, MS Visual С++ 2010 спрограммирована для работы под управлением .NET Framework 4.0 и сильно к нему привязана. Если точнее, к нему сильно привязана система инкрементной компоновки приложений (incremental linking), которая по умолчанию включена для всех создаваемых проектов.
Во время установки MS Visual C++ 2010 пытается установить свой "родной" .NET Framework 4.0, проверяя версию этой программной платформы, установленную в ОС на данный момент. Если версия .NET Framework ниже 4.0, то она обновляется до 4.0 и всё прекрасно компилируется. Если версия .NET Framework выше 4.0, то всё оставляется как есть: IDE успешно завершает установку, но при компиляции ВСЕХ приложений выскакивает данная ошибка. Более того, ошибка была замечена даже при наличии в системе .NET Framework версии 4.0, но отличающейся от "родной" припиской вроде "Beta" или "Release Candidate".

ВАРИАНТЫ РЕШЕНИЙ:
1. Отключить инкрементную линковку в опциях Проекта.
В главном меню MSVC++2010 выбираем: Проект->Свойства->Свойства конфигурации->Компоновщик(Linker)->Включить инкрементное построение (Incremental Linking). Данный пункт по умолчанию включен для всех новых проектов. Выставляем его в Нет (No). И жмём OK. Инкрементное построение заметно сокращает время компилирования больших проектов. Но в нашем случае его отсутствие некритично.
2. Удалить (переместить в другое место) утилиту cvtres.exe из каталога bin установленной MS Visual C++ 2010.
В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe . Официальное название данного приложения - Microsoft® Resource File To COFF Object Conversion Utility (утилита конвертации файлов двоичных ресурсов в Component Object File Format). Опытным путём установлено, что при линковке IDE "спотыкается" именно об него.
3. Удалить из системы все версии .NET Framework (включая языковые пакеты и всякие профайлеры, если есть) и саму MS Visual C++ 2010.
Всё вышеперечисленное можно без труда найти в меню "Программы и компоненты" (MS Windows Vista/7/8). Затем заново установить MS Visual C++ 2010. При этом автоматом установится .NET Framework 4.0, идущий с ней в наборе.

Третий пункт - самый долгий. На деле почти всегда хватает выполнения первых двух. Данным вопросом озадачивались ребята здесь: https://www.cyberforum.ru/cpp-beginners/thread637174.html?ysclid=l3akpmanrq(external link). После этого линковка (=компоновка) проходит идеально. Подобные "костыли" в IDE от Майков - не редкость. Можно предположить, что команда с головой ударилась в тестирование .NET-возможностей MSVC++2010, совсем забыв о Win32-направлении (либо признав его бесперспективным).


Отключим инкрементную компоновку в свойствах открытого Проекта. Для MS Visual C++ 2010 порядок следующий:
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь.
В Обозревателе решений видим: "Решение "..."", а строкой ниже жирным шрифтом название Проекта. Обычно с тем же названием (если не менял вручную).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение Нет (/INCREMENTAL:NO).
  • Жмём ОК.

Удаляем файл cvtres.exe

В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe
В Win32-кодинге он особо ни на что не влияет. Зато без него линковка идёт как по маслу.

Компилируем Проект Shell01

Наконец, наш тестовый Проект готов к компиляции.
  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 на клавиатуре.
Image
Скомпилированное .exe-приложение в нашем случае (Win7 x64) расположено по пути C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\Process01\Debug .
После компилирования приложение Process01 автоматически запустится и покажет поочерёдно 3 модальных (Popup) окна с кнопкой ОК. По её нажатию одно окно закрывается и тут же появляется другое. При этом отображаемый счётчик процессов уменьшается на 1. Стэк процессов состоит из часто вызываемых функций. Каждая функция, добавленная в менеджер процессов, выполняется при вызове метода cProcessManager::Process .
Закрыть
noteОбрати внимание

Всякиий раз при вызове процесса из менеджера процессов вызываются все процессы в стеке. Это незаменимо для быстрого вызова частовызываемых функций.


Источники


1. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press, 2002


Последние изменения страницы Вторник 21 / Июнь, 2022 20:47:09 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++