The purpose of this tutorial is to introduce some of the most used vJASS features in a nice and understandable way. The tutorial is formatted into chapters which can be read separately. However, I do recommend learning chapter two before heading to chapter three because structs and interfaces are closely bound to each other.
In the beginning of each chapter I will explain the main definitions the chapter is about. When explaining, I may rely on the concepts brought out in the glossary, so it's advised to read through the glossary as well.
If you have taken up JASS recently and don't feel yourself comfortable using it then this is not a tutorial for you. I highly suggest learning the basics and the API and getting familiar with the normal JASS' features and syntax before reading this tutorial.
- Authentic copy of Warcraft 3: The Frozen Throne
- Latest JASS NewGen Package
- Intermediate knowledge in JASS
TABLE OF CONTENTS
Chapter one – encapsulation
- scopes Chapter two – object composition
- modules Chapter three – polymorphism
- interfaces Chapter four - manual labour
- textmacros Ending words
I will be using general computer terms when explaining the features of vJASS. Despite vJASS is compiled to regular JASS when saved, it still offers the concepts of object oriented programming (OOP). Therefore, general computer terms which apply to any OOP language can be used (with certain restrictions or modifications, of course) when explaining vJASS.
ABSTRACTION - A mechanism and practice to reduce and factor out details so that one can focus on a few concepts at a time.
SUBROUTINE - A portion of code within a larger program, which performs a specific task.
e.g a function
ELEMENT - An abstract part of something.
e.g a variable inside a struct
CLASS - A set of objects having the same behavior.
e.g structs, libraries, scopes etc.
OBJECT - An instance of a class.
CHAPTER ONE – ENCAPSULATION
Back in the days without vJASS, one had to put all his „global“ functions to the map header. It was a working concept but it got awfully difficult to separate functions by their purpose when the amount of code increased. Not to mention possible collisions due to functions having the same names (e.g very possible when importing different systems).
vJASS allows us protect our code from collisions more easily by introducing encapsulation – the freedom to declare private or public objects.
ENCAPSULATION - The grouping of data and the code that manipulates it into a single object to protect it from unwanted access or alteration.
LIBRARY - A collection of standard programs and subroutines that are stored and available for immediate use.
vJASS introduces libraries – the ability to group data, move it to the map header and make parts of it inaccessible to certain parts of other code. To declare a library, you must use the „library“ and „endlibrary“ keyword.
As you can see, there are three types of functions inside the library seen in example 1 – private, public and normal functions.
Private functions are denoted by the „private“ keyword and are not accessible outside the library – you can only call the function internally. When you try to call a private function externally, you will get an error saying „undeclared function“.
The public is denoted by the keyword „public“. It shares similarities with a normal function, as it can be used outside the library. The only difference is that you must use the library's name as prefix when calling that function (as seen in example 1).
The „private“ and „public“ keywords can be used in conjuration with other objects as well: such as structs, their elements, interfaces etc.
It is possible to arrange a library to be dependant on another library:
In order to make a library to be dependant on another one, you must use „requires“, „needs“ or „uses“ keywords. All of these options are valid. If a certain library needs more than one other library, you must separate the members of the library requirement list with commas.
NOTE: It is possible to create a endless loop with library requirements. e.g library A needs library B, but library B needs library A. It is best to avoid such of a situation, because it will not compile.
An illustration of the process how libraries are arranged and moved to the map header:
SCOPE - An enclosing context where values and expressions are associated.
Scopes are very similar to libraries, only the content in them is not moved to the map header. This is particurarly good when coding spells and systems, where you might want to use the same name for certain functions or globals variables (e.g), and don't want collisions (function redeclared error e.g) between them.
To declare a scope, you must use the „scope“ and „endscope“ keywords. The private function „doSomething“ is inaccessible outside the scope, which is the effect we are looking for.
Libraries and scopes can have initializers (as you can see in example 3) – functions that run on map initialization when the library is compiled. Initializers are denoted by the „initializer“ keyword and require you to supply it with a take-nothing, return-nothing function. Initializer function must be declared after the scope or library declaration but before the requirement list.
In order to execute private functions inside a scope, you have to use the „SCOPE_PRIVATE“ prefix, because scopes make the functions private by adding that said prefix to the function names when the code is compiled.
CHAPTER TWO – OBJECT COMPOSITION
STRUCT – A simple data structure which consists of elements.
Think of structs as a way to bind different elements into a single object; the elements inside the object affect the way the object performs.
e.g lets imagine a box as a struct. The box would have three dimensions – length, width and height. These dimensions would be the elements which characterise the box. In addition, boxes can be made out of different materials and come in a wide variety of colours. So the material and the colour are also parameters which describe the box.
As you can see from example 1, declaring structs is similar to declaring global variables - „struct“ to mark the beginning of a struct and „endstruct“ to note the end of it.
Next, you can declare variables inside a struct in three different ways – variables that are uninitialized or initialized and uninitialized arrays. Note how you must specify the size of the array to declare it.
NOTE: You can have a maximum of 8190 instances of a struct of certain type. When you exceed the limit you are no longer able to create that struct type as it will return 0.
In order not to reach the limit, you must destroy the struct after you're done with it using the .destroy method (see next subchapter - methods).
NOTE: Arrays inside the struct affect the instance limit - 8190 is divided with the largest array index to determine the instance limit of the given struct.
Structs can extend eachother to inherit the properties and code. To extend a struct, you must use the „extends“ keyword. There has to be at least one parent struct – a struct that does not extend anything.
You can imagine your family as structs. Your parents are parent structs and you are a child struct which extends the parent structs. Now, when you take a look at your parents you see resemblances – you might have the same hair, eye and skin tone. You have inherited their genes or parts of them and that affects how you look.
When you take a look at example 2 the struct „child“ extends struct „parent“ and therefore the struct "child" can use the members of the „parent“ struct. If you have any methods (see below), the child struct can use them as well.
METHOD - A subroutine that is exclusively associated with an object.
Methods are very similar to functions and in essence, they are functions, only associated with a certain struct.
There are two types of methods, „normal“ and static methods. Normal methods are associated with every struct instance and static methods aren't. This means that static methods behave like global variables – you can change their values indefinitely.
As you take a look at example 3, you will notice some really weird syntax if you've never dealt with vJASS before. Lets approach the example systematically.
To declare a method, use the „method“ and „endmethod“ keywords. Methods are like functions – they can have a parameter list and they can return a value.
Next, as you've have noticed already, the method in example 2 is static, denoted by the „static“ keyword in front of the method declaration. In addition, you might ask „what does the .allocate method do?“. Shortly, .allocate method gives the struct its unique instance number (when you reach the instance limit, it will return 0, like mentioned previously).
Now, when you take a look inside the method, you will notice this bizarre dot-syntax. The dot syntax is there to access the elements of the struct. Explained shortly, I allocate the struct „newBox“, which is of type box, an unique instance number; use the „newBox“ reference name to gain access to the elements of that struct instance and assign values to them; lastly, I return the struct to the function which called the .create method.
All defined structs have a few default methods which can be used to manipulate the data inside the struct; and for some of them, you can declare your own version.
• .create – to create a struct (example 3)
• .destroy - to destroy a struct (cannot have your own version)
• .allocate – to retrieve the struct's unique index number (cannot have your own version)
• .onDestroy – runs upon destruction
• .onInit – runs upon map initialization
Firstly, as you can notice, I omitted declaring a custom .create method and used the default one. By default, it doesn't have a parameter list.
Next, lets move to inside the struct. As you can see, the „static“ keyword can be used in conjuration with variables as well. As a matter of fact, all variables which are about to be used in a static method without a struct reference name must be static (in example 3, I could use non-static variables only because I used the struct reference name „newBox“; this is always not possible or needed, hence the need of static members).
When the method .onInit run on map initialization, the instance (non-static) .onDestroy method runs when you call the „.destroy“ method to get rid of the struct.
Before proceeding to modules, I want to bring one last example - how to create and call a „callback“ method inside a struct.
The callback method must be static so it could be used as a parameter for the „TimerStart“ function. If you think a little, ... „data.onCallback“ is treated like a function. That's because static methods in essence are functions (as are non-static methods).
MODULE - A „container“ of specifications and definitions to be used in other objects.
Sometimes, you have a whole bunch of different structs which need the same set of methods or variables. It would be really tiresome to copy&paste the elements to all of the structs. Not to mention when you decide to change the code...You should copy&paste the changed coded all over again.
MODULAR PROGRAMMING - A software design technique that increases the extent to which code is composed from separate parts, modules.
Modules introduce one aspect of modular programming to JASS. Lets take a look at an example.
To declare a module, you must use the „module“ and „endmodule“ keywords. To implement a module to a struct, you must use the „implement“ keyword.
As you can see from the example, I declared only a single module, but try to implement two modules to the struct. Funnily enough, this compiles and doesn't give any errors. That's because I've used the „optional“ keyword after implementation. Basically, all modules that are non-existant (due to updates and modifications to code; e.g system updates) but have the „optional“ keyword will be ignored and don't give errors. If you try to implement a module which is non-existant to a struct, but don't use the „optional“ keyword, it will error and notify you of that.
Next, in the module, you can also notice the „thistype“ keyword. It's needed to access static elements (because static members need struct reference name to be accessed) – when you create a module and give it content which deals with static members, the module doesn't know the type of the struct you are going to implement it in, hence the need for „thistype“ keyword. Basically, „thistype“ performs as the struct's name in the struct it's used.
Example 5 would compile into the following piece of code:
CHAPTER THREE – POLYMORPHISM
POLYMORPHISM- A programming language feature that allows values of different data types to be handled using a uniform interface.
INTERFACE - Interface is what or how unrelated objects use to communicate with each other. These are definitions of methods and values which the objects agree upon in order to cooperate.
Interfaces are needed when you want to call a certain method inside the struct, but you don't know the type of the struct (e.g). You can imagine interfaces as set of rules or „actions“.
Lets imagine a little jumping contest with two participants – Thomas and Billy. Both of them can jump, but Billy is technically a lot better than Thomas and will probably win. The contestants jumping order is decided by a draw.
The interface in example 1 represents the motions both Thomas and Billy can do, in detail, jumping. To declare a interface, you must use the „interface“ and „endinterface“ keywords.
Next, like said earlier, Billy is more trained and jumps higher than Thomas. Their level of jumping skill is represented by two structs - „beginner“ and „pro“. The structs contain a method which will return the jumping height, dependant on the struct. As you can see from the example, the structs extend the interface, because no matter how high you jump, the basic action is still jumping.
Moving on, you can spot the „GetContestantJumpHeight“ function which takes the interface „motions“ as a parameter. There is a reason for that.
Imagine the contestants now, ready to jump and set their mark. In order to get to know their results, they must jump – call the „.jumpUp„ method which returns the result. However, the structs, which represent the contestants, are of different types and it's awfully painstaking to code „GetThomasJumpHeight“, „GetBillyJumpHeight“ etc. functions. That's where the interface comes to play.
The interface allows you pass either of the structs as a parameter and the correct method would be called. e.g if you'd pass „Billy“ to „GetContestantJumpHeight“, the method inside the „pro“ struct will be ran.
When you want to make a implementing a method optional inside a interface, you have to use the „defaults“ keyword, which is followed a value (normally, „nothing“).
In example 2, both of the structs extend the same interface, but implementing the method „bark“ is optional. The struct „dog“ has it whereas „human“ doesn't.
When the function „IssueToBark“ is called and a struct of type „dog“ is passed, it will run the method „.bark“ inside the given struct. Now, when the struct human is passed, you might think it will error because the struct doesn't have the method „.bark“. That's not true. The „defaults nothing“ part in the interface makes sure that when you call an non-existant method it will do and return nothing (or return a value if you defaulted it to a value).
You can check if a function is non-existant or not by using the „.exists“ method. It will return true if the struct you're testing has the method and false otherwise.
Note how the method ".exists" is used in conjuration with the "Jerry.bark" accessing part - it's separed with an additional dot plus it uses no parenthesis.
CHAPTER FOUR – MANUAL LABOUR
TEXTMACRO - A computer science term for specific textual replacement patterns which follow certain sets of intructions.
As the definition says, textmacros allow you to replace text with the content you desire. This is particurarly good, if you need to create multiple similar functions (e.g), that take too long to manually copy&paste and modify.
Declaring a textmacro is similar to declaring a function – you must specify the beginning and the ending of it plus give it a name. „//! textmacro“ is used to mark the beginning, „//! endtextmacro“ to note the ending of the textmacro.
To replace something with the content you want, you must add a parameter to the textmacro and use the same parameter where replacing is needed. In order to tell the machine when to start replacing, you must enclose the parameter with the „$“ signs, as seen in example 1.
Lastly, to run the textmacro, you must use the „//! runtextmacro“ command together with the needed textmacro and its parameters. As seen in the example, all parameters must be enclosed with quotation marks („“).
Example 1 would compile into the following piece of code:
As seen from example 2, you can replace the content of a function (e.g). Replace isn't actually the right word, assemble should fit better, instead. Note how the contest is „floating“, only enclosed with a textmacro.
Example 2 would compile into the following piece of code:
These were the most used features of vJASS, in my opinion at least. Take our time to read through parts of the tutorial if you didn't understand something.
As it is with learning, the key is practise. Try to write vJASS-fied code yourself and practise it thoroughly.
That's all from me, feedback is appreciated and happy mapping!