Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.4 Добавляем поддержку менеджеров ресурсов (ResourceManagement.h)
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.4 Добавляем поддержку менеджеров ресурсов (ResourceManagement.h)


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

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

Рис. 1 Сейчас Проект нашего движка выглядит так
Рис. 1 Сейчас Проект нашего движка выглядит так

Сейчас в Проекте нашего движка всего 3 файла: Engine.h, Engine.cpp и LinkedList.h, которые мы создали в предыдущих главах (см. Рис. 1).

Создаём ResourceManagement.h

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

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

Напомним, что директива #include "ResourceManagement.h" (ссылка на заголовок) уже указана в Engine.h (Проверь!). Таким образом, для того, чтобы воспользоваться всем функционалом различных заголовочных файлов (в том числе ResourceManagement.h), достаточно в любом из файлов исходного кода (.cpp) указать всего 1 директиву: #include Engine.h. Это соответствует концепции единой точки контакта.

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы".
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "ResourceManagement.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле ResourceManagement.h набираем следующий код:
ResourceManagement.h
//-----------------------------------------------------------------------------
// Используется для управления созданием и уничтожением любых ресурсов.
// Менеджер ресурсов (ResourceManager) предотвратит избыточность
// и, связанный с этим, чрезмерный расход памяти.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef RESOURCE_MANAGEMENT_H
#define RESOURCE_MANAGEMENT_H

//-----------------------------------------------------------------------------
// Resource Class
//-----------------------------------------------------------------------------
template< class Type > class Resource
{
public:
	//-------------------------------------------------------------------------
	// The resource class constructor.
	//-------------------------------------------------------------------------
	Resource( char *name, char *path = "./" )
	{
		// Сохраняем имя.
		if( name != NULL )
		{
			m_name = new char[strlen( name ) + 1];
			memcpy( m_name, name, ( strlen( name ) + 1 ) * sizeof( char ) );
		}

		// Сохраняем путь.
		if( path != NULL )
		{
			m_path = new char[strlen( path ) + 1];
			memcpy( m_path, path, ( strlen( path ) + 1 ) * sizeof( char ) );
		}

		// Создаём имя файла (например при сохранении ресурса).
		if( name != NULL && path != NULL )
		{
			m_filename = new char[strlen( name ) + strlen( path ) + 1];
			sprintf( m_filename, "%s%s", path, name );
		}

		// Стартуем счётчик ссылок на данный ресурс.
		m_refCount = 1;
	}

	//-------------------------------------------------------------------------
	// The resource class destructor.
	//-------------------------------------------------------------------------
	virtual ~Resource()
	{
		SAFE_DELETE_ARRAY( m_name );
		SAFE_DELETE_ARRAY( m_path );
		SAFE_DELETE_ARRAY( m_filename );
	}

	//-------------------------------------------------------------------------
	// Возвращает имя ресурса.
	//-------------------------------------------------------------------------
	char *GetName()
	{
		return m_name;
	}

	//-------------------------------------------------------------------------
	// Возвращает путь к ресурсу.
	//-------------------------------------------------------------------------
	char *GetPath()
	{
		return m_path;
	}

	//-------------------------------------------------------------------------
	// Возвращает имя файла ресурса.
	//-------------------------------------------------------------------------
	char *GetFilename()
	{
		return m_filename;
	}

	//-------------------------------------------------------------------------
	// Инкрементирует (увеличивает на 1) счётчик ссылок на ресурс.
	//-------------------------------------------------------------------------
	void IncRef()
	{
		m_refCount++;
	}

	//-------------------------------------------------------------------------
	// Декрементирует (уменьшает на 1) счётчик ссылок на ресурс.
	//-------------------------------------------------------------------------
	void DecRef()
	{
		m_refCount--;
	}

	//-------------------------------------------------------------------------
	// Возвращает значение счётчика ссылок на ресурс
	//-------------------------------------------------------------------------
	unsigned long GetRefCount()
	{
		return m_refCount;
	}

private:
	char *m_name; // Name of the resource.
	char *m_path; // Path to the resource.
	char *m_filename; // Filename (name + path) of the resource.
	unsigned long m_refCount; // Reference count.
};

