Повседневные приёмы в программировании варкрафта | |
Основные понятия ООП
ООП, как вы, конечно, все прекрасно знаете, расшифровывается как
Объектно-Ориентированное Программирование. Это очень широкая тема и я не буду ее
пересказывать, для этого есть вики. Коротко о главном.
Инкапсуляция
Есть такое фундаментальное понятие как Инкапсуляция. Это принцип ООП,
который подразумевает внешнее разделение данных между вашим объектом и другими
внешними объектами. Другими словами, вашим объектом извне могут управлять
только так, как вы этого хотите.
Обычный пример:
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
#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) {
BAG this = BAG.create()
return this
}
void Delete() {
}
void AddItem(item it) {
.doAddItem(it)
}
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)
ArrayX10(unit,16380)
Кстати, небезызвестная 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
:
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
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) {
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
}
В итоге достаточно простой интерфейс привязки.
|