Загрузка...
 
Печать
Кодим 3D FPS DX9

1.4 Добавляем поддержку менеджеров ресурсов




Intro

Ресурсы часто называют ассетами (англ. asset), которые представляют собой графические, звуковые и компоненты, используемые игрой.1
Ресурсом может быть что угодно, начиная с текстуры или меша (сетки) и заканчивая звуками и скриптами. Все движки должны иметь возможность обрабатывать ресурсы в той или иной степени, так как без них ты не сможешь представить игроку интерактивное окружение и яркие впечатления от игры. Представь себе игру без звуков, скриптов, мешей или текстур. Это будет очень пустая игра на самом деле. Наш движок не является исключением, и поэтому нам нужен способ управлять игровыми ресурсами.
Очевидно, что методы, которые ты используешь для загрузки звукового файла будут отличаться от методов загрузки и процессинга файла меша (трёхмерной сетки) или любого другого вида файлов. Единственное, что у всех этих ресурсов будет общее, так это то, что все они основаны на обычных файлах, и перед использованием обязательно должны быть загружены в память, а затем позднее удалены из неё. Таким образом, мы можем использовать эту схожесть в своих целях и создать класс Resource, который будет хранить некоторую базовую информацию об обобщённом (generic) ресурсе, под которым подразумевается абсолютно любой ресурс, независимо от его вида.
Сейчас в Проекте нашего движка всего 3 файла: Engine.h, Engine.cpp и LinkedList.h, которые мы создали в предыдущих главах.

Добавляем ResourceManagement.h (Проект Engine)

В заголовочном файле ResourceManagement.h будут содержаться объявления классов и функций, ответственных за создание и управление внутриигровыми менеджерами ресурсов (их может быть несколько).
ОК, приступаем.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" (Header Files).
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи ResourceManagement.h.
  • Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле ResourceManagement.h набираем следующий код:
ResourceManagement.h
//-----------------------------------------------------------------------------
//	File: 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 = "./" )
	{
		// Store the name.
		// Сохраняем имя.
		if( name != NULL )
		{
			m_name = new char[strlen( name ) + 1];
			memcpy( m_name, name, ( strlen( name ) + 1 ) * sizeof( char ) );
		}

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

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

		// Start the reference count.
		// Стартуем счётчик ссылок.
		m_refCount = 1;
	}

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

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

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

	//-------------------------------------------------------------------------
	// Returns the filename of the resource.
	// Возвращает имя файла + путь до него.
	//-------------------------------------------------------------------------
	char *GetFilename()
	{
		return m_filename;
	}

	//-------------------------------------------------------------------------
	// Increments the resource's reference count.
	// Увеличивает счётчик ссылок на единицу.
	//-------------------------------------------------------------------------
	void IncRef()
	{
		m_refCount++;
	}

	//-------------------------------------------------------------------------
	// Decrements the resource's reference count.
	// Уменьшает счётчик ссылок на единицу.
	//-------------------------------------------------------------------------
	void DecRef()
	{
		m_refCount--;
	}

	//-------------------------------------------------------------------------
	// Returns the resource's reference count.
	// Возвращает текущее значение счётчика ссылок.
	//-------------------------------------------------------------------------
	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 );
	}

	//-------------------------------------------------------------------------
	// Adds a new resource to the manager.
	// Добавляет новый ресурс в менеджер ресурсов.
	//-------------------------------------------------------------------------
	Type *Add( char *name, char *path = "./" )
	{
		// Ensure the list, the resource name, and its path are valid.
		// Проверяем корректность списка, имени ресурса и пути к нему.
		if( m_list == NULL || name == NULL || path == NULL )
			return NULL;

		// If the element already exists, then return a pointer to it.
		// Если элемент уже есть в списке, возвращаем указатель на него.
		Type *element = GetElement( name, path );
		if( element != NULL )
		{
			element->IncRef();
			return element;
		}

		// Create the resource, preferably through the application specific
		// function if it is available.
		// Создаём ресурс. Желательно через функцию, специфичную для приложения
		// (по возможности).
		Type *resource = NULL;
		if( CreateResource != NULL )
			CreateResource( &resource, name, path );
		else
			resource = new Type( name, path );

		// Add the new resource to the manager and return a pointer to it.ъ
		// Добавляем новый ресурс в список ресурсов и возвращаем указатель на него.
		return m_list->Add( resource );
	}

	//-------------------------------------------------------------------------
	// Removes the given resource from the manager.
	// Удалеят данный ресурс из менеджера.
	//-------------------------------------------------------------------------
	void Remove( Type **resource )
	{
		// Ensure the resource to be removed and the list is valid.
		// Проверяем валидность списка и удаляемого ресурса.
		if( *resource == NULL || m_list == NULL )
			return;

		// Decrement the resource's reference count.
		// Уменьшаем на единицу счётчик ссылок
		// на данный ресурс.
		(*resource)->DecRef();

		// If the resource is no long being used then destroy it.
		// Если счётчик ссылок на ресурс равен нулю,
		// то данный ресурс больше не используется и его можно
		// смело удалять.
		if( (*resource)->GetRefCount() == 0 )
			m_list->Remove( resource );
	}

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

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

	//-------------------------------------------------------------------------
	// Returns a resource by its filename.
	// Возвращает ресурс по его имени.
	//-------------------------------------------------------------------------
	Type *GetElement( char *name, char *path = "./" )
	{
		// Ensure the name and path are valid, and the list is valid and not empty.
		// Проверяем, что имя, путь и список не пусты.
		if( name == NULL || path == NULL || m_list == NULL )
			return NULL;
		if( m_list->GetFirst() == NULL )
			return NULL;

		// Iterate the list looking for the specified resource.
		// Итерируем через список в поисках определённого ресурса.
		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.
		// Возвращаем NULL, если данный ресурс не найден.
		return NULL;
	}

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

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