//-----------------------------------------------------------------------------
// Resource Manager Class
//-----------------------------------------------------------------------------
template< class Type > class ResourceManager
{
public:
	//-------------------------------------------------------------------------
	// The resource manager class constructor.
	//-------------------------------------------------------------------------
	ResourceManager( void (*CreateResourceFunction)( Type **resource, char *name, char *path ) = NULL )
	{
		m_list = new LinkedList< Type >;

		CreateResource = CreateResourceFunction;
	}

	//-------------------------------------------------------------------------
	// The resource manager class destructor.
	//-------------------------------------------------------------------------
	~ResourceManager()
	{
		SAFE_DELETE( m_list );
	}

	//-------------------------------------------------------------------------
	// Добавляет новый ресурс в менеджер.
	//-------------------------------------------------------------------------
	Type *Add( char *name, char *path = "./" )
	{
		// Проверяем, что список, имя ресурса и его путь верны.
		if( m_list == NULL || name == NULL || path == NULL )
			return NULL;

		// Если элемент уже сушествует, возвращаем указатель на него.
		Type *element = GetElement( name, path );
		if( element != NULL )
		{
			element->IncRef();
			return element;
		}

		// Создаём ресурс, предпочтительно через функции, специфичные для приложения,
		// если таковые доступны.
		Type *resource = NULL;
		if( CreateResource != NULL )
			CreateResource( &resource, name, path );
		else
			resource = new Type( name, path );

		// Добавляем новый ресурс в менеджер и возвращаем указатель на него.
		return m_list->Add( resource );
	}

	//-------------------------------------------------------------------------
	// Удаляет данный ресурс из менеджера.
	//-------------------------------------------------------------------------
	void Remove( Type **resource )
	{
		// Проверяем, что удаляемый ресурс и его список существуют.
		if( *resource == NULL || m_list == NULL )
			return;

		// Декрементируем счётчик ссылок на ресурс.
		(*resource)->DecRef();

		// Если ресурс более не используется, удаляем его.
		if( (*resource)->GetRefCount() == 0 )
			m_list->Remove( resource );
	}

	//-------------------------------------------------------------------------
	// Опустошает список ресурсов.
	//-------------------------------------------------------------------------
	void EmptyList()
	{
		if( m_list != NULL )
			m_list->Empty();
	}

	//-------------------------------------------------------------------------
	// Возвращает список ресурсов.
	//-------------------------------------------------------------------------
	LinkedList< Type > *GetList()
	{
		return m_list;
	}

	//-------------------------------------------------------------------------
	// Возвращает ресурс по имени файла.
	//-------------------------------------------------------------------------
	Type *GetElement( char *name, char *path = "./" )
	{
		// Проверяем, что имя, путь ресурса и его список верны и не пусты.
		if( name == NULL || path == NULL || m_list == NULL )
			return NULL;
		if( m_list->GetFirst() == NULL )
			return NULL;

		// Итерируем через весь список в поисках указанного ресурса.
		m_list->Iterate( true );
		while( m_list->Iterate() )
			if( strcmp( m_list->GetCurrent()->GetName(), name ) == 0 )
				if( strcmp( m_list->GetCurrent()->GetPath(), path ) == 0 )
					return m_list->GetCurrent();

		// Возвращает NULL, если ресурс не найден.
		return NULL;
	}

private:
	LinkedList< Type > *m_list; // Связный список (Linked list) ресурсов.

	void (*CreateResource)( Type **resource, char *name, char *path ); // Специфичная для приложения функция создания ресурса.
};

#endif

  • Сохрани Решение (Файл -> Сохранить все).

Исследуем код ResourceManagement.h

Класс Resource

Рассмотрим класс Resource, который инкапсулирует основные детали ( = создан для хранения базовой информации) любого ресурса:

