Среда, 18.12.2024, 05:15


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

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

Мини-чат

Статистика

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

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

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

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

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

Поиск

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

  • Copyright MyCorp © 2024uCoz