- Joined
- Jun 30, 2006
- Messages
- 230
Introduction to vJASS, Structs and Good JASS Technique
Note: I will convert this to a tutorial for everyone, but I am teaching Flame and he needs this now.
Methods
Scopes
Structs
Structs are part of vJASS, an extension to JASS that can be obtained through JassHelper which is part of JassNewGen. If are somewhat familiar with Handle Vars, think of this as the place where data is stored, kind of like the gamecache in Handle Vars. It is sort of a mini-database. Structs are used as datatypes. Inside of them they can hold whatever data you tell it to. Behind the scenes, structs are just cleverly designed arrays, so they are much faster than a gamecache.
The above trigger can be written like this, as well:
In this example, we store a unit and a timer. These two datatypes are used very often in spells, so that is why they were chosen for this example. You can store any dataype you would like inside of a struct. When structs are declared in the latter way, the struct assigns the values to
Structs help us greatly because instead of using gamecache to store each of the datatypes we need, we can store them in our struct. This lets us do an attachment once, just on our custom datatype, rather than each piece one at a time.
Methods
Structs can also hold functions in them. When a function is used inside of a struct, it is called a method. They are declared just like functions are. However, you call a method slightly different from a function. Remember, structs are datatypes, so we have a special syntax for calling methods. You have already seen it. When you define a struct, you used the .create() method. The .create() method does some things behind the scenes. It is part of every struct. You can give it more things to do, though, like in the following example:
There are essentially three types of methods: private, public, and static. Private means that only methods within the struct can access and use the method. Public means that the method can be accessed from outside the struct. Private and public methods create an 'instance', thus you cannot use it in code callback (examples:
You might have noticed there are no local variables (instances of the new datatype are the exception). You do not need local variables inside of methods, you declare them where
Another keyword in vJASS is "constant". When you define something as constant, you can no longer change its value:
It is important to note that you can declare a method or function as constant if the method/function returns a value that does not change. This actually does nothing in and of itself. However, if you use a map optimizer, it will "inline" the method/function, meaning it literally replaces the method/function call with the value it returns, this improving speed.
Also, when you are done with a struct, you should destroy it. This is done by calling the .destroy() method. You cannot modify the .destroy() method. However, when the .destroy() method is called, a method named onDestroy will be called as well if you define one. This allows you to do cleanup of our own when we call the .destroy method. This is very usefull for pausing and destroying timers, as well as deleting groups and many other things. The following code is a very simpilistic knockback struct:
Basically, when the static method Knockback is called with a timer, it will automate the rest of the knockback and do cleanup by calling .destroy() when you are done. Notice that onDestroy is private, because we do not want people calling it outside of the struct as it will prematurely destroy our timer. That would make the unit stop being "knockbacked", and we do not want this to happen.
Scopes / Good Programming Practice
Each struct should generally have two entry points into the struct. The first one is obviously the create method. The second is the method that automates the rest of the sequence. For example, the afore mentioned knockback struct has only 1 entry point besides create. If you design your structs to automate the entire purpose of the trigger, then you can keep what the trigger does separate from Conditions and the InitTrig. It also makes it easier for you to transfer your spell from map-to-map. Doing this will also help eliminate errors (does for me, at least).
There are some more practices I think every programmer who uses NewGen should know and use. Part of vJASS has things called scopes. Scopes are very useful when you are programming because they easily let you do a few good programming techniques automatically, as you will see. I think the best way to teach scopes is to show you how it used first. The following is a an ability that is basically Blur from DotA:
And then using scopes:
Functions inside of a scope can be declared as private or public, just like structs. However, unlike methods, there is no need for a static function, and thus do not exist. Unlike structs, triggers should have only one entry point: your InitTrig function. This is so that you do not accidentally use a function you are not suppose to, which will help eliminate errors. So you will notice inside of the scope that all my functions are private EXCEPT InitTrig, which is declared public. InitTrig is the entry point, of course.
Also, notice that I renamed my functions. I dropped the Blur_ prefixes. In scopes, you do not need them, as the function gets the Blur_ prefix automatically. The exception to this rule is InitTrig, which it sticks _Blur after it, but only if you declare it as public which you should do anyways. The automatic prefixes help when you are transferring spells from map to map, as well as when you have conflicting trigger names. With scopes, just change the scope name. It is far easier than having to rename each function to have the <triggername>_ prefix.
Note: I will convert this to a tutorial for everyone, but I am teaching Flame and he needs this now.
An Introduction to vJASS, Structs, and Good Programming Practice
Introduction to StructsMethods
Scopes
Structs
Structs are part of vJASS, an extension to JASS that can be obtained through JassHelper which is part of JassNewGen. If are somewhat familiar with Handle Vars, think of this as the place where data is stored, kind of like the gamecache in Handle Vars. It is sort of a mini-database. Structs are used as datatypes. Inside of them they can hold whatever data you tell it to. Behind the scenes, structs are just cleverly designed arrays, so they are much faster than a gamecache.
JASS:
struct MyStruct //declares a struct and gives it a name
unit u //this is how to declare datatypes inside of a struct
timer t
endstruct //ends the struct
function Actions takes nothing returns nothing
// how to use a struct:
// local <structname> <variablename> = <structname>.create()
local MyStruct data = MyStruct.create()
// to set a value inside of a struct, you use:
// set <variable name for your struct>.<variable you want to set>
set data.u = GetTriggerUnit() //set u in our struct to the triggering unit
set data.t = CreateTimer() //and the timer to create a new timer
endfunction
JASS:
struct MyStruct
unit u = GetTriggerUnit()
timer t = CreateTimer()
endstruct
function Actions takes nothing returns nothing
local MyStruct data = MyStruct.create()
endfunction
In this example, we store a unit and a timer. These two datatypes are used very often in spells, so that is why they were chosen for this example. You can store any dataype you would like inside of a struct. When structs are declared in the latter way, the struct assigns the values to
unit u
and timer t
when a .create()
is used. I prefer the use of the second method over the first, but sometimes it is not possible. Your variables can change, so you may need to set them like we did in the first example.Structs help us greatly because instead of using gamecache to store each of the datatypes we need, we can store them in our struct. This lets us do an attachment once, just on our custom datatype, rather than each piece one at a time.
Methods
Structs can also hold functions in them. When a function is used inside of a struct, it is called a method. They are declared just like functions are. However, you call a method slightly different from a function. Remember, structs are datatypes, so we have a special syntax for calling methods. You have already seen it. When you define a struct, you used the .create() method. The .create() method does some things behind the scenes. It is part of every struct. You can give it more things to do, though, like in the following example:
JASS:
struct MyStruct
unit u
timer t
//the create method must return a MyStruct and be static
static method create takes nothing returns MyStruct
local MyStruct data = MyStruct.allocate()//must define this line
set data.u = GetTriggerUnit() //sets the variables when create is called
set data.t = CreateTimer()
return data
endmethod
endstruct
function Actions takes nothing returns nothing
local MyStruct data = MyStruct.create()
endfunction
ForGroup(),Condition(),TimerStart()[icode=jass]). If a method takes nothing and is declared as static, it does not create an 'instance' so you can use that method as code callback. The create method is required to be static.
In the above code, while we can declare/use [icode=jass]unit u
and timer t
outside of our struct, integer i
cannot.You might have noticed there are no local variables (instances of the new datatype are the exception). You do not need local variables inside of methods, you declare them where
unit u
and timer t
are declared. As you can see in the above code, you use the variable name with a period before it.Another keyword in vJASS is "constant". When you define something as constant, you can no longer change its value:
JASS:
struct MyStruct
unit u
timer t
constant integer AID_SPELL_NAME='A000'
private integer i
private method Constant takes nothing returns nothing
set AID_SPELL_NAME='A001' //doesn't work, AID_SPELL_NAME is constant
endmethod
static method create takes nothing returns MyStruct
local MyStruct data = MyStruct.allocate()
set data.u = GetTriggerUnit()
set data.t = CreateTimer()
set data.i = 0
call data.Constant() //works because we are in the same struct
return data
endmethod
endstruct
function Actions takes nothing returns nothing
local MyStruct data = MyStruct.create()
call data.Constant() //does not work, .Constant is private
endfunction
It is important to note that you can declare a method or function as constant if the method/function returns a value that does not change. This actually does nothing in and of itself. However, if you use a map optimizer, it will "inline" the method/function, meaning it literally replaces the method/function call with the value it returns, this improving speed.
Also, when you are done with a struct, you should destroy it. This is done by calling the .destroy() method. You cannot modify the .destroy() method. However, when the .destroy() method is called, a method named onDestroy will be called as well if you define one. This allows you to do cleanup of our own when we call the .destroy method. This is very usefull for pausing and destroying timers, as well as deleting groups and many other things. The following code is a very simpilistic knockback struct:
JASS:
scope Knockback
struct knockback
unit u
real power
real angle
real x
real y
timer t
static method create takes unit u, real power, real angle returns knockback
local knockback data = knockback.allocate()
set data.t = CreateTimer()
set data.u = u
set data.x = GetUnitX(u)
set data.y = GetUnitY(u)
set data.power = power
set data.angle = angle
return data
endmethod
private method onDestroy takes nothing returns nothing
//ClearTimerStructA is part of the ABC system
//don't worry about it for now
call ClearTimerStructA(.t)
call PauseTimer(.t)
call DestroyTimer(.t)
endmethod
static method Knockback takes nothing returns nothing
//GetTimerStructA is part of the ABC system
//don't worry about it for now
local knockback data = GetTimerStructA(GetExpiredTimer())
set data.power = data.power * 0.96
set data.x = data.x + data.power * Cos(data.angle * bj_DEGTORAD)
set data.y = data.y + data.power * Sin(data.angle * bj_DEGTORAD)
call SetUnitPosition(data.u, data.x, data.y)
if data.power < 4 then
call data.destroy()
endif
endmethod
endstruct
Basically, when the static method Knockback is called with a timer, it will automate the rest of the knockback and do cleanup by calling .destroy() when you are done. Notice that onDestroy is private, because we do not want people calling it outside of the struct as it will prematurely destroy our timer. That would make the unit stop being "knockbacked", and we do not want this to happen.
Scopes / Good Programming Practice
Each struct should generally have two entry points into the struct. The first one is obviously the create method. The second is the method that automates the rest of the sequence. For example, the afore mentioned knockback struct has only 1 entry point besides create. If you design your structs to automate the entire purpose of the trigger, then you can keep what the trigger does separate from Conditions and the InitTrig. It also makes it easier for you to transfer your spell from map-to-map. Doing this will also help eliminate errors (does for me, at least).
There are some more practices I think every programmer who uses NewGen should know and use. Part of vJASS has things called scopes. Scopes are very useful when you are programming because they easily let you do a few good programming techniques automatically, as you will see. I think the best way to teach scopes is to show you how it used first. The following is a an ability that is basically Blur from DotA:
JASS:
globals
private constant integer AID_BLUR='A000'
private integer array alpha
endglobals
struct StructUnit
unit u=GetTriggerUnit()
timer t=CreateTimer()
endstruct
function Blur_Refresh takes nothing returns nothing
local StructUnit s=GetTimerStructA(GetExpiredTimer())
call SetUnitVertexColor(s.u,255,255,255,alpha[GetUnitAbilityLevel(s.u,AID_BLUR)])
endfunction
function Blur_Conditions takes nothing returns boolean
return GetLearnedSkill()==AID_BLUR and IsUnitIllusion(GetTriggerUnit())==false
endfunction
function Blur_Actions takes nothing returns nothing
local StructUnit s=StructUnit.create()
call SetUnitVertexColor(s.u,255,255,255,alpha[GetUnitAbilityLevel(s.u,AID_BLUR)])
call SetTimerStructA(s.t,s)
call TimerStart(s.t,2,true,function Blur_Refresh)
endfunction
function InitTrig_Blur takes nothing returns nothing
set gg_trg_Blur=CreateTrigger()
set alpha[1] = 90
set alpha[2] = 65
set alpha[3] = 40
set alpha[4] = 20
call TriggerRegisterAnyUnitEventBJ(gg_trg_Blur,EVENT_PLAYER_HERO_SKILL)
call TriggerAddCondition(gg_trg_Blur,Condition(function Blur_Conditions))
call TriggerAddAction(gg_trg_Blur,function Blur_Actions)
endfunction
endscope
And then using scopes:
JASS:
scope Blur
globals
private constant integer AID_BLUR='A000'
private integer array alpha
endglobals
struct StructUnit
unit u
timer t=CreateTimer()
endstruct
private function Refresh takes nothing returns nothing
local StructUnit s=GetTimerStructA(GetExpiredTimer())
call SetUnitVertexColor(s.u,255,255,255,alpha[GetUnitAbilityLevel(s.u,AID_BLUR)])
endfunction
private function Conditions takes nothing returns boolean
return GetLearnedSkill()==AID_BLUR and IsUnitIllusion(GetTriggerUnit())==false
endfunction
private function Actions takes nothing returns nothing
local StructUnit s=StructUnit.create()
set s.u=GetTriggerUnit()
call SetUnitVertexColor(s.u,255,255,255,alpha[GetUnitAbilityLevel(s.u,AID_BLUR)])
call SetTimerStructA(s.t,s)
call TimerStart(s.t,2,true,function Refresh)
endfunction
public function InitTrig takes nothing returns nothing
local trigger trig=CreateTrigger()
set alpha[1] = 90
set alpha[2] = 65
set alpha[3] = 40
set alpha[4] = 20
call TriggerRegisterAnyUnitEventBJ(trig,EVENT_PLAYER_HERO_SKILL)
call TriggerAddCondition(trig,Condition(function Conditions))
call TriggerAddAction(trig,function Actions)
endfunction
endscope
Functions inside of a scope can be declared as private or public, just like structs. However, unlike methods, there is no need for a static function, and thus do not exist. Unlike structs, triggers should have only one entry point: your InitTrig function. This is so that you do not accidentally use a function you are not suppose to, which will help eliminate errors. So you will notice inside of the scope that all my functions are private EXCEPT InitTrig, which is declared public. InitTrig is the entry point, of course.
Also, notice that I renamed my functions. I dropped the Blur_ prefixes. In scopes, you do not need them, as the function gets the Blur_ prefix automatically. The exception to this rule is InitTrig, which it sticks _Blur after it, but only if you declare it as public which you should do anyways. The automatic prefixes help when you are transferring spells from map to map, as well as when you have conflicting trigger names. With scopes, just change the scope name. It is far easier than having to rename each function to have the <triggername>_ prefix.
Last edited: