Начну с того, что статья рассчитана на людей (а не киборгов, хоть и написана одним из них), владеющих как минимум базовыми знаниями Jass, vJass и cJass.
Но ведь вам скучно, вам нужно БОЛЬШЕ?! Тогда вам сюда.
Для практики требуется иметь установленными и рабочими:
Содержание:
- Основные понятия ООП
- Инкапсуляция
- Абстракция
- Наследование
- Полиморфизм (пропущен)
- Контейнеры
- Классы-конфигурации
- Стек и "Аттачи"
- Заключение
Основные понятия ООП
ООП, как вы, конечно, все прекрасно знаете, расшифровывается как Объектно-Ориентированное Программирование. Это очень широкая тема и я не буду ее пересказывать, для этого есть вики.
Коротко о главном.
Инкапсуляция
Есть такое фундаментальное понятие как Инкапсуляция. Это принцип ООП, который подразумевает внешнее разделение данных между вашим объектом и другими внешними объектами.
Другими словами, вашим объектом извне могут управлять только так, как вы этого хотите.
Обычный пример:
struct square
private real x
private real y
private real a
void SetSide(real newSide) {
a=newSide
}
endstruct
void somefunc() {
square A = square.create()
//...
A.SetSide(2.0)
}
У нас есть какой-то квадрат, мы задаем его сторону. И зачем нам эта функция SetSide? Она только мешает.. почему бы не убрать private и не сделать
A.a = 2.0 ?
А вот что стало когда вы скачали новую версию "квадрата" на следующий день:
#define SQUARE_MIN_SIDE = 0.01
#define SQUARE_MAX_SIDE = 2000.0
struct square
private real x
private real y
private real a
void SetSide(real newSide) {
if(newSide < SQUARE_MIN_SIDE or newSide > SQUARE_MAX_SIDE) {
.a=SQUARE_MIN_SIDE
} else {
.a=newSide
}
.Update()
}
private void Update() {
//...
}
endstruct
А еще разработчик "квадрата" сказал что в будующем его можно будет запускать в космос.
А в вашей старой системе все еще полно простых присвоений.
Конечно, все это зависит от ситуации, но в каждом случае следует задуматься наперед - "а хватит ли мне простого присвоения?".
Почти такая-же ситуация с библиотеками.
Все функции, которые не используются вне библиотеки, должны быть приватными. А данные вобще почти всегда должны быть приватными, кроме некоторых незначительных вещей.
Обычный пример:
library Ustack initializer InitStack // инициализируем как InitStack
#define private MAX_SIZE = 8190
// данные приватны
private int size=0
private unit array U
private int array V
bool IsUnitExist(unit u) { // функция используется внутри и снаружи
//...
}
private void doPush(unit u, int v) { // внутренняя функция, внешний доступ запрещен
//...
}
void Push(unit u, int v) {
if( (not IsUnitExist(u)) and size < MAX_SIZE and u != null) {
doPush(u,v)
}
}
private void InitStack() { // внутренняя функция, внешний доступ запрещен
size=0
U[MAX_SIZE-1] = null
V[MAX_SIZE-1] = 0
}
endlibrary
Также это дает понять пользователям библиотеки, какие функции предназначены для использования библиотеки, а какие - для внутренней работы. В C++ для этого существуют header-файлы, а здесь все не так красиво, зато быстро.
Абстракция
Абстракция - еще одно фундаментальное понятие ООП. Хотя о ней обычно, хоть и поверхностно, знает любой программист.
В целом Абстракция подразумевает физическое разделение внутренних и внешних данных во благо удобства. (В то время как Инкапсуляция подразумевала внешнее.)
Пример покажет это подробнее:
struct BAG
private item i1
private item i2
private item i3
private item i4
private item i5
private item i6
private unit Owner
static BAG New(unit owner) { // статическая функция, заменяет директовый .create() на .New, который удобен
BAG this = BAG.create()
//...
return this
}
void Delete() {
//...
}
void AddItem(item it) { // простая функция добавления
//...
.doAddItem(it) // вызываем независимую do..
}
private void doAddItem(item it) { // а тут еще проверки
if(.GetFreeSlot(it)) {
.doAddItemToSlot(it,sl)
}
}
private int GetFreeSlot(item it) {
//...
}
private void doAddItemToSlot(item it,int sl) {
//...
}
endstruct
Хотя здесь просто стоит учесть, что для объекта лучше изначально определить поведение внутри и снаружи, чем потом все переписывать.
Наследование
Полезный механизм ООП, но сразу скажу, что в варкрафте не стоит им злоупотреблять.
Во-первых, варкрафт не настолько широк, чтобы создавать так много структур, содержащих одинаковые данные. Во-вторых, он очень ограничен возможностями Jass.
Наследование позволяет одним структурам включать в себя все то, что содержат другие структуры.
Пример:
struct WEAPON
string title
real minDamage
real maxDamage
real range
real accuracy
endstruct
struct RocketLauncher extends WEAPON
int rocketNum
//...
endstruct
struct SniperRiffle extends WEAPON
int ClipNum
int CartridgeNum
//...
endstruct
struct Knife extends WEAPON
//...
endstruct
Создается общий класс "Оружие" и все другие классы используют его как родительский.
Полиморфизм
Также есть такое понятие как Полиморфизм. Но в Jass он совсем не применим и я не буду о нем рассказывать.
Контейнеры
Широкое применение в общедоступных библиотеках получили контейнеры. Контейнером является конструкция, включающая в себя объекты изначально неопределенного типа и реализацию некоторых алгоритмов работы с ними. Другими словами, контейнер является полноценной структурой, которая работает с такими типами данных, которые задал пользователь.
Чаще всего, в языках типа C++, контейнеры задаются с помощью шаблонов(templates).
А у нас есть define из cJass :)
И вот распространенный пример:
#define ArrayX10(TYPE,MAXELEMENTS) = {
struct TYPE##ArrayX10
static constant int MAX = MAXELEMENTS
private TYPE array x1
private TYPE array x2
private TYPE array x3
private TYPE array x4
private TYPE array x5
private TYPE array x6
private TYPE array x7
private TYPE array x8
private TYPE array x9
private TYPE array x10
TYPE##ArrayX10 New() {
TYPE##ArrayX10 this = TYPE##ArrayX10.create()
//...
return this
}
void Set(int num, TYPE val) { // не забываем инкапсуляцию !
doSet(num,val)
}
private void doSet(int num, TYPE val) { // здесь, например, можно организовать бинарный поиск
//...
}
TYPE Get(int num) {
//...
}
endstruct
}
//======================================================
// Далее мы реализуем этот контейнер
ArrayX10(int,50000) // создадим массив int на 50000 элементов
ArrayX10(unit,16380) // а тут, скажем, два простых массива юнитов (8190*2)
Кстати, небезызвестная XAT устроена примерно по такому-же принципу.
В результате для ArrayX10(int,50000) будет такой код:
struct intArrayX10
static constant int MAX = 50000
private int array x1
private int array x2
private int array x3
private int array x4
private int array x5
private int array x6
private int array x7
private int array x8
private int array x9
private int array x10
intArrayX10 New() {
intArrayX10 this = intArrayX10.create()
//...
return this
}
void Set(int num, int val) {
doSet(num,val)
}
private void doSet(int num, int val) {
//...
}
int Get(int num) {
//...
}
endstruct
Пример контейнера посложнее(часть взята из DGUI):
struct MATRIX2
static MATRIX2 Zero
static MATRIX2 E
real m11
real m12
real m21
real m22
static MATRIX2 New() {
//...
}
MATRIX2 Multiply(MATRIX2 two) {
//...
}
endstruct
struct MATRIX3
static MATRIX3 Zero
static MATRIX3 E
real m11
real m12
real m13
real m21
real m22
real m23
real m31
real m32
real m33
static MATRIX3 New() {
//...
}
MATRIX3 Multiply(MATRIX3 two) {
//...
}
endstruct
struct MATRIX4
static MATRIX4 Zero
static MATRIX4 E
real m11
real m12
real m13
real m14
real m21
real m22
real m23
real m24
real m31
real m32
real m33
real m34
real m41
real m42
real m43
real m44
static MATRIX4 New() {
//...
}
MATRIX4 Multiply(MATRIX4 two) {
//...
}
endstruct
// ================================================
// А вот и контейнер
#define DOUBLEMATRIX(NAME,aMATR,bMATR) = {
struct NAME
aMATR A
bMATR B
static NAME New() {
NAME this = NAME.create()
.A = aMATR.New()
.B = bMATR.New()
return this
}
NAME Multiply(NAME two) {
NAME result = NAME.New()
result.A = .A.Multiply(two.A)
result.B = .B.Multiply(two.B)
return result
}
endstruct
}
// Реализуем
DOUBLEMATRIX(M2M4,MATRIX2,MATRIX4)
В итоге получаем структуру "M2M4", содержащую в себе MATRIX2 и MATRIX4, а также свои функции управления ими(в данном случае New и Multiply).
Классы-конфигурации
Сам я не видел подробного описания таких классов, а название "Классы-конфигурации" было придумано мной, как наиболее подходящее.
Вон NETRAT говорит: en.wikipedia.org/wiki/Helper_Class_(computer_science)
Классы-конфигурации - это "вспомогательные" классы, которые создаются для удобной передачи параметров в другие функции. После этого они почти всегда удаляются.
Основная особенность такого класса - он имеет только несколько конструкторов и деструктор.
Я думаю, цветной пример все покажет:
enum (stringcolors) { aqua, grey, navy, silver, black, green, olive, teal, blue, lime, purple, white, fuchsia, maroon, red, yellow }
struct COLOR // вспомогательный класс передачи цвета
int A
int R
int G
int B
COLOR RGB(int r, int g, int b) {
COLOR this = COLOR.create()
.A=255
.R=r
.G=g
.B=b
return this
}
COLOR ARGB(int alpha, int r, int g, int b) {
COLOR this = COLOR.create()
.A=alpha
.R=r
.G=g
.B=b
return this
}
COLOR C(int color) {
//...
}
COLOR AC(int alpha, int color) {
//...
}
COLOR S(string color) {
//...
}
void Delete() {
//...
}
endstruct
// ============================
// Дальше у нас есть функция
void FunctionTakesColor(COLOR c) {
//...
c.Delete()
}
// Мы можем вызвать ее разными способами :)
void Caller() {
FunctionTakesColor(COLOR.RGB(80,80,128))
FunctionTakesColor(COLOR.ARGB(220,80,60,180))
FunctionTakesColor(COLOR.C(black))
FunctionTakesColor(COLOR.AC(220,green))
FunctionTakesColor(COLOR.S("FF808000"))
}
Таким образом, одним аргументом может быть и набор целых, и число из перечисления, и даже hex строка.
Этот удобный способ передачи параметров очень поможет сделать вашу библиотеку "уникальнее". Пользователь может поступать в каждом случае по-своему.
Я думаю, это даже частично можно отнести к Полиморфизму.
Стек и "Аттачи"
В программировании есть такое понятие как "Стек". Это набор однотипных данных, добавление и удаление которых производится по типу LIFO (Last In - First Out).
В вармейкинге это используют чаще всего для Мультиприменения и Аттача.
Мультиприменение чего-либо означает, что какой-либо процесс может работать в нескольких экземплярах в один момент времени. Пример - заклинание.
Аттач применяется для "подсоединения" каких-либо дополнительных данных к объектам. Чаще всего такими объектами являются хэндлы из common.j, т.к. их структура неизменяема.
Мультиприменение
Рассмотрим работу мультиприменения.
У нас есть процесс, который имеет свой набор данных и функций. Чтобы сделать его мультиприменяемым, мы должны организовать стек этих данных. С каждым вызовом процесса ему создается своя "область работы" в этом стеке. По завершению, ячейка стека очищается и мы удаляем процесс.
Рассмотрим на простом примере. Стеком будет массив юнитов, а процессом - быстрое повышение здоровья этих юнитов.
Реализуем функции добавления, удаления и лечения юнитов, а также запустим таймер с периодом.
library Healer initializer init
#define private MAX_SIZE = 8190
#define private HEALING_PERIOD = 0.1 // через какие промежутки времени мы будем их лечить
// Внимание ! это не влияет на вылеченное здоровье, т.к. внизу мы умножим здоровье на это число
// и получим здоровья в один "такт"
private real LIFE_PER_SECOND = 4.0 // вылечивает HP в секунду, можно менять по ходу игры
private unit array stack // наш массив
private int count=0 // высота текущего стека
private timer healer = CreateTimer() // таймер для периода
bool HealerEnable = true // переключатель, на всякий случай
int ConvertUnit(unit u) { // Ищем юнита в массиве перебором
int i=0
whilenot(i>=count) {
if(u==stack[i]) { // проверяем, есть ли такой
return i
}
i++
}
return -1
}
int GetHealerStackSize() { return count } // кому-то может понадобиться длина стека
bool HealerAdd(unit u) { // добавляем нового юнита в стек для лечения
if(count<MAX_SIZE) { // проверяем ограничение длины
if(ConvertUnit(u) == -1) { // проверяем, если юнит уже был добавлен
stack[count] = u
count++
return true
}
}
return false
}
void HealerRemove(unit u) { // удаляем юнита из стека
int i=ConvertUnit(u)
if(i != -1) {
count-- // на его место ставим последнего и уменьшаем длину стека
stack[i] = stack[count]
stack[count] = null
}
}
private void HealUnit(int index) { // лечим указанного юнита из стека
real life = GetUnitState(stack[i], UNIT_STATE_LIFE)
if(life < GetUnitState(stack[i], UNIT_STATE_MAX_LIFE)) {
SetUnitState(stack[i], UNIT_STATE_LIFE,life + (LIFE_PER_SECOND*HEALING_PERIOD)) // умножаем период на жизнь/сек, чтобы получить жизнь/такт
}
}
private void Heal() { // пускаем лечение для каждого юнита стека
int i=0
if(HealerEnable) { // наш выключатель..
whilenot(i>=count) {
HealUnit(i)
i++
}
}
}
private void init() {
count=0
TimerStart(healer,HEALING_PERIOD,true,function Heal)
}
endlibrary
Аттачи
С аттачами не намного сложнее.
Цель аттача - привязка чего-либо куда-либо. Рассмотрим на примере юнита.
На этот раз мы будем находить ячейку не перебором, а с помощью "Unit Custom Value".
Для этого удобно создать структуру, содержащую хэндл этого юнита и дополнительные данные.
Попробуем привязать к юниту другого юнита.
struct UNIT
private static int count=1
private static UNIT array All // а здесь наш стек
private int index
private unit me // наш юнит
unit aux // дополнительный юнит
// тут могут быть любые данные*
static UNIT New(unit u, unit aux) {
UNIT this = UNIT.create()
// заполняем данные*
.me=u
.aux=aux
.All[.count] = this // увеличиваем стек
.index = .count
.count ++
SetUnitUserData(.me,.index)
return this
}
void Delete() {
SetUnitUserData(.me,0)
// очищаем данные*
.me = null
.aux = null
.count--
.All[.index] = .All[.count]
.All[.count] = 0
.destroy()
}
static UNIT GetByIndex(unit u) { // узнаем ячейку по Custom Value
return All[GetUnitUserData(u)]
}
void SetAux(unit newaux) {
.aux = newaux
}
endstruct
// =========================================
// А тут наши пользовательские функции
void AttachUnitToUnit(unit mainUnit, unit auxUnit) { // прикрепить юнита к юниту. Если уже прикреплен - прикрепляет другого
UNIT u = GetUnitUserData(u)
if(u <= 0) { // проверяем, не создан-ли такой юнит
UNIT u = UNIT.New(mainUnit,auxUnit)
} else {
u.SetAux(auxUnit)
}
}
void DetachUnitFromUnit(unit mainUnit) { // отделяем юнит от юнита, удаляем структуру
UNIT.GetByIndex(mainUnit).Delete()
}
unit GetAttachedUnit(unit mainUnit) { // возвращаем прикрепленного юнита
return UNIT.GetByIndex(mainUnit).aux
}
// *вы можете прикреплять любые данные, дополнив места со звездочкой
В итоге достаточно простой интерфейс привязки.
Также вы можете почитать, как прикреплять данные с помощью Кэша, Хэш-таблиц и XAT..
Например, здесь: xgm.guru/articles.php?name=ex_jass
Или здесь: xgm.guru/forum/showthread.php?t=12894
(и все это будет зависеть от версии варкрафта)
|