GDScript: An introduction to dynamic languages

About

This tutorial aims to be a quick reference for how to use GDScript moreefficiently. It focuses on common cases specific to the language, butalso covers a lot of information on dynamically typed languages.

It's meant to be especially useful for programmers with little or no previousexperience with dynamically typed languages.

Dynamic nature

Pros & cons of dynamic typing

GDScript is a Dynamically Typed language. As such, its main advantagesare that:

  • The language is easy to get started with.

  • Most code can be written and changed quickly and without hassle.

  • The code is easy to read (little clutter).

  • No compilation is required to test.

  • Runtime is tiny.

  • It has duck-typing and polymorphism by nature.

While the main disadvantages are:

  • Less performance than statically typed languages.

  • More difficult to refactor (symbols can't be traced).

  • Some errors that would typically be detected at compile time instatically typed languages only appear while running the code(because expression parsing is more strict).

  • Less flexibility for code-completion (some variable types are onlyknown at runtime).

This, translated to reality, means that Godot used with GDScript is a combinationdesigned to create games quickly and efficiently. For games that are verycomputationally intensive and can't benefit from the engine built-intools (such as the Vector types, Physics Engine, Math library, etc), thepossibility of using C++ is present too. This allows you to still create most of thegame in GDScript and add small bits of C++ in the areas that needa performance boost.

Variables & assignment

All variables in a dynamically typed language are "variant"-like. Thismeans that their type is not fixed, and is only modified throughassignment. Example:

Static:

inta;// Value uninitialized.a=5;// This is valid.a="Hi!";// This is invalid.

Dynamic:

vara# 'null' by default.a=5# Valid, 'a' becomes an integer.a="Hi!"# Valid, 'a' changed to a string.

As function arguments:

Functions are of dynamic nature too, which means they can be called withdifferent arguments, for example:

Static:

voidprint_value(intvalue){printf("value is %i\n",value);}[..]print_value(55);// Valid.print_value("Hello");// Invalid.

Dynamic:

funcprint_value(value):print(value)[..]print_value(55)# Valid.print_value("Hello")# Valid.

Pointers & referencing:

In static languages, such as C or C++ (and to some extent Java and C#),there is a distinction between a variable and a pointer/reference to avariable. The latter allows the object to be modified by other functionsby passing a reference to the original one.

In C# or Java, everything not a built-in type (int, float, sometimesString) is always a pointer or a reference. References are alsogarbage-collected automatically, which means they are erased when nolonger used. Dynamically typed languages tend to use this memory model,too. Some Examples:

  • C++:

voiduse_class(SomeClass*instance){instance->use();}voiddo_something(){SomeClass*instance=newSomeClass;// Created as pointer.use_class(instance);// Passed as pointer.deleteinstance;// Otherwise it will leak memory.}
  • Java:

@Overridepublicfinalvoiduse_class(SomeClassinstance){instance.use();}publicfinalvoiddo_something(){SomeClassinstance=newSomeClass();// Created as reference.use_class(instance);// Passed as reference.// Garbage collector will get rid of it when not in// use and freeze your game randomly for a second.}
  • GDScript:

funcuse_class(instance):# Does not care about class typeinstance.use()# Will work with any class that has a ".use()" method.funcdo_something():varinstance=SomeClass.new()# Created as reference.use_class(instance)# Passed as reference.# Will be unreferenced and deleted.

In GDScript, only base types (int, float, string and the vector types)are passed by value to functions (value is copied). Everything else(instances, arrays, dictionaries, etc) is passed as reference. Classesthat inheritRefCounted (the default if nothing is specified)will be freed when not used, but manual memory management is allowed tooif inheriting manually fromObject.

Arrays

Arrays in dynamically typed languages can contain many different mixeddatatypes inside and are always dynamic (can be resized at any time).Compare for example arrays in statically typed languages:

int*array=newint[4];// Create array.array[0]=10;// Initialize manually.array[1]=20;// Can't mix types.array[2]=40;array[3]=60;// Can't resize.use_array(array);// Passed as pointer.delete[]array;// Must be freed.// orstd::vector<int>array;array.resize(4);array[0]=10;// Initialize manually.array[1]=20;// Can't mix types.array[2]=40;array[3]=60;array.resize(3);// Can be resized.use_array(array);// Passed reference or value.// Freed when stack ends.

And in GDScript:

vararray=[10,"hello",40,60]# You can mix types.array.resize(3)# Can be resized.use_array(array)# Passed as reference.# Freed when no longer in use.

In dynamically typed languages, arrays can also double as otherdatatypes, such as lists:

vararray=[]array.append(4)array.append(5)array.pop_front()

Or unordered sets:

vara=20ifain[10,20,30]:print("We have a winner!")

Dictionaries

Dictionaries are a powerful tool in dynamically typed languages. InGDScript, untyped dictionaries can be used for many cases where a staticallytyped language would tend to use another data structure.

Dictionaries can map any value to any other value with completedisregard for the datatype used as either key or value. Contrary topopular belief, they are efficient because they can be implementedwith hash tables. They are, in fact, so efficient that some languageswill go as far as implementing arrays as dictionaries.

Example of Dictionary:

vard={"name":"John","age":22}print("Name: ",d["name"]," Age: ",d["age"])

Dictionaries are also dynamic, keys can be added or removed at any pointat little cost:

d["mother"]="Rebecca"# Addition.d["age"]=11# Modification.d.erase("name")# Removal.

In most cases, two-dimensional arrays can often be implemented moreeasily with dictionaries. Here's a battleship game example:

# Battleship GameconstSHIP=0constSHIP_HIT=1constWATER_HIT=2varboard={}funcinitialize():board[Vector2(1,1)]=SHIPboard[Vector2(1,2)]=SHIPboard[Vector2(1,3)]=SHIPfuncmissile(pos):ifposinboard:# Something at that position.ifboard[pos]==SHIP:# There was a ship! hit it.board[pos]=SHIP_HITelse:print("Already hit here!")# Hey dude you already hit here.else:# Nothing, mark as water.board[pos]=WATER_HITfuncgame():initialize()missile(Vector2(1,1))missile(Vector2(5,8))missile(Vector2(2,3))

Dictionaries can also be used as data markup or quick structures. WhileGDScript's dictionaries resemble python dictionaries, it also supports Luastyle syntax and indexing, which makes it useful for writing initialstates and quick structs:

# Same example, lua-style support.# This syntax is a lot more readable and usable.# Like any GDScript identifier, keys written in this form cannot start# with a digit.vard={name="John",age=22}print("Name: ",d.name," Age: ",d.age)# Used "." based indexing.# Indexingd["mother"]="Rebecca"d.mother="Caroline"# This would work too to create a new key.

For & while

Iterating using the C-style for loop in C-derived languages can be quite complex:

constchar**strings=newconstchar*[50];[..]for(inti=0;i<50;i++){printf("Value: %c Index: %d\n",strings[i],i);}// Even in STL:std::list<std::string>strings;[..]for(std::string::const_iteratorit=strings.begin();it!=strings.end();it++){std::cout<<*it<<std::endl;}

Because of this, GDScript makes the opinionated decision to have a for-in loop over iterables instead:

forsinstrings:print(s)

Container datatypes (arrays and dictionaries) are iterable. Dictionariesallow iterating the keys:

forkeyindict:print(key," -> ",dict[key])

Iterating with indices is also possible:

foriinrange(strings.size()):print(strings[i])

The range() function can take 3 arguments:

range(n)# Will count from 0 to n in steps of 1. The parameter n is exclusive.range(b,n)# Will count from b to n in steps of 1. The parameters b is inclusive. The parameter n is exclusive.range(b,n,s)# Will count from b to n, in steps of s. The parameters b is inclusive. The parameter n is exclusive.

Some examples involving C-style for loops:

for(inti=0;i<10;i++){}for(inti=5;i<10;i++){}for(inti=5;i<10;i+=2){}

Translate to:

foriinrange(10):passforiinrange(5,10):passforiinrange(5,10,2):pass

And backwards looping done through a negative counter:

for(inti=10;i>0;i--){}

Becomes:

foriinrange(10,0,-1):pass

While

while() loops are the same everywhere:

vari=0whilei<strings.size():print(strings[i])i+=1

Custom iterators

You can create custom iterators in case the default ones don't quite meet yourneeds by overriding the Variant class's_iter_init,_iter_next, and_iter_getfunctions in your script. An example implementation of a forward iterator follows:

classForwardIterator:varstartvarcurrentvarendvarincrementfunc_init(start,stop,increment):self.start=startself.current=startself.end=stopself.increment=incrementfuncshould_continue():return(current<end)func_iter_init(arg):current=startreturnshould_continue()func_iter_next(arg):current+=incrementreturnshould_continue()func_iter_get(arg):returncurrent

And it can be used like any other iterator:

varitr=ForwardIterator.new(0,6,2)foriinitr:print(i)# Will print 0, 2, and 4.

Make sure to reset the state of the iterator in_iter_init, otherwise nestedfor-loops that use custom iterators will not work as expected.

Duck typing

One of the most difficult concepts to grasp when moving from astatically typed language to a dynamic one is duck typing. Duck typingmakes overall code design much simpler and straightforward to write, butit's not obvious how it works.

As an example, imagine a situation where a big rock is falling down atunnel, smashing everything on its way. The code for the rock, in astatically typed language would be something like:

voidBigRollingRock::on_object_hit(Smashable*entity){entity->smash();}

This way, everything that can be smashed by a rock would have toinherit Smashable. If a character, enemy, piece of furniture, small rockwere all smashable, they would need to inherit from the class Smashable,possibly requiring multiple inheritance. If multiple inheritance wasundesired, then they would have to inherit a common class like Entity.Yet, it would not be very elegant to add a virtual methodsmash() toEntity only if a few of them can be smashed.

With dynamically typed languages, this is not a problem. Duck typingmakes sure you only have to define asmash() function where requiredand that's it. No need to consider inheritance, base classes, etc.

func_on_object_hit(object):object.smash()

And that's it. If the object that hit the big rock has a smash() method,it will be called. No need for inheritance or polymorphism. Dynamicallytyped languages only care about the instance having the desired methodor member, not what it inherits or the class type. The definition ofDuck Typing should make this clearer:

"When I see a bird that walks like a duck and swims like a duck andquacks like a duck, I call that bird a duck"

In this case, it translates to:

"If the object can be smashed, don't care what it is, just smash it."

Yes, we should call it Hulk typing instead.

It's possible that the object being hit doesn't have a smash() function.Some dynamically typed languages simply ignore a method call when itdoesn't exist, but GDScript is stricter, so checking if the functionexists is desirable:

func_on_object_hit(object):ifobject.has_method("smash"):object.smash()

Then, define that method and anything the rock touches can be smashed.


User-contributed notes

Please read theUser-contributed notes policy before submitting a comment.