WarCraft 3: Базы данных

» Раздел: Триггеры и объекты

Вступление.

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

Что же такое база данных? База данных - это обычная таблица, в которой храниться нужная нам информация о ком или о чем либо.

Первые шаги

1. Планирование

При создании любой базы данных следует начинать именно с этого шага, иначе можно столкнуть с множеством проблем, решение которых может положить на нет всю работу проделанную ранее. Что же в этом шаге такого особенного? А то, что следует продумать, с какой целью нам нужна БД и что мы будем в ней хранить. Начнем по порядку. Допустим, я делаю карту, в которой я хочу добавить предметам новые данные: Коэффициент прочности (Насколько быстро ломается предмет), Минимальная прочность (Когда предмет становиться не пригодным для ремонта), Стоимость ремонта (Сколько стоит починка 1 ед. прочности). С помощью РО такое сделать достаточно сложно, а точнее вообще не возможно. После того, как мы продумали, что нам требуется от базы данных, мы можем перейти к непосредственному конструированию

2. Конструирование

На предыдущем шаге мы выяснили, как должна выглядеть наша БД. В этом шаге мы рассмотрим простую (табличную) базу данных. Её создание не составит нам труда. Табличная БД состоит из 2 главных элементов - это поле и запись. Поле БД - элемент, который определяет, что мы будем хранить в нем, а так же тип хранимых данных. Запись - непосредственно данные. Давайте теперь разберемся, какие поля нам требуется создать:
  1. Коэффициент прочности
  2. Минимальная прочность
  3. Стоимость ремонта
Теперь надо понять, какие значения нужно хранить в них. С этим ничего сложного, в нашем примере видно, что оптимальным вариантом будет целое (integer) значение. Получается, что нам нужно будет создать таблицу с 3 полями, в которой будут храниться значения типа целое (integer)

3. Реализация

Теперь мы знаем, какие должны быть поля у нашей базы данных. Перейдем к непосредственной реализации БД в редакторе. Для этого открываем редактор триггеров. Затем открываем окно редактора переменных. И начинаем создавать нужные поля, для этого нажимаем создать переменную, выбираем нужный тип, даем нужное имя переменной и не забываем ставить галочку Массив (Array). И так нужно сделать для каждого поля БД. Теперь поясню, почему именно так. Массив - это линейная таблица, то есть таблица размером 1х8192. Поэтому создавая еще переменные, получаем уже прямоугольную таблицу. В нашем случае получается таблица, размером 3x8192. В своем примере я прозвал переменные, следующим образом:
BD_itm_kLife Поле №1
BD_itm_mLife Поле №2
BD_itm_rCost поле №3
Советую, вам именовать переменные по такому же принципу, а именно: BD_<имя БД>_<имя поля>. Я считаю, что так лучше ориентироваться в списке переменных.
После того как создали все эти переменные, нужно инициализировать БД. Это делается очень просто. Создаем новый триггер, например BD_itm_InitTrig. В событии указываем инициализация карты. В действиях заполняем поля. Например, у меня есть предмет "Кинжал не прочности". Он имеет следующий код в игре 'I001' (RawCode). Если вы не знаете, как его узнать, то зайдите в Редактор объектов (РО) и нажмите ^D (Ctrl+D), и вы увидите список кодов. По сколько массивы в игре ограничены размером в 8192, индекс может лежать только в пределах от 0 до 8191, а наш код 'I001' = 73*256^3+1 = 1224736769. Как видите, он превышает размер массива, чтобы напрямую его использовать в качестве индекса (ID) записи в БД. Поэтому произведем небольшую манипуляцию с этим числом. Мы просто вычтем основание равкода предмета, а оно равно 'I000'. Получим 'I001' - 'I000' = 1.
Вот и все наша БД инициализирована. Теперь поговорим, как её использовать. Допустим, у нас есть триггер, который срабатывает, когда оружие ломается. В нем нужный предмет уже записан в переменную broke_item. Нам нужно уменьшить его прочность. Но как нам подобраться к коэффициенту прочности? Просто! Для этого мы возьмём равкод нужного нам предмета с помощью функции GetItemTypeID, вычтем значение 'I000' из него, после полученное значение мы используем в качестве ID в БД.
Поскольку в GUI нет возможности напрямую вводить равкод, да и вообще такое число там ввести просто не реально, то я предлагаю такой выход: создадим еще одну переменную типа целое (integer) с именем I000 и зададим значение 1224736768 = 73 * 256^3. Теперь при вычитании можно использовать переменную.