Фрагмент файла ResourceManagement.h
...
//-----------------------------------------------------------------------------
// Resource Class
//-----------------------------------------------------------------------------
template< class Type > class Resource
{
public:
	//-------------------------------------------------------------------------
	// The resource class constructor.
	//-------------------------------------------------------------------------
	Resource( char *name, char *path = "./" )
	{
		// Сохраняем имя.
		if( name != NULL )
		{
			m_name = new char[strlen( name ) + 1];
			memcpy( m_name, name, ( strlen( name ) + 1 ) * sizeof( char ) );
		}

		// Сохраняем путь.
		if( path != NULL )
		{
			m_path = new char[strlen( path ) + 1];
			memcpy( m_path, path, ( strlen( path ) + 1 ) * sizeof( char ) );
		}

		// Создаём имя файла (например при сохранении ресурса).
		if( name != NULL && path != NULL )
		{
			m_filename = new char[strlen( name ) + strlen( path ) + 1];
			sprintf( m_filename, "%s%s", path, name );
		}

		// Стартуем счётчик ссылок на данный ресурс.
		m_refCount = 1;
	}

	//-------------------------------------------------------------------------
	// The resource class destructor.
	//-------------------------------------------------------------------------
	virtual ~Resource()
	{
		SAFE_DELETE_ARRAY( m_name );
		SAFE_DELETE_ARRAY( m_path );
		SAFE_DELETE_ARRAY( m_filename );
	}

	//-------------------------------------------------------------------------
	// Возвращает имя ресурса.
	//-------------------------------------------------------------------------
	char *GetName()
	{
		return m_name;
	}

	//-------------------------------------------------------------------------
	// Возвращает путь к ресурсу.
	//-------------------------------------------------------------------------
	char *GetPath()
	{
		return m_path;
	}

	//-------------------------------------------------------------------------
	// Возвращает имя файла ресурса.
	//-------------------------------------------------------------------------
	char *GetFilename()
	{
		return m_filename;
	}

	//-------------------------------------------------------------------------
	// Инкрементирует (увеличивает на 1) счётчик ссылок на ресурс.
	//-------------------------------------------------------------------------
	void IncRef()
	{
		m_refCount++;
	}

	//-------------------------------------------------------------------------
	// Декрементирует (уменьшает на 1) счётчик ссылок на ресурс.
	//-------------------------------------------------------------------------
	void DecRef()
	{
		m_refCount--;
	}

	//-------------------------------------------------------------------------
	// Возвращает значение счётчика ссылок на ресурс
	//-------------------------------------------------------------------------
	unsigned long GetRefCount()
	{
		return m_refCount;
	}

private:
	char *m_name; // Name of the resource.
	char *m_path; // Path to the resource.
	char *m_filename; // Filename (name + path) of the resource.
	unsigned long m_refCount; // Reference count.
};
...


Из исходного кода видно, что в базовую информацию о ресурсе входит:

  • Имя файла
  • Путь к файлу
  • Полное имя файла (то есть, путь + имя файла, присоединённое к нему).

Ресурс также использует счётчик ссылок (reference count), который очень скоро мы увидим в деле. Ещё раз напомним, что Resource - это базовый класс, применимый для любого ресурса. Для создания нового ресурса, например из звукового файла, необходимо создать новый класс, который ответвляется от класса Resource. Рассмотрим следующий пример (прочти и разберись):

Пример создания саунд-ресурса
...
class Sound : public Resource
{
public:
	Sound( char *name, char *path = "./" );
	virtual	~Sound();
}
...


Всё, что необходимо сделать в конструкторе класса Sound, это передать имя (параметр name) и путь к файлу (параметр pathname) в конструктор класса Resource, чтобы данный класс мог сам настроить себя для корректной работы с данным ресурсом:

Пример создания саунд-ресурса
...
Sound::Sound( char *name, char *path);
	: Resource <Sound>(name, path);
{
// Загружаем звуковой ресурс
}
...

Сразу после этого ты можешь использовать вновь созданный ресурс и получить доступ ко всем основным его деталям, предосталяемым классом Resource (имя, путь и имя+путь). Конечно, это уже тебе как программеру решать, каким именно способом загружать или уничтожать тот или иной ресурс, и что с ним делать дальше после загрузки. Заметим лишь, что по-настоящему весь потенциал класса Resource раскрывается лишь в комбинации с классом ResourceManager, о котором пойдёт речь далее.

Класс ResourceManager

  • Является ядром менеджера ресурсов.
  • - это то, что действительно делает систему управления ресурсами столь эффективной.


В файле ResourceManager.h, сразу после объявления класса Resource, ты найдёшь объявление и реализацию класса ResourceManager, скомбинированные внутри класса (то есть объявление и реализация самого класса, а также различных служебных функций размещены внутри класса):

