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.ru/articles.php?name=ex_jass
Или здесь: xgm.ru/forum/showthread.php?t=12894
(и все это будет зависеть от версии варкрафта)

Заключение

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

Просмотров: 13 048

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


Rewenger #1 - 8 лет назад 5
Всё-таки добавил. Название вызывает сомнения, это скорее "Повседневные приёмы программирования в варкрафте".
А, по сути, придраться не к чему, всё весьма понятно описано. Хорошая статья.
XimikS #2 - 8 лет назад 2
Добавил бы хоть что нибудь еще.полиморфизм можно, но это будет нечто) впрочем он и есть
ScorpioT1000 #3 - 8 лет назад 0
нету там полиморфизма. не-ту !
XimikS #4 - 8 лет назад 2
Ты уверен?)
[url]http://www.wc3c.net/vexorian/jasshelpermanual.html#interfs[/url]
ScorpioT1000 #5 - 8 лет назад 0
химикс, ты, видимо не понимаешь понятия полиморфизм. я не имею ввиду в процессе компиляции.
XimikS #6 - 8 лет назад 2
Понимаю.
в процессе компилирования
Сразу сказал бы =0
ScorpioT1000 #7 - 8 лет назад 0
полиморфизм то во время работы определяется, т.е. "какую виртуальную функцию вызывать"
2 комментария удалено
Alex_Hell #10 - 8 лет назад 3
автор, статья норм..
нашел ошибку: в листинге про Аттачи в методе
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 - 8 лет назад 0
спасибо, исправил.
юнит юзер дата нужна чтобы избежать метода перебора при прикреплении структуры к юниту
Msey #12 - 7 лет назад 1
Cтатья просто ЧУМОВАЯ )) вкурил.. но не все
Спасибо!
FYAN #13 - 6 лет назад (отредактировано ) 2
объясните в чем практическая суть статьи
что можно сделать такого, зная инкапсуляцию и прочее?) Вопрос ради интереса
Bornikkeny #14 - 6 лет назад 2
Эхх, понять бы концепцию ООП... Чую что еще рановато, но потом пригодиться.
SlenderMan #15 - 5 лет назад 2
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
ScopteRectuS #16 - 2 недели назад (отредактировано ) 0
ScorpioT1000, хотелось бы, что Вы рассказали про полиморфизм. Интерфейсы вроде дают такую возможность.
Хоть убейте, не могу понять, как работать с интерфейсами и где их стоит применять.
PT153 #17 - 2 недели назад (отредактировано ) 2
инкапсуляцию
Это техника скрытия от клиентов некоторых методов или полей у поставщика. Клиент - класс, который использует поставщика, поставщик тоже класс. Таким образом можно убедится, например, что некоторые поля могут меняться только внутри методов класса. Также можно убирать не важные для клиента методы и поля. Такие поля и методы называются приватными (private), то есть их можно использовать и изменять только внутри методов класса.
Зачем это? Для удобства.
Также есть protected и public поля и методы. Protected могут использоваться и изменяться только внутри класса и его наследников. Public, очевидно, могут использоваться и изменяться всеми классами.
ScopteRectuS:
полиморфизм
Это техника, которая позволяет одной переменной принимать разные типы во время выполнения программы. Т. н. dynamic binding.
Покажу на примере JASS, хотя там ООП вообще нет (точнее, есть завуалированное использование триггеров и массивов по видом ООП).
struct A
    stub method someMethod takes nothing returns nothing
        call BJDebugMsg("This is A!")
    endmethod
endstruct

struct B extends A
    method someMethod takes nothing returns nothing
        call BJDebugMsg("This is B!")
    endmethod

    stub method someMethod2 takes nothing returns nothing
        call BJDebugMsg("This is B2!")
    endmethod
endstruct

struct C extends B
    method someMethod2 takes nothing returns nothing
        call BJDebugMsg("This is C!")
    endmethod
endstruct
...
function f takes nothing returns nothing
    local A a = A.create()
    local B b = B.create()
    call a.someMethod() // будет выведено "This is A!".
    set a = B.create()
    call a.someMethod() // будет выведено "This is B!".
    call a.someMethod2() // будет выведено "This is B2!".
    set a = C.create()
    call a.someMethod() // будет выведено "This is B!", так как C унаследовало метод someMethod() от B.
    call a.someMethod2() // будет выведено "This is C!".
    set b = A.create() // неверно, так как A не является ни наследником класса B, ни самим классом B.
endfunction
Т. о. переменная а во время выполнения функции f имела в себе 3 разных типа.
Надо отметить, что это не касается vJASS, так как А, В и С на самом деле будут иметь тип integer, потому что настоящего ООП в vJASS нет.
ScopteRectuS:
Хоть убейте, не могу понять, как работать с интерфейсами и где их стоит применять.
А теперь про интерфейсы. Они нужны тогда, когда у некоторых классов есть одинаковые поля и методы. В интерфейсах нельзя описывать методы, только декларировать, а вот все наследники интерфейса должны будут этот метод описать.

Я мог чутка ошибиться в синтаксисе интерфейсов в vJASS, так как не использую их.

interface A
    unit u
    method someMethod takes nothing returns nothing
endinterface

struct B extends A
    method someMethod takes nothing returns nothing
        call BJDebugMsg("This is B!")
    endmethod
endstruct

struct C extends A
    method someMethod takes nothing returns nothing
        call BJDebugMsg("This is C!")
    endmethod