Полное погружение в реализацию

4. Реализация с помощью Jass

Приступая к этой части, я предполагаю, что читатель уже ознакомлен с Jass и имеет хотя бы минимальное представление о vJass
В предыдущей части статьи я рассказал, как создать БД в GUI. Довольно таки рутинная работа. Да и подбираться к данным не очень удобно. Поэтому предлагаю рассмотреть еще один вариант создания БД на jass, а точнее на vJass, т.к. он обладает некоторыми полезными свойствами. Реализовывать будем точно такую же БД. Для начала создадим новый триггер, сконвертируем его в текст и очистим содержимое полностью. Триггер я назвал "ItemEx Data". Для удобства создадим библиотеку, так можно будет скрыть информацию, которая не рекомендуется для просмотра. В библиотеке нам понадобиться функция для инициализации, пока оставим её пустой. На данном этапе у вас должно быть, что то вроде этого:
library ItemExDataLib initializer InitLib
//Реализация БД для предметов
	//Функция инициализации библиотеки
	private function InitLib takes nothing returns nothing
	endfunction
endlibrary
Теперь нам нужно создать саму БД. В принципе процесс ничем не отличается от GUI'шного, только, наверное, даже быстрее. Описывать ячейки базы очень просто с помощью структур. Напомню, что нам нужны 3 поля (Коэффициент прочности, Минимальная прочность, Стоимость ремонта) типа Integer (Целое). Ну что создадим нашу структуру поля БД:
struct ITEMEXDATA
	integer kLife = 0
	integer mLife = 0
	integer rCost = 0
endstruct
Как видите, ничего сложного. Теперь осталось создать саму таблицу данных. Для этого в блоке globals/endglobals создаем новый массив, например "ItemExData". Дальше я бы советовал добавить функцию для автоматического внесения новой записи в БД. Например, такую:
public function AddNewRecord takes integer ItemID, integer kLife, integer mLife, integer rCost returns ITEMEXDATA
	set ItemExData[ItemID - 'I000'] = ITEMEXDATA.create()
	set ItemExData[ItemID - 'I000'].kLife = kLife
	set ItemExData[ItemID - 'I000'].mLife = mLife
	set ItemExData[ItemDB - 'I000'].rCost = rCost
	return ItemExData[ItemDB - 'I000']
endfunction
Осталось только добавить соли по вкусу, и наша БД будет готово. А вся соль в заполнении данных. Для этого вернемся к нашей функции инициализации. В ней нужно будет вызывать функцию добавления новой записи в БД, с нужными значениями. Давай те внесем данные о нашем "Кинжале не прочности". Это делается таким кодом:
	call AddNewRecord('I001', 5, 10, 2)
Полностью моя библиотека теперь выглядит вот так:
» code
library ItemExDataLib initializer InitLib
	//Реализация БД для предметов
	globals
		ITEMEXDATA array ItemExData	//Наша БД
	endglobals
	struct ITEMEXDATA
		integer kLife = 0 //Коэффициент прочности
		integer mLife = 0 //Минимальная прочность для починки
		integer rCost = 0 //Стоимость починки одной единицы
	endstruct
	//Функция добавления записи
	//	Входные параметры:
	//		integer ItemID	- RawCode предмета
	//		integer kLife 	- коэффициент прочности
	//		integer mLife	- минимальная прочность починки
	//		integer rCost	- стоимость починки одной единицы брони
	//	Выходные параметры
	//		ITEMEXDATA		- указатель на поле БД
	public function AddNewRecord takes integer ItemID, integer kLife, integer mLife, integer rCost returns ITEMEXDATA
		set ItemExData[ItemID - 'I000'] = ITEMEXDATA.create()
		set ItemExData[ItemID - 'I000'].kLife = kLife
		set ItemExData[ItemID - 'I000'].mLife = mLife
		set ItemExData[ItemDB - 'I000'].rCost = rCost
		return ItemExData[ItemDB - 'I000']
	endfunction
	//Функция инициализации библиотеки
	private function InitLib takes nothing returns nothing
		//Кинжал не прочности
		call AddNewRecord('I001', 5, 10, 2)
	endfunction
