Пятница, 17.05.2024, 12:12


Главная
Регистрация
Вход
Welcome to Home Приветствую Вас Гость | RSS  
Меню сайта

Категории раздела
Мои статьи [25]

Мини-чат

Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Главная » Статьи » Мои статьи

Базы данных

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

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

 Первые шаги

 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

А теперь как нам все это получить непосредственно в коде карты. Допустим нам надо получить стоимость затрат на ремонт предмета, находящегося в переменной 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

Теперь рассмотрим небольшую функцию для проверки на возможность строительства здания\юнита

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

Теперь я бы хотел рассказать о более быстрых способах поиска. Поскольку простой перебор при большом объеме данных может обойтись дорого, если обращение к БД происходит достаточно часто. Если же у вас не горит повысить скорость доступа к записи в БД, то эту часть статьи можно пропустить. А теперь собственно о способах поиска. Есть много разных алгоритмов поиска в массиве. Я показал один из самых простых, это полный перебор. Довольно таки долгий, но простой метод. Так же существует алгоритм поиска с помощью дерева. Про него я вам и хочу рассказать. Суть его в том, чтобы разделить область поиска на 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

Так же в метод сортировки надо сбросить флаг изменения. В итоге у вас должна получиться такая структура

Категория: Мои статьи | Добавил: Enemy1PK (02.04.2017)
Просмотров: 366 | Рейтинг: 0.0/0
Всего комментариев: 0
avatar
Вход на сайт

Поиск

Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • База знаний uCoz

  • Copyright MyCorp © 2024uCoz