GDScript: 동적 언어 소개

정보

이 튜토리얼은 GDScript를 더 효과적으로 사용하는 방법에 대한 빠른 참고 문헌입니다. 언어와 관련된 일반적인 사례 뿐만 아니라, 동적 타입 언어의 많은 정보도 다루고 있습니다.

이전까지 동적 타입 언어에 대한 경험이 없던 프로그래머에게 이 문서는 특히 더 유용합니다.

동적 성격

동적 타이핑의 장단점

GDScript는 동적 타입 언어입니다. 따라서, 주요 이점은 다음과 같습니다:

  • 언어가 간결하고 배우기 쉽습니다.
  • 대부분의 코드를 쉽고 빠르게 작성하고 변경할 수 있습니다.
  • 적은 코드 작성으로 오류와 실수를 줄일 수 있습니다.
  • 코드를 읽기 더 쉽습니다 (덜 혼란스러움).
  • 테스트하기 위한 컴파일 작업이 필요 없습니다.
  • 런타임이 작습니다.
  • 덕 타이핑(Duck-typing)과 폴리모피즘(polymorphism).

반면 주요 단점은 다음과 같습니다:

  • 정적 타입 언어보다 낮은 퍼포먼스.
  • refactor하기 더 어려움(기호를 추적할 수 없음)
  • 정적 타입 언어에서 컴파일 시간에 감지되는 오류가 오직 코드를 실행할 때만 나타납니다 (표현 구문 분석이 더 엄격하기 때문입니다).
  • 코드 완성에 덜 유연함 (일부 변수 타입은 런타임에만 알려짐).

This, translated to reality, means that Godot+GDScript are a combination designed to create games quickly and efficiently. For games that are very computationally intensive and can't benefit from the engine built-in tools (such as the Vector types, Physics Engine, Math library, etc), the possibility of using C++ is present too. This allows you to still create most of the game in GDScript and add small bits of C++ in the areas that need a performance boost.

변수 & 지정

동적 타입 언어의 모든 변수는 "변형"과 유사합니다. 즉, 그들의 타입은 고정되어 있지 않고, 오직 지정을 통해 수정됩니다. 예제:

정적 언어:

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

동적 언어:

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

함수 인수로:

함수도 동적인 특성을 지닙니다, 그래서 함수를 다른 인수로 호출할 수 있습니다, 예를 들어:

정적 언어:

void print_value(int value) {

    printf("value is %i\n", value);
}

[..]

print_value(55); // Valid
print_value("Hello"); // Invalid

동적 언어:

func print_value(value):
    print(value)

[..]

print_value(55) # Valid
print_value("Hello") # Valid

포인터(Pointer) & 참조:

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 a variable. The latter allows the object to be modified by other functions by passing a reference to the original one.

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

  • C++:
void use_class(SomeClass *instance) {

    instance->use();
}

void do_something() {

    SomeClass *instance = new SomeClass; // Created as pointer
    use_class(instance); // Passed as pointer
    delete instance; // Otherwise it will leak memory
}
  • Java:
@Override
public final void use_class(SomeClass instance) {

    instance.use();
}