endstruct
...
function f takes nothing returns nothing
    local A a = A.create() // неверно, нельзя инициировать объект интерфейса!
    local B b = B.create()
    local C c = C.create()
    set a = b
    call a.someMethod() // будет выведено "This is B!".
    set a.u = CreateUnit(Player(0), 'hfoo', 0., 0., 0.) // теперь в b.u записан пехотинец.
    set a = c
    call a.someMethod() // будет выведено "This is C!".
    set a.u = CreateUnit(Player(0), 'Hpal', 0., 0., 0.) // теперь в c.u записан паладин.
endfunction
А вообще, читайте мануал к vJASS, для маппинга в WCIII вполне хватит, а для обучения ООП поступите в университет или поищите книги (я вот знаю Touch of Class Бертрана Мейера).
ScopteRectuS #18 - 2 недели назад (отредактировано ) 0
PT153, а что насчёт function interface?
Написал следующий код для себя, подглядывая сюда:
» код:
    function interface callback takes integer this returns nothing defaults nothing


    module linkedList
        private  static  timer     period  =  null
        private          thistype  prev
        private          thistype  next
        private          callback  handlerFunc


        method destroyList takes nothing returns nothing
            set  this.prev.next  =  this.next
            set  this.next.prev  =  this.prev
            if ( thistype( 0 ).next == 0 ) then
                call PauseTimer( thistype.period )
            endif
            call thistype.deallocate( this )
        endmethod


        private static method iterate takes nothing returns nothing
            local  thistype  this  =  thistype( 0 ).next
            loop
                exitwhen ( this == 0 )
                call this.handlerFunc.evaluate( integer( this ) )
                set  this  =  this.next
            endloop
        endmethod


        static method createList takes real timeout, callback handlerFunc returns thistype
            local  thistype  this  =  thistype.allocate( )
            set  this.next         =  thistype( 0 )
            set  this.prev         =  thistype( 0 ).prev
            set  this.next.prev    =  this
            set  this.prev.next    =  this
            set  this.handlerFunc  =  handlerFunc
            if ( this.prev == 0 ) then
                call TimerStart( thistype.period, timeout, true, function thistype.iterate )
            endif
            return this
        endmethod


        private static method onInit takes nothing returns nothing
            set  thistype.period  =  CreateTimerEx( )
        endmethod


    endmodule
» пример использования:
    struct abili
        unit caster
        unit target
        implement linkedList

        method update takes nothing returns nothing
//          ...
        endmethod


        static method createA takes nothing returns nothing
            local  thistype  this  =  thistype.createList( 0.03125, update )

//          ...
        endmethod
        
    endstruct
Я так понял она нужна, только для того чтобы как-то заменить code array, который нельзя создать. Она создаёт триггер, который будет вызван через TriggerEvaluate( ), а параметры будут переданы через глобальные переменные. Это её единственное применение или можно еще что-нибудь замутить интересное?

А насчёт interface/endinterface вроде всё понятно. Просто набор правил, чтобы в случае чего выдать ошибку?
PT153 #19 - 2 недели назад (отредактировано ) 2
function interface
Вот этим не интересовался, лучше написать код, сохранить карту, а после открыть её MPQEditor и посмотреть, что же вышло.
Я так понял она нужна, только для того чтобы как-то заменить code array, который нельзя создать.
Ну как видишь, можно с помощью этого ещё и функции передавать, что довольно неплохо.
Она создаёт триггер, который будет вызван через TriggerEvaluate( ), а параметры будут переданы через глобальные переменные.
Лучше глянуть, что происходит на самом деле, нормальный пример есть в мануале vJass (кидал выше).
Лол, пока смотрел в мануале про function interface, обнаружил TypeCast.
А насчёт interface/endinterface вроде всё понятно. Просто набор правил, чтобы в случае чего выдать ошибку?
Я не вижу большого смысла в интерфейсах в Jass, тут все объекты целые числа, потому интерфейс можно с лёгкостью заменить на textmacro. Как, в общем-то, и модули.
Есть такое понятие абстрактный класс, гибрид обычного класса и интерфейса. Абстрактный класс может иметь полноценный метод, а не только его декларацию в отличие от интерфейса, но его объект всё также не может быть создан. Тогда вопрос, а зачем нужны вообще интерфейсы, если абстрактные классы лучше? Например, в C# благодаря интерфейсам класс может наследоваться от нескольких интерфейсов и не более 1 класса одновременно. Наследование от нескольких абстракный классов может вызвать т. н. diamond problem, так как в абстрактных классах некоторые методы могут быть описаны. Потому в C# смысл в интерфейсах есть, а вот в vJass я его не вижу.
В vJass абстрактных классов нет, а мне пригодились бы.

Diamond Problem

struct A
    stub method F takes nothing returns nothing
        call BJDebugMsg("This is A!")
    endmethod
endstruct

struct B extends A
    timer t

    method F takes nothing returns nothing
        call BJDebugMsg("This is B!")
    endmethod
endstruct

struct C extends A
    unit k

    method F takes nothing returns nothing
        call BJDebugMsg("This is C!")
    endmethod
endstruct

struct D extends B, C // так нельзя в vJass, но нужно для примера
endstruct
...
function f takes nothing returns nothing
    local D d = D.create()
    set d.t = CreateTimer()
    set d.k = CreateUnit(Player(0), 'hfoo', 0., 0., 0.)
    call d.F() // а что вызвать? Метод из B или из C?
endfunction