Фрагмент файла ResourceManagement.h
...
//-----------------------------------------------------------------------------
// Resource Manager Class
//-----------------------------------------------------------------------------
template< class Type > class ResourceManager
{
public:
	//-------------------------------------------------------------------------
	// The resource manager class constructor.
	//-------------------------------------------------------------------------
	ResourceManager( void (*CreateResourceFunction)( Type **resource, char *name, char *path ) = NULL )
	{
		m_list = new LinkedList< Type >;

		CreateResource = CreateResourceFunction;
	}

	//-------------------------------------------------------------------------
	// The resource manager class destructor.
	//-------------------------------------------------------------------------
	~ResourceManager()
	{
		SAFE_DELETE( m_list );
	}

	//-------------------------------------------------------------------------
	// Добавляет новый ресурс в менеджер.
	//-------------------------------------------------------------------------
	Type *Add( char *name, char *path = "./" )
	{
		// Проверяем, что список, имя ресурса и его путь верны.
		if( m_list == NULL || name == NULL || path == NULL )
			return NULL;

		// Если элемент уже сушествует, возвращаем указатель на него.
		Type *element = GetElement( name, path );
		if( element != NULL )
		{
			element->IncRef();
			return element;
		}

		// Создаём ресурс, предпочтительно через функции, специфичные для приложения,
		// если таковые доступны.
		Type *resource = NULL;
		if( CreateResource != NULL )
			CreateResource( &resource, name, path );
		else
			resource = new Type( name, path );

		// Добавляем новый ресурс в менеджер и возвращаем указатель на него.
		return m_list->Add( resource );
	}

	//-------------------------------------------------------------------------
	// Удаляет данный ресурс из менеджера.
	//-------------------------------------------------------------------------
	void Remove( Type **resource )
	{
		// Проверяем, что удаляемый ресурс и его список существуют.
		if( *resource == NULL || m_list == NULL )
			return;

		// Декрементируем счётчик ссылок на ресурс.
		(*resource)->DecRef();

		// Если ресурс более не используется, удаляем его.
		if( (*resource)->GetRefCount() == 0 )
			m_list->Remove( resource );
	}

	//-------------------------------------------------------------------------
	// Опустошает список ресурсов.
	//-------------------------------------------------------------------------
	void EmptyList()
	{
		if( m_list != NULL )
			m_list->Empty();
	}

	//-------------------------------------------------------------------------
	// Возвращает список ресурсов.
	//-------------------------------------------------------------------------
	LinkedList< Type > *GetList()
	{
		return m_list;
	}

	//-------------------------------------------------------------------------
	// Возвращает ресурс по имени файла.
	//-------------------------------------------------------------------------
	Type *GetElement( char *name, char *path = "./" )
	{
		// Проверяем, что имя, путь ресурса и его список верны и не пусты.
		if( name == NULL || path == NULL || m_list == NULL )
			return NULL;
		if( m_list->GetFirst() == NULL )
			return NULL;

		// Итерируем через весь список в поисках указанного ресурса.
		m_list->Iterate( true );
		while( m_list->Iterate() )
			if( strcmp( m_list->GetCurrent()->GetName(), name ) == 0 )
				if( strcmp( m_list->GetCurrent()->GetPath(), path ) == 0 )
					return m_list->GetCurrent();

		// Return NULL if the resource was not found.
		return NULL;
	}

private:
	LinkedList< Type > *m_list; // Связный список (Linked list) ресурсов.

	void (*CreateResource)( Type **resource, char *name, char *path ); // Специфичная для приложения функция создания ресурса.
};
...


Судя по ключевому слову template (англ. "шаблон"), класс ResourceManager также является шаблонным классом, который требует указания типа при создании своего экземпляра (инстанса). А это означает, что для каждого вида ресурсов, которые ты планируешь использовать в своей игре, тебе необходимо создать по одному менеджеру ресурсов. Например, если ты задумал в игре поддержку звуковых эффектов, текстур (куда же без них...) и мешей (полигональных сеток), то в этом случае тебе необходимо создать 3 отдельных менеджера ресурсов (по одному на каждый вид ресурсов). Когда создаётся новый экземпляр класса ResourceManager, вызывается его конструктор, который просто подготавливает менеджер ресурсов, создавая связный список с указанным типом ресурсов.