public final void do_something() {

    SomeClass instance = new SomeClass(); // 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:
func use_class(instance); # Does not care about class type
    instance.use() # Will work with any class that has a ".use()" method.

func do_something():
    var instance = SomeClass.new() # Created as reference
    use_class(instance) # Passed as reference
    # Will be unreferenced and deleted

GDScript에서 기본 타입 (정수, 실수, 문자열 그리고 벡터 타입)만이 함수에 값으로 보낼 수 있습니다 (값은 복사됩니다). 나머지 (인스턴스, 배열, 딕셔너리 등)는 참조로 보내집니다. Reference 를 상속한 클래스 (지정한 것이 없으면 기본 값)는 사용되지 않을 때 해제되지만, Object 로 부터 수동으로 상속하는 경우 수동 메모리 관리가 가능합니다.

배열

동적 타입 언어에서의 배열은 다양한 혼합 데이터 타입을 포함할 수 있으며 항상 동적입니다 (어느 때나 크기가 조절될 수 있습니다). 정적 타입 언어의 배열과 비교하는 예제:

int *array = new int[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

// or

std::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

그리고 GDScript에서는:

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

동적 타입 언어에서, 배열은 목록과 같이, 다른 데이터 타입으로 두 배가 될 수도 있습니다:

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

또는 순서가 없는 세트가 될 수 있습니다:

var a = 20
if a in [10, 20, 30]:
    print("We have a winner!")

딕셔너리

Dictionaries are a powerful tool in dynamically typed languages. Most programmers that come from statically typed languages (such as C++ or C#) ignore their existence and make their life unnecessarily more difficult. This datatype is generally not present in such languages (or only in limited form).

딕셔너리는 키 또는 값으로 사용되는 데이터 타입을 무시하고 다른 값으로 매핑 할 수 있습니다. 대중적 신념과는 반대로, 그들은 해시 테이블로 구현될 수 있기 때문에 효과적입니다. 사실, 일부 언어는 배열을 딕셔너리로 구현하는 것이 더 효과적입니다.

딕셔너리의 예제:

var d = {"name": "John", "age": 22} # Simple syntax
print("Name: ", d["name"], " Age: ", d["age"])

딕셔너리 또한 동적이기에, 적은 손해로 어디에나 키를 추가하고 삭제할 수 있습니다:

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

대부분의 경우, 2 차원 배열은 종종 딕셔너리로 더 쉽게 구현될 수 있습니다. 여기 간단한 전함 게임 예제가 있습니다:

# Battleship game

const SHIP = 0
const SHIP_HIT = 1
const WATER_HIT = 2

var board = {}

func initialize():
    board[Vector2(1, 1)] = SHIP
    board[Vector2(1, 2)] = SHIP
    board[Vector2(1, 3)] = SHIP

func missile(pos):
    if pos in board: # Something at that pos
        if board[pos] == SHIP: # There was a ship! hit it
            board[pos] = SHIP_HIT
        else:
            print("Already hit here!") # Hey dude you already hit here
    else: # Nothing, mark as water
        board[pos] = WATER_HIT

func game():
    initialize()
    missile(Vector2(1, 1))
    missile(Vector2(5, 8))
    missile(Vector2(2, 3))

Dictionaries can also be used as data markup or quick structures. While GDScript's dictionaries resemble python dictionaries, it also supports Lua style syntax and indexing, which makes it useful for writing initial states 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.

var d = {
    name = "John",
    age = 22
}

print("Name: ", d.name, " Age: ", d.age) # Used "." based indexing

# Indexing

d["mother"] = "Rebecca"
d.mother = "Caroline" # This would work too to create a new key

For & while

일부 정적 타입 언어에서 반복은 꽤 복잡해질 수 있습니다:

const char* strings = new const char*[50];

[..]

for (int i = 0; i < 50; i++)
{

    printf("Value: %s\n", i, strings[i]);
}

// Even in STL:

for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) {

    std::cout << *it << std::endl;
}

보통 동적 타입 언어에서는 이것이 매우 단순화됩니다:

for s in strings:
    print(s)

컨테이너 데이터 타입 (배열과 딕셔너리)도 반복 가능합니다. 딕셔너리는 키를 반복할 수 있습니다:

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

인덱스로 반복하기도 가능합니다:

for i in range(strings.size()):
    print(strings[i])

range() 함수는 3개의 인수를 가질 수 있습니다:

range(n) # Will go from 0 to n-1
range(b, n) # Will go from b to n-1
range(b, n, s) # Will go from b to n-1, in steps of s

Some statically typed programming language examples:

for (int i = 0; i < 10; i++) {}

for (int i = 5; i < 10; i++) {}

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

번역:

for i in range(10):
    pass

for i in range(5, 10):
    pass

for i in range(5, 10, 2):
    pass

그리고 음수 카운터를 통해 역방향 루프가 됩니다:

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

이렇게 됩니다:

for i in range(10, 0, -1):
    pass

While

while() 루프는 모든 곳에서 동일합니다:

var i = 0

while i < strings.size():
    print(strings[i])
    i += 1

커스텀 반복자(Iterator)

기본 반복자가 필요에 맞지 않는다면 변수 클래스의 스크립트의 _iter_init, _iter_next, 그리고 _iter_get 함수를 오버라이딩하여 커스텀 반복자를 생성하실 수 있습니다. forward 반복자의 예제 구현은 다음과 같습니다:

class FwdIterator:
    var start, curr, end, increment

    func _init(start, stop, inc):
        self.start = start
        self.curr = start
        self.end = stop
        self.increment = inc

    func is_done():
        return (curr < end)

    func do_step():
        curr += increment
        return is_done()

    func _iter_init(arg):
        curr = start
        return is_done()

    func _iter_next(arg):
        return do_step()

    func _iter_get(arg):
        return curr

그리고 모든 다른 반복자처럼 사용될 수 있습니다:

var itr = FwdIterator.new(0, 6, 2)
for i in itr:
    print(i) # Will print 0, 2, and 4

_iter_init 에서 반복자의 상태를 리셋해야 합니다, 그렇지 않으면 커스텀 반복자에 사용하는 중첩된 for 루프가 예상대로 작동하지 않을 것입니다.

덕 타이핑(Duck typing)

정적 타입 언어에서 동적 타입 언어로 바꿀 때 이해하기 어려운 개념들 중 하나가 덕 타이핑입니다. 덕 타이핑은 전체 코드 설계를 더 간단하고 쓰기 쉽게 만들어주지만, 어떻게 작동하는지 분명하지 않습니다.

예제와 같이, 큰 바위가 터널로 떨어지고 있는 상황을 생각해보세요, 그 길의 모든 것들을 박살내 버립니다. 바위의 코드는, 정적 언어에서는 다음과 같이 될 것입니다:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

이 방법은, 바위에 의해 박살날 수 있는 모든 것들은 Smashable(박살날 수 있음)을 상속해야 합니다. 캐릭터, 적, 가구의 일부, 작은 돌들은 모두 박살날 수 있고, 그들은 클래스 Smashable로 부터 상속해야 하고, 거기에 다중 상속이 필요할 수도 있습니다. 다중 상속이 바람직하지 않은 경우, 그들은 Entity와 같은 일반 클래스에 상속해야 합니다. 그러나 Entity들 중 일부만이 박살날 수 있다고 해서 가상 메서드 smash() 를 추가하는 것은 그리 우아하지 않을 것입니다.

동적 타입 언어에서는, 문제없습니다. 덕 타이핑이 smash() 함수가 필요한 곳에만 정의하도록 만듭니다. 상속, 기본 클래스 등등을 고려할 필요가 없습니다.

func _on_object_hit(object):
    object.smash()

그리고 됐습니다. 큰 오리를 치는 물체가 smash() 메서드를 갖고 있다면, 호출될 것입니다. 상속이나 폴리모피즘이 필요 없습니다. 동적 타입 언어는 상속된 인스턴스나 클래스 타입이 아니라, 바람직한 메서드나 멤버를 가진 인스턴스가 맞는지 관리하시면 됩니다. 덕 타이핑의 정의는 이를 명확하게 만듭니다:

"내가 오리처럼 걷고 오리처럼 수영하고 오리처럼 우는 새를 볼 때, 나는 그 새를 오리라고 부른다"

우리의 경우에는, 다음과 같이 번역됩니다:

"물체를 박살낼 수 있다면, 무엇이든 상관없이, 그냥 박살낸다."

예, 우리는 이를 대신 헐크 타이핑이라 불러야겠네요.

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

func _on_object_hit(object):
    if object.has_method("smash"):
        object.smash()

그런 다음, 간단히 바위에 닿는 모든 것들을 메서드로 정의하여 박살날 수 있도록 합니다.