#endif

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

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


Класс Resource

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

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

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

		// Start the reference count.
		// Стартуем счётчик ссылок.
		m_refCount = 1;
	}

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

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

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

	//-------------------------------------------------------------------------
	// Returns the filename of the resource.
	// Возвращает имя файла + путь до него.
	//-------------------------------------------------------------------------
	char *GetFilename()
	{
		return m_filename;
	}

	//-------------------------------------------------------------------------
	// Increments the resource's reference count.
	// Увеличивает счётчик ссылок на единицу.
	//-------------------------------------------------------------------------
	void IncRef()
	{
		m_refCount++;
	}

	//-------------------------------------------------------------------------
	// Decrements the resource's reference count.
	// Уменьшает счётчик ссылок на единицу.
	//-------------------------------------------------------------------------
	void DecRef()
	{
		m_refCount--;
	}

	//-------------------------------------------------------------------------
	// Returns the resource's reference count.
	// Возвращает текущее значение счётчика ссылок.
	//-------------------------------------------------------------------------
	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 );
	}

	//-------------------------------------------------------------------------
	// Adds a new resource to the manager.
	// Добавляет новый ресурс в менеджер ресурсов.
	//-------------------------------------------------------------------------
	Type *Add( char *name, char *path = "./" )
	{
		// Ensure the list, the resource name, and its path are valid.
		// Проверяем корректность списка, имени ресурса и пути к нему.
		if( m_list == NULL || name == NULL || path == NULL )
			return NULL;

		// If the element already exists, then return a pointer to it.
		// Если элемент уже есть в списке, возвращаем указатель на него.
		Type *element = GetElement( name, path );
		if( element != NULL )
		{
			element->IncRef();
			return element;
		}

		// Create the resource, preferably through the application specific
		// function if it is available.
		// Создаём ресурс. Желательно через функцию, специфичную для приложения
		// (по возможности).
		Type *resource = NULL;
		if( CreateResource != NULL )
			CreateResource( &resource, name, path );
		else
			resource = new Type( name, path );

		// Add the new resource to the manager and return a pointer to it.ъ
		// Добавляем новый ресурс в список ресурсов и возвращаем указатель на него.
		return m_list->Add( resource );
	}

	//-------------------------------------------------------------------------
	// Removes the given resource from the manager.
	// Удалеят данный ресурс из менеджера.
	//-------------------------------------------------------------------------
	void Remove( Type **resource )
	{
		// Ensure the resource to be removed and the list is valid.
		// Проверяем валидность списка и удаляемого ресурса.
		if( *resource == NULL || m_list == NULL )
			return;

		// Decrement the resource's reference count.
		// Уменьшаем на единицу счётчик ссылок
		// на данный ресурс.
		(*resource)->DecRef();

		// If the resource is no long being used then destroy it.
		// Если счётчик ссылок на ресурс равен нулю,
		// то данный ресурс больше не используется и его можно
		// смело удалять.
		if( (*resource)->GetRefCount() == 0 )
			m_list->Remove( resource );
	}

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

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

	//-------------------------------------------------------------------------
	// Returns a resource by its filename.
	// Возвращает ресурс по его имени.
	//-------------------------------------------------------------------------
	Type *GetElement( char *name, char *path = "./" )
	{
		// Ensure the name and path are valid, and the list is valid and not empty.
		// Проверяем, что имя, путь и список не пусты.
		if( name == NULL || path == NULL || m_list == NULL )
			return NULL;
		if( m_list->GetFirst() == NULL )
			return NULL;

		// Iterate the list looking for the specified resource.
		// Итерируем через список в поисках определённого ресурса.
		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.
		// Возвращаем NULL, если данный ресурс не найден.
		return NULL;
	}

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

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


Image
Рис.1 Сопоставление ресурсов, менеджеров ресурсов, движка и приложения


Судя по ключевому слову template (англ. "шаблон"), класс ResourceManager также является шаблонным классом, который требует указания типа при создании своего экземпляра (инстанса). А это означает, что для каждого вида ресурсов, которые ты планируешь использовать в своей игре, тебе необходимо создать по одному менеджеру ресурсов. Например, если ты задумал в игре поддержку звуковых эффектов, текстур (куда же без них...) и мешей (полигональных сеток), то в этом случае необходимо создать 3 отдельных менеджера ресурсов (по одному на каждый вид ресурсов). Когда создаётся новый экземпляр класса ResourceManager, вызывается его конструктор, который просто подготавливает менеджер ресурсов, создавая связный список с указанным типом ресурсов.
Конструктор также принимает указатель на функцию CreateResourceFunction, которая на деле является функцией обратного вызова (call-back function), которая вызывается всякий раз, когда в менеджер ресурсов поступает команда добавить (Add, здесь это то же самое что создать) новый ресурс. Когда ты вызываешь функцию Add, менеджер ресурсов создаёт новый ресурс, используя укзанное имя файла и путь к нему. Но в то же время, если функции обратного вызова CreateResourceFunction присвоено какое-либо ненулевое значение (или указатель), то она, в свою очередь, будет обязательно вызвана функцией Add. Такой подход позволяет тебе назначить любую произвольную функцию в коде, специфичном для приложения (application specific code), для создания ресурсов определённого типа. А это означает, что таким образом, ты сможешь создавать ресурсы, которые специфичны только для твоей игры (и не предусмотрены функционалом движка)! Если тебе не требуется в игре поддержки загрузки каких-либо специфичных ресурсов, не поддерживаемых движком, ты можешь спокойно присвоить функции CreateResourceFunction значение NULL. В этом случае менеджер ресурсов будет использовать код загрузки ресурсов по умолчанию, предусмотренный в движке. Ты увидишь применение этой функции обратного вызова позднее, когда мы будем создавать игру. А сейчас посмотри на Рис.1, который показывает взаимосвязь между ресурсами, менеджерами ресурсов, движком и приложением. Стрелками указано, кто у кого запрашивает информацию.
Функция Remove позволяет удалить любой ресурс из менеджера ресурсов. Именно здесь счётчик ссылок (reference count) выходит на первый план.
Вообще, менеджер ресурсов спроектирован так, чтобы исключить ситуации повторной загрузки одних и тех же ресурсов, помогая избежать чрезмерного расхода памяти. При добавлении нового ресурса, счётчик ссылок на него увеличивается на единицу. Если ты попытаешься загрузить тот же самый ресурс ещё раз, менеджер ресурсов, вместо того, чтобы заново загружать файл ресурса, просто возвращает указатель на загруженную ранее копию ресурса и затем инкрементирует счётчик ссылок ещё на единицу (т.е. число ссылок на этот ресурс становится равен 2). Таким образом, всякий раз, когда ты запрашиваешь у менеджера ресурсов копию определённого ресурса, менеджер ресурсов просто даёт тебе указатель на уже имеющийся (т.к. ранее он уже был загружен в память) ресурс и инкрементирует (увеличивает на единицу) счётчик ссылок.
Закрыть
noteПримечание

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

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

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

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

Интегрируем поддержку менеджеров ресурсов в движок

"Подключим" к Проекту Engine заголовок ResourceManagement.h .

Изменения в Engine.h (Проект Engine)

  • Добавь инструкцию #include "ResourceManagement.h" в файл Engine.h, чуть ниже строки #include "LinkedList.h":
Фрагмент Engine.h
...
//-----------------------------
// Engine Includes
//-----------------------------
#include "LinkedList.h"
#include "ResourceManagement.h"
...

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

Источники


1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005


ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.5 Добавляем поддержку базовой геометрии (Geometry.h)

Последние изменения страницы Суббота 09 / Июль, 2022 00:31:38 MSK

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

No records to display

Search Wiki Page

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

Категории

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