endlibrary
А теперь как нам все это получить непосредственно в коде карты. Допустим нам надо получить стоимость затрат на ремонт предмета, находящегося в переменной itm. Стоимость ремонта занесем в переменную repairCost, а возможность починки занесем в переменную isCanRepair
...
	set itmLife = GetItemLife(itm)
	set itmId	= GetItemTypeId(itm) - 'I000' //Id предмета в БД
	set isCanRepair = itmLife >= ItemExData[itmId].mLife
	if isCanRepair then
		//Вычесляем стоимость починки предмета
		set repairCost = (GetItemMaxLife(itm) - itmLife)*ItemExData[itmId].rCost
		//Дальнейшие действия
		...
	else
		//Вывод сообщения, что предмет починить не возможно
		...
	endif
...
Как видите ничего сложного. На этом данная часть статьи заканчивается

5. Подводные камни и их решение

Теперь рассмотрим некоторые корыстные случаи, которые нас могут поджидать. Увы, не все предметы имеют хороший диапазон равкодов и при вычитании базового равкода, id все равно выходит за пределы размерности массивов. Но предметы это еще полбеды, а если нам нужно записывать данные для юнита? У них базовый равкод зависит от расы, поэтому вычитание базового равкода нам ничего не даст. Так что нам придется изменить структуру нашей базы данных. Давай те создадим еще одну, но теперь только для юнитов, где будет храниться его стоимость, для примера я выбрал следующие ресурсы: железо, нефть и камень. Для хранения данных пригодится тип integer (целое). Создадим новую библиотеку по такому же принципу, как и в прошлой части статьи. Далее создаем структуру (я её назвал UNITEXDATA), от предыдущего пункта в принципе ничего не отличается, кроме парочки моментов. Первым делом нам нужно добавить два новых приватных поля типа integer (целое) с именем id и unitRawCode. Это нам понадобиться для поиска нужной нам информации. Так же потребуется еще одно приватное статическое поле, таково же типа - integer, в котором будет храниться кол-во занятых ячеек в БД. В итоге это у меня выглядит так:
struct UNITEXDATA
	private static integer count = 0	//Занятые ячейки
	//=====
	private integer id	//Положение записи в БД
	private integer unitRawCode	//raw code нашего юнита
	integer MetalCost	= 0	//Стоимость
	integer OilCost		= 0
	integer StoneCost	= 0
endstruct
Теперь осталось создать нужные методы (для создания, нахождения и удаления новой записи в БД). Теперь расскажу о получении id для записи. Будем брать последнюю доступную ячейку. Для этого нам и понадобилась информация о кол-ве занятых ячеек. А при удалении будем очищать текущую ячейку, и переносить данные с последней ячейки в только-что освободившуюся. Теперь непосредственно код:
static method Create takes integer unitRawCode, integer MetalCost, integer OilCost, integer StoneCost returns UNITEXDATA
	local UNITEXDATA this = .create()
	set .id = .count //Записываем в последнюю ячейку
	set .count = .count + 1	//увеличиваем кол-во занятых ячеек
	set UnitExData [.id] = this //UnitExData - наша база данных (UNITEXDATA array UnitExData) объявленная в globals текущей библиотеки
	//Записываем данные
	set .unitRawCode = unitRawCode
	set .MetalCost = MetalCost
	set .OilCost = OilCost
	set .StoneCost = StoneCost
	return this
endmethod
method Destroy takes nothing returns nothing
	set .count = .count -  1 //Уменьшаем кол-во занятых ячеек
	set UnitExData[.id] = UnitExData[.count] //Переносим последнюю ячейку на место текущей
	set UnitExData[.id].id = .id //Обновляем информацию о положении ячейки
	call .destroy()
endmethod
Теперь осталось написать поиск нужной нам записи. Вы можете использовать любой другой способ поиска записи, даже с предварительной сортировкой, ну а я воспользуюсь обычным перебором.
static method Get takes integer UnitRawCode returns UNITEXDATA
	local integer i = .count - 1
	loop
		exitwhen i < 0
		if UnitExData[i].unitRawCode == UnitRawCode then
			return UnitExData[i]
		endif
		set i = i - 1
	endloop
	return 0