Рис. 2 Взаимоотношения между ресурсами, менеджерами ресурсов, движком и приложением
Рис. 2 Взаимоотношения между ресурсами, менеджерами ресурсов, движком и приложением

Конструктор также принимает указатель на функцию CreateResourceFunction, которая на деле является функцией обратного вызова (call-back function), которая вызывается всякий раз, когда в менеджер ресурсов поступает команда добавить (Add, здесь это то же самое что создать) новый ресурс. Когда ты вызываешь функцию Add, менеджер ресурсов создаёт новый ресурс, используя укзанное имя файла и путь к нему. Но в то же время, если функции обратного вызова CreateResourceFunction присвоено какое-либо ненулевое значение (или указатель), то она, в свою очередь, будет обязательно вызвана функцией Add. Такой подход позволяет тебе назначить любую произвольную функцию в коде, специфичном для приложения (application specific code), для создания ресурсов определённого типа. А это означает, что таким образом, ты сможешь создавать ресурсы, которые специфичны только для твоей игры (и не предусмотрены функционалом движка)! Если тебе не требуется в игре поддержки загрузки каких-либо специфичных ресурсов, не поддерживаемых движком, ты можешь спокойно присвоить функции CreateResourceFunction значение NULL. В этом случае менеджер ресурсов будет использовать код загрузки ресурсов по умолчанию, предусмотренный в движке. Ты увидишь применение этой функции обратного вызова позднее, когда мы будем создавать игру. А сейчас посмотри на Рис. 2, который показывает взаимосвязь между ресурсами, менеджерами ресурсов, движком и приложением. Стрелками указано, кто у кого запрашивает информацию.

Функция Remove позволяет удалить любой ресурс из менеджера ресурсов. Именно здесь счётчик ссылок (reference count) выходит на первый план.
Вообще, менеджер ресурсов спроектирован так, чтобы исключить ситуации повторной загрузки одних и тех же ресурсов, помогая избежать чрезмерного расхода памяти. При добавлении нового ресурса, счётчик ссылок на него увеличивается на единицу. Если ты попытаешься загрузить тот же самый ресурс ещё раз, менеджер ресурсов, вместо того, чтобы заново загружать файл ресурса, просто возвращает указатель на загруженную ранее копию ресурса и затем инкрементирует счётчик ссылок ещё на единицу (т.е. число ссылок на этот ресурс становится равен 2). Таким образом, всякий раз, когда ты запрашиваешь у менеджера ресурсов копию определённого ресурса, менеджер ресурсов просто даёт тебе указатель на уже имеющийся (т.к. ранее он уже был загружен в память) ресурс и инкрементирует (увеличивает на единицу) счётчик ссылок.

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

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

Именно с помощью счётчика ссылок менеджер ресурсов отслеживает, сколько экземплляров определённого ресурса используется в игре в данный момент. Всякий раз, когда приложение заканчивает работу с ресурсом, оно говорит менеджеру ресурсов удалить ресурс. Менеджер ресурсов в этом случае декрементирует (уменьшает на 1) счётчик ресурсов и затем проверяет, не равно ли это значение 0 (нулю). Когда счётчик ссылок становится равен нулю, это означает что уже ничто не использует ресурс и его можно спокойно удалить из памяти.

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

Функция EmptyList уничтожит все ресурсы, хранимые в менеджере ресурсов вне зависимости от значений их счётчиков ссылок.


Последние 2 функции GetList и GetElement возвращают указатель ресурсов во внутренний связный список менеджера ресурсов. При этом кроме указателя на ресурс, также возвращаются имя ресурса и путь к нему. Функция GetList полезна, когда необходимо произвести "ручную" итерацию содержимого менеджера ресурсов либо внести какие-либо изменения.
В любом случае, "играя" с ресурсами в связном списке, будь осторожен и не изменяй имена ресурсов, пути к ним или значение счётчика ссылок на них, так как это ррезко снижает эффективность менеджер ресурсов и может привести к утечкам памяти.

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


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.4 Добавляем поддержку менеджеров ресурсов (ResourceManagement.h)

Contributors to this page: slymentat .
Последнее изменение страницы Вторник 29 / Ноябрь, 2016 13:55:32 MSK автор slymentat.

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

No records to display