WarCraft 3: Повседневные приёмы в программировании варкрафта

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

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

Классы-конфигурации

Сам я не видел подробного описания таких классов, а название "Классы-конфигурации" было придумано мной, как наиболее подходящее.
Классы-конфигурации - это "вспомогательные" классы, которые создаются для удобной передачи параметров в другие функции. После этого они почти всегда удаляются.
Основная особенность такого класса - он имеет только несколько конструкторов и деструктор.
Я думаю, цветной пример все покажет:
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
(и все это будет зависеть от версии варкрафта)

Заключение

В заключение я хочу сказать, хватит палить уже мою статью) Я и так сидел три часа..
Так о чем я, в наши тяжелые времена программистам приходится нелегко итп.. ну вобщем сами че-нибудь придумайте.
Я пошел :) Слушайте рок !

Просмотров: 11 273

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


Rewenger #1 - 7 лет назад 4
Всё-таки добавил. Название вызывает сомнения, это скорее "Повседневные приёмы программирования в варкрафте".
А, по сути, придраться не к чему, всё весьма понятно описано. Хорошая статья.
XimikS #2 - 7 лет назад 1
Добавил бы хоть что нибудь еще.полиморфизм можно, но это будет нечто) впрочем он и есть
ScorpioT1000 #3 - 7 лет назад 0
нету там полиморфизма. не-ту !
XimikS #4 - 7 лет назад 1
Ты уверен?)
[url]http://www.wc3c.net/vexorian/jasshelpermanual.html#interfs[/url]
ScorpioT1000 #5 - 7 лет назад 0
химикс, ты, видимо не понимаешь понятия полиморфизм. я не имею ввиду в процессе компиляции.
XimikS #6 - 7 лет назад 1
Понимаю.
в процессе компилирования
Сразу сказал бы =0
ScorpioT1000 #7 - 7 лет назад 0
полиморфизм то во время работы определяется, т.е. "какую виртуальную функцию вызывать"
2 комментария удалено
Alex_Hell #10 - 7 лет назад 2
автор, статья норм..
нашел ошибку: в листинге про Аттачи в методе
static UNIT GetIndex(unit u) {
return GetUnitUserData(u)
}
GetUnitUserData() возвращает int, а не UNIT, нужно сделать так:
return All[GetUnitUserData(u)]

хотя если вернуться к той же инкапсуляции, никакого метода GetIndex() не должно быть, т.к. незачем пользователю структуры знать индекс юнита во внутреннем массиве...
лучше сделать метод:
unit UNIT::GetAux() { return .aux }

а из внешней среды вызывать так:
void DetachUnitFromUnit(unit mainUnit) {
mainUnit.Delete()
}
unit GetAttachedUnit(unit mainUnit) {
return mainUnit.GetAux()
}
так что заморочки с SetUnitUserData / GetUnitUserData не нужны, т.к. любой экземпляр структуры имеет поле index, где находится его индекс в статическом массиве All.
ScorpioT1000 #11 - 7 лет назад 0
спасибо, исправил.
юнит юзер дата нужна чтобы избежать метода перебора при прикреплении структуры к юниту
Msey #12 - 6 лет назад 1
Cтатья просто ЧУМОВАЯ )) вкурил.. но не все
Спасибо!
FYAN #13 - 5 лет назад (отредактировано ) 1
объясните в чем практическая суть статьи
что можно сделать такого, зная инкапсуляцию и прочее?) Вопрос ради интереса
Bornikkeny #14 - 4 года назад 1
Эхх, понять бы концепцию ООП... Чую что еще рановато, но потом пригодиться.
SlenderMan #15 - 4 года назад 1
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)) умножаем период на жизнь/сек, чтобы получить жизнь/такт
}
}
Тут скорее не ошибка, а небольшая опечатка)
Индекс стека не указан.
вот так: int i=index
Это сообщение удалено