endmethod
В принципе ничего сложного. Для заполнения данных используем метод Create. Это делается таким образом:
call UNITEXDATA.Create('hfoo',50,0,0)
//А для получения информации используем такой код:
local integer mCost = UNITEXDATA.Get('hfoo').MetalCost
Кто не разобрался с кусочками кода вот полная библиотека:
» code
library UnitExDataLib initializer InitLib
	globals
		UNITEXDATA array UnitExData //Наша БД
	endglobals
	struct UNITEXDATA
		private static integer count = 0
		//=====
		private integer id
		private integer unitRawCode
		integer MetalCost	= 0	//Требуется стали
		integer OilCost		= 0 //Требуется нефти
		integer StoneCost	= 0 //Требуется камня
		//Добавление новой записи
		//	Входные параметры
		//		integer unitRawCode - равкод юнита, для которого создается новая запись в БД
		//		integer MetalCost	- требует стали
		//		integer OilCost		- требует нефти
		//		integer StoneCost	- требует камня
		//	Выходные параметры
		//		UNITEXDATA			- указатель на запись
		static method Create takes integer unitRawCode, integer MetalCost, integer OilCost, integer StoneCost returns UNITEXDATA
			local UNITEXDATA this = .create()
			set .id = .count
			set .count = .count + 1
			set UnitExData [.id] = this //UnitExData - наша база данных (UNITEXDATA array UnitExData)
			set .unitRawCode = unitRawCode
			set .MetalCost = MetalCost
			set .OilCost = OilCost
			set .StoneCost = StoneCost
			return this
		endmethod
		//Удаление записи с БД
		method Destroy takes nothing returns nothing
			set .count = .count -  1
			set UnitExData[.id] = UnitExData[.count]
			set UnitExData[.id].id = .id
			call .destroy()
		endmethod
		//Поиск записи в БД
		//		integer UnitRawCode - равкод юнита, которого ищем в БД
		//	Выходные параметры
		//		UNITEXDATA	- указатель на запись в БД
		static method Get takes integer UnitRawCode returns UNITEXDATA
			local integer i = .count - 1
			loop
				exitwhen i < 0
				if UnitExData[i].unitRawCode == UnitRawCode then
					return UnitExData[i]
				endif
				set i = i - 1
			endloop
			return 0
		endmethod
	endstruct
	//Инициализация базы данных
	private function InitLib takes nothing returns nothing
		call UNITEXDATA.Create('hfoo',50,0,0)
	endfunction
endlibrary
Теперь рассмотрим небольшую функцию для проверки на возможность строительства здания\юнита
function CanBuild takes integer unitId returns boolean
	local UNITEXDATA record = 0
	set record = UNITEXDATA.Get(buildId)
	if record == 0 then
		return true
	endif
	return record.MetalCost <= currentMetal and record.OilCost <= currentOil and record.StoneCost <= currentStone
endfunction
Как видите, мы используем дополнительную локальную переменную, в которой хранится указатель на нужную нам запись в БД. Таким образом, мы ищем запись всего один раз, вместо четырех. Четвертая идет на проверку. Желательно её делать самостоятельно, тогда можно избежать не предсказуемости выполнения кода. Если же вам достаточно все один раз получить значение в записи и вам далеко начхать на проверку, то можно воспользоваться и такой конструкцией:
UNITEXDATA.Get('hfoo').MetalCost
Но запомните, такая конструкция не приемлема.

6. Возможные доработки и улучшения

В предыдущем пункте мы создали базу данных для юнитов. Для того, чтобы использовать запись несколько раз нам приходилось создавать новую переменную, в которую записывали найденное значение. Сейчас я покажу один небольшой трюк, который упростит работу с записями. Для этого мы добавим новую статическую приватную запись в нашу структуру этого же типа, что и сама запись. Назовем её lastFounded и назначим начальное значение на 0. Для начала изменим метод Get в нашей структуре, так, чтобы при повторном поиске такой же записи он не искал, а сразу выдал нужный указатель. Это делается так
method Get takes integer UnitRawCode returns UNITEXDATA
	local integer i = .count - 1
	if .lastFounded != 0 then	//Если мы недавно искали этот равкод, то вернем найденную ранее запись
		if UnitRawCode == .lastFounded.unitRawCode then
			return .lastFounded
		endif
	endif
	//В прошлый раз искали другое, поэтому ищем по новой
	loop
		exitwhen i < 0
		if UnitExData[i].unitRawCode == UnitRawCode then
			set .lastFounded = UnitExData[i] //Запоминаем найденную запись
			return UnitExData[i]
		endif
		set i = i - 1
	endloop
	//Сбрасывать предыдущую запись нет смысла, так как она может пригодиться
	return 0
endmethod
Этот код не сильно отличается от оригинального, лишь добавилось пару условий на проверку. Теперь можно спокойно писать так:
function CanBuild takes integer unitId returns boolean
	if UNITEXDATA.Get(unitId) == 0 then
		return false
	endif
	return UNITEXDATA.Get(unitId).MetalCost <= currentMetal and <...>
endfunction
Теперь остается только одно неудобство. Запрос происходит через длинную цепочку методов. Но и это можно легко исправить. Для этого в vJass есть очень полезная функция перегрузки операторов. Мы воспользуемся оператором []. Для этого требуется создать статичный метод оператор. Он выглядит так
static method operator [] takes integer id returns UNITEXDATA
	<...>
endmethod
Данный оператор может принимать только число и возвращать что угодно. Теперь осталось написать внутренность этого оператора. А она очень простая, нужно вызвать метод Get с переданным в оператор значением. В итоге получаем следующий метод для перегрузки оператора
static method operator [] takes integer id returns UNITEXDATA
	return .Get(id)
endmethod
А теперь еще один вариант функции CanBuild
function CanBuild takes integer unitId  returns boolean
	if UNITEXDATA[unitId] == 0 then
		return false
	endif
	return UNITEXDATA[unitid].MetalCost <= currentMetal and <...>
endfunction
Как вы уже могли заметить мы пришли к такому же использованию нашей БД, как и в пункте 4. Для тех, кто не полностью разобрался, прикладываю код всей структуры, библиотека в целом не изменилась, поэтому не вижу смысла её включать.
» code
	struct UNITEXDATA
		private static integer count = 0
		private static UNITEXDATA lastFounded = 0
		//=====
		private integer id
		private integer unitRawCode
		integer MetalCost	= 0	//Требуется стали
		integer OilCost		= 0 //Требуется нефти
		integer StoneCost	= 0 //Требуется камня
		//Добавление новой записи
		//	Входные параметры
		//		integer unitRawCode - равкод юнита, для которого создается новая запись в БД
		//		integer MetalCost	- требует стали
		//		integer OilCost		- требует нефти
		//		integer StoneCost	- требует камня
		//	Выходные параметры
		//		UNITEXDATA			- указатель на запись
		static method Create takes integer unitRawCode, integer MetalCost, integer OilCost, integer StoneCost returns UNITEXDATA
			local UNITEXDATA this = .create()
			set .id = .count
			set .count = .count + 1
			set UnitExData [.id] = this //UnitExData - наша база данных (UNITEXDATA array UnitExData)
			set .unitRawCode = unitRawCode
			set .MetalCost = MetalCost
			set .OilCost = OilCost
			set .StoneCost = StoneCost
			return this
		endmethod
		//Удаление записи с БД
		method Destroy takes nothing returns nothing
			set .count = .count -  1
			set UnitExData[.id] = UnitExData[.count]
			set UnitExData[.id].id = .id
			call .destroy()
		endmethod
		//Поиск записи в БД
		//		integer UnitRawCode - равкод юнита, которого ищем в БД
		//	Выходные параметры
		//		UNITEXDATA	- указатель на запись в БД
		static method Get takes integer UnitRawCode returns UNITEXDATA
			local integer i = .count - 1
			if .lastFounded != 0 then
				if UnitRawCode == .lastFounded.unitRawCode then
					return .lastFounded
				endif
			endif
			loop
				exitwhen i < 0
				if UnitExData[i].unitRawCode == UnitRawCode then
					set .lastFounded = UnitExData[i]
					return UnitExData[i]
				endif
				set i = i - 1
			endloop
			return 0
		endmethod
		//Обращение к записи через оператор
		static method operator [] takes integer id returns UNITEXDATA
			return .Get(id)
		endmethod
	endstruct
Теперь я бы хотел рассказать о более быстрых способах поиска. Поскольку простой перебор при большом объеме данных может обойтись дорого, если обращение к БД происходит достаточно часто. Если же у вас не горит повысить скорость доступа к записи в БД, то эту часть статьи можно пропустить. А теперь собственно о способах поиска. Есть много разных алгоритмов поиска в массиве. Я показал один из самых простых, это полный перебор. Довольно таки долгий, но простой метод. Так же существует алгоритм поиска с помощью дерева. Про него я вам и хочу рассказать. Суть его в том, чтобы разделить область поиска на 2 части. Проверить в одной. Если его нет в той области, значит он в другой. Вторую часть делят еще на 2 части. И так до тех пор пока не найдут нужный объект. Если же делать поиск в частях таким же образом, хоть скорость и увеличится, но не на очень много. Так как мы ищем числовую информацию, то мы можем быстро производить поиск простым сравнением. Для этого потребуется отсортировать нашу область поиска. Например, по возрастанию. Дальше простыми вопросами найти маленький участок и пройтись по нему перебором. Для начала рассмотрим алгоритм на небольшом примере. Допустим, у нас есть ряд чисел, и нам надо найти число 8
9	5	2	3	4	1	6	8	7	//Сортируем по возрастанию
1	2	3	4	5	6	7	8	9	//Выбираем средний элемент и задаем вопрос: 8 < центрального числа, если да, то вырезаем левую часть и повторяем действие, если число больше, то вырезаем правую часть, ну а если равны, то центральный элемент искомый
5	6	7	8	9	//Повторяем предыдущий шаг.
8	9	//Поскольку длина этого кусочка меньше рекомендованного < 3, то переберем
В результате мы нашли наше число уже на 4 шаге. Если бы мы перебирали массив, потребовалось бы намного больше попыток. Осталось только реализовать этот алгоритм поиска.
Начнем с сортировки массива. Для этого в нашей структуре создадим пару новых статических методов. Один для сортировки, другой для перестановки элементов массива местами, назовем их Sort и Reloc соответственно.
private static method Reloc takes integer i, integer j returns nothing
	local UNITEXDATA loc = UnitExData[i]
	set UnitExData[i] = UnitExData[j]
	set UnitExData[j] = loc
	//=====
	set UnitExData[i].id = i
	set UnitExData[j].id = j
endmethod
//Пузырьковая сортировка
//Легко запомнить, легко применить, адекватно работает до 400-500 элементов
//Для больших советую qSort + тот же пузырек при длине кусочка < 256
static method Sort takes nothing returns nothing
	local integer i = 0
	local integer j = 0
	loop
		exitwhen i < .count
		set j = i
		loop
			exitwhen j < .count
			if (UnitExData[i].unitRawCode < UnitExData[j].unitRawCode) then
				call .Reloc(i,j)
			endif
			set j = j + 1
		endloop
		set i = i + 1
	endloop
endmethod
Теперь мы можем отсортировать нашу БД по рав коду юнитов, что сможет ускорить процесс поиска. Для этого надо переписать наш старый метод Get
method Get takes integer UnitRawCode returns UNITEXDATA
	local integer i = .count - 1
	local integer startIndex
	local integer endIndex
	
	if .lastFounded != 0 then
		if UnitRawCode == .lastFounded.unitRawCode then
			return .lastFounded
		endif
	endif
	
	call .Sort()
	
	set i = .count / 2
	set startIndex = 0
	set endIndex = .count - 1
	
	loop
		endloop i < 4
		set i = i / 2
		if (UnitRawCode < UnitExData[startIndex+i].unitRawCode) then
			set endIndex = startIndex + i
		elseif (UnitRawCode > UnitExData[startIndex+i].unitRawCode
			set startIndex = endIndex - i
		else
			set .lastFounded = UnitExData[startIndex+i]
			return UnitExData[i]
		endif
	endloop
	
	set i = startIndex
	loop
		exitwhen i <= endIndex
		if UnitRawCode == UnitExData[i].unitRawCode then
			set .lastFounded = UnitExData[i]
			return UnitExData[i]
		endif
		set i = i + 1
	endloop
	
	set .lastFounded = 0
	return 0
endmethod
Метод написан, поиск работает чуть быстрее. Но давайте рассмотрим такой случай, когда мы изменяли БД только в самом начале, а потом постоянно обращались к какой-нибудь записи. Исходя из выше приведенного листинга, сортировка будет выполняться каждый раз, когда мы пытаемся обратиться к данным, но к чему нам такие затраты? Правильно не к чему, поэтому я предлагаю такой выход. Обзавестись новой статической приватной переменной типа boolean с названием isUpdated. По умолчанию это поле должно иметь значение false. Данный флаг отвечает за состояние массива нашей БД. При всяком изменении массива, связанном с положением ячеек, надо устанавливать этот флаг на значение true. Такими действиями являются создание и удаление записей из базы данных. Для этого надо дописать в методах Create и Destroy такую строчку:
set .isUpdated = true
Теперь заменим эту строчку из метода Get
call .Sort()
на эту
if .isUpdated then
	call .Sort
endif
Так же в метод сортировки надо сбросить флаг изменения. В итоге у вас должна получиться такая структура
» code
struct UNITEXDATA
		private static integer count = 0
		private static UNITEXDATA lastFounded = 0
		private static boolean isUpdated = false
		//=====
		private integer id
		private integer unitRawCode
		integer MetalCost	= 0	//Требуется стали
		integer OilCost		= 0 //Требуется нефти
		integer StoneCost	= 0 //Требуется камня
		//Добавление новой записи
		//	Входные параметры
		//		integer unitRawCode - равкод юнита, для которого создается новая запись в БД
		//		integer MetalCost	- требует стали
		//		integer OilCost		- требует нефти
		//		integer StoneCost	- требует камня
		//	Выходные параметры
		//		UNITEXDATA			- указатель на запись
		static method Create takes integer unitRawCode, integer MetalCost, integer OilCost, integer StoneCost returns UNITEXDATA
			local UNITEXDATA this = .create()
			set .id = .count
			set .count = .count + 1
			set UnitExData [.id] = this //UnitExData - наша база данных (UNITEXDATA array UnitExData)
			set .unitRawCode = unitRawCode
			set .MetalCost = MetalCost
			set .OilCost = OilCost
			set .StoneCost = StoneCost
			set .isUpdated = true
			return this
		endmethod
		//Удаление записи с БД
		method Destroy takes nothing returns nothing
			set .count = .count -  1
			set UnitExData[.id] = UnitExData[.count]
			set UnitExData[.id].id = .id
			set .isUpdated = true
			call .destroy()
		endmethod
		private static method Reloc takes integer i, integer j returns nothing
			local UNITEXDATA loc = UnitExData[i]
			set UnitExData[i] = UnitExData[j]
			set UnitExData[j] = loc
			//=====
			set UnitExData[i].id = i
			set UnitExData[j].id = j
		endmethod
		//Пузырьковая сортировка
		//Легко запомнить, легко применить, адекватно работает до 400-500 элементов
		static method Sort takes nothing returns nothing
			local integer i = 0
			local integer j = 0
			loop
				exitwhen i < .count
				set j = i
				loop
					exitwhen j < .count
					if (UnitExData[i].unitRawCode < UnitExData[j].unitRawCode) then
						call .Reloc(i,j)
					endif
					set j = j + 1
				endloop
				set i = i + 1
			endloop
		endmethod
		//Поиск записи в БД
		//		integer UnitRawCode - равкод юнита, которого ищем в БД
		//	Выходные параметры
		//		UNITEXDATA	- указатель на запись в БД
		method Get takes integer UnitRawCode returns UNITEXDATA
			local integer i = .count - 1
			local integer startIndex
			local integer endIndex
			if .lastFounded != 0 then
				if UnitRawCode == .lastFounded.unitRawCode then
					return .lastFounded
				endif
			endif		
			if .isUpdated then
				call .Sort()	
			endif
			set i = .count / 2
			set startIndex = 0
			set endIndex = .count - 1	
			loop
				endloop i < 4
				set i = i / 2
				if (UnitRawCode < UnitExData[startIndex+i].unitRawCode) then
					set endIndex = startIndex + i
				elseif (UnitRawCode > UnitExData[startIndex+i].unitRawCode
					set startIndex = endIndex - i
				else
					set .lastFounded = UnitExData[startIndex+i]
					return UnitExData[i]
				endif
			endloop
			set i = startIndex
			loop
				exitwhen i <= endIndex
				if UnitRawCode == UnitExData[i].unitRawCode then
					set .lastFounded = UnitExData[i]
					return UnitExData[i]
				endif
				set i = i + 1
			endloop
			set .lastFounded = 0
			return 0
		endmethod
		//Обращение к записи через оператор
		static method operator [] takes integer id returns UNITEXDATA
			return .Get(id)
		endmethod
	endstruct

PS. Сори за кривое оформление кода в некоторых местах, при переносе кода сбилась табуляция

Подводим итог

И так, если вы прочитали всю мою статью, то вы сможете сделать любую БД для своих любимых предметов или же юнитов. И я надеюсь, что вам это было полезно, а если есть какие вопросы и замечания, пишите здесь, исправлю и допишу. Удачи в создании ваших карт, и спасибо за внимание.

Просмотров: 6 690

» Лучшие комментарии


Hanabishi #1 - 6 лет назад 2
Хорошая статья, давно была нужна.
ScorpioT1000 #2 - 6 лет назад 4
русския язык хромает, пофиксите ктонибудь, там жесть просто
ё-маё, снял с публикации, там в каждом предложении по 3 ошибки, анрил)
prog #3 - 6 лет назад 0
для автоматизации вбивания данных из РО в БД предлагаю ознакомиться со следующим инструментом: xgm.guru/p/fly-data
Doc #4 - 6 лет назад -1
Прошлый век =)
Ой, ну опоздал чуть
prog #5 - 6 лет назад 1
Статья ничего так. Но было бы неплохо добавить вариант без vjass, и еще сказать пару слов о хештаблицах - при грамотном применении они будут работать быстрее чем поиск по массиву (не путать со слоупочным кешем, хештаблицы отстают от массивов по скорости обращения примерно в полтора раза, на сколько я помню - иногда одно обращение к таблице будет куда лучше чем десяток обращений к массиву и тонна арифметических операций).
П.С. Нет, я не призываю отказаться от структур и перейти на голые хештаблицы.
Doc, почему прошлый век? наоборот, грамотно построенная база данных гармонично дополняется извлечением данных из РО. Моя утилита предоставляет только инструмент, позволяющий достать данные, а как их сохранить в коде, это уже головная боль мапмейкера.
Doc #6 - 6 лет назад 0
Я всмысле что заполнение ручками теперь прошлый век.
prog #7 - 6 лет назад 0
Doc, опять не согласен - пока я не разберусь как дополнять список полей в РО, не вызывая конфликтов с UMSWE, ручками придется дописывать все поля, которые не удастся записать в РО.
alexprey #8 - 6 лет назад 0
Doc, а я и не предлагаю заполнение базы данных вручную. В примерах я использовал поля БД, которых нет в РО
prog #9 - 6 лет назад 0
alexprey, малое количество метаданных можно разместить и в существующих полях, не нарушая целостность данных объекта (естественно при условии что известен способ достать эти метаданные), а список полей в РО можно и расширить, на сколько я знаю.
J64_ #10 - 6 лет назад 0
указывать равкоды вполне возможно в JNGP -> дает кучу возможностей для своих систем
alexprey #11 - 6 лет назад 0
prog, а динамически менять по ходу игры?
prog #12 - 6 лет назад 0
alexprey, алгоритм такой: данные вынимаются из РО на момент сохранения карты и ложатся в твою базу данных, а дальше что хочешь, то с ними и делай.
Judycaster64, JNGP тут причем?
Suite #13 - 6 лет назад 0
маладца все таки написали статью :)
ScorpioT1000 #14 - 6 лет назад (отредактировано ) 0
Ну помогите кто-нибудь с русским и опубликуем.
Suite #15 - 6 лет назад 0
prog:
Но было бы неплохо добавить вариант без vjass
плюсую. покажи на примере отдельного ресурса к примеру еды чтоли?)
alexprey #16 - 6 лет назад 0
ScorpioT1000, прогнал сквозь ворд
Steal nerves #17 - 10 месяцев назад (отредактировано ) 0
с равкодами проблема какая-то. Вот сделал две фермы: 'h000'. 'h001'. Когда вычитывал между двумя типами, я рассчитывал получить единицу (была такая хитрость), и прочее. Выяснил, что совсем не так что-то. Эти равкоды фермы хоть на одну единицу отличаются, то числа в десятичной системе счисления совсем другие у одного 14 миллионов-миллиардов, у другого 15 миллионов-миллиардов. Че не так то?
DracoL1ch #19 - 10 месяцев назад 1
nvc123 #20 - 10 месяцев назад (отредактировано ) 0
14 миллиардов получиться не могло т.к. инт ограничен 32 битами (приблизительно от -2 миллиардов до +2 миллиардов)
Steal nerves #21 - 10 месяцев назад (отредактировано ) 0
nvc123, а почему вот здесь в карте делает так?
Все врубился. У меня получилось так:
1751674741 = 'hhou' (ферма Альянса)
1747988531 = 'h003' (более улучшенная ферма)
короче рабу забыл поставить нужную ферму, и он строил не ту. Все спасибо
прикреплены файлы