Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript: 동적 언어 소개

소개

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

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

동적 성질

동적 타이핑의 장단점

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

  • 언어가 간결하고 배우기 쉽습니다.

  • 대부분의 코드를 쉽고 빠르게 작성하고 변경할 수 있습니다.

  • 코드는 읽기 쉽습니다(약간 복잡함).

  • 테스트하기 위한 컴파일 작업이 필요 없습니다.

  • 런타임이 작습니다.

  • 덕 타이핑(Duck-typing)과 폴리모피즘(polymorphism).

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

  • 정적 타입 언어보다 낮은 퍼포먼스.

  • 리팩토링하기 더 어려움(기호(symbol)를 추적할 수 없음)

  • 정적 타입 언어에서 컴파일 시간에 감지되는 오류가 오직 코드를 실행할 때만 나타납니다 (정적 타입 언어에서의 표현 구문 분석이 더 엄격하기 때문입니다).

  • 코드 완성에 덜 유연함 (일부 변수 타입은 런타임에만 알려집니다).

이것은 현실로 번역하면 Godot+GDScript가 게임을 빠르고 효율적으로 생성하도록 설계된 조합임을 의미합니다. 매우 계산 집약적이고 엔진 내장 툴(예: 벡터 타입, 물리 엔진, 수학 라이브러리 등)의 이점을 얻을 수 없는 게임의 경우 C++를 사용할 수 있는 가능성도 있습니다. 이를 통해 여전히 GDScript에서 대부분의 게임을 생성하고 성능 향상이 필요한 영역에만 약간의 C++를 추가할 수 있습니다.

변수 및 할당

동적 타입 언어의 모든 변수는 "변이형(variant)"과 유사합니다. 즉, 변수 타입은 고정되어 있지 않고 할당을 통해서만 수정됩니다. 예제:

정적 언어:

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.

포인터 및 참조:

C 나 C++ (그리고 어느 정도는 Java와 C#) 같은 정적 언어에서, 변수와 변수에 대한 포인터/참조는 구별됩니다. 후자는 원래의 참조를 전달하여 다른 함수에 의해 오브젝트가 수정되도록 할 수 있습니다.

C#, Java에서 내장 타입(int, float, 때로는 String)이 아닌 모든 것은 항상 포인터 또는 참조입니다. 참조도 자동으로 가비지 컬렉터가 수집하므로 더 이상 사용하지 않으면 지워집니다. 동적으로 타입이 지정된 언어도 이 메모리 모델을 사용하는 경향이 있습니다. 몇 가지 예:

  • 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에서는 기본 타입(int, float, String, Vector 타입)만 값으로 함수에 전달됩니다(값이 복사됨). 다른 모든 것(인스턴스, 배열, 딕셔너리 등)은 참조로 전달됩니다. class_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] # You can mix types.
array.resize(3) # Can be resized.
use_array(array) # Passed as reference.
# Freed when no longer in use.

동적 타입 언어에서, 배열은 리스트(list)와 같은 다른 데이터 타입으로도 사용할 수도 있습니다:

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!")

딕셔너리

사전은 동적 유형 언어의 강력한 도구입니다. GDScript에서는 정적으로 유형이 지정된 언어가 다른 데이터 구조를 사용하는 경향이 있는 많은 경우에 유형이 지정되지 않은 사전을 사용할 수 있습니다.

딕셔너리는 키나 값으로 사용되는 데이터 타입을 완전히 무시하고 모든 값을 다른 값에 매핑할 수 있습니다. 일반적인 편견과 달리 딕셔너리는 해시 테이블로 구현될 수 있기 때문에 효율적입니다. 실제로 일부 언어는 배열을 딕셔너리로 구현하기까지 할 정도로 효율적입니다.

딕셔너리의 예제:

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

딕셔너리 또한 동적이기에, 적은 비용으로 어디에나 키를 추가하거나 제거할 수 있습니다:

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

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

# 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 position.
        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))

딕셔너리는 데이터 마크업이나 빠른 구조에도 사용될 수 있습니다. GDScript 딕셔너가 python 딕셔너리와 닮은 한편, GDScript는 초기 상태와 빠른 구조체를 작성하는 데 유용한 Lua 스타일 문법과 인덱싱도 지원합니다:

# 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: %c Index: %d\n", strings[i], i);
}

// Even in STL:
std::list<std::string> strings;

[..]

for (std::string::const_iterator it = strings.begin(); it != strings.end(); it++) {
    std::cout << *it << std::endl;
}

이 때문에 GDScript는 대신 반복 가능 항목에 대해 for-in 루프를 사용하기로 독선적인 결정을 내립니다.

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 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.

C 스타일 for 루프와 관련된 몇 가지 예:

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() 루프는 모든 곳에서 동일합니다:

var i = 0

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

커스텀 반복자(Iterator)

You can create custom iterators in case the default ones don't quite meet your needs by overriding _iter_init(), _iter_next(), and _iter_get() functions in your script. An example implementation of a forward iterator follows:

class ForwardIterator:
    var _start
    var _end
    var _increment

    func _init(start, end, increment):
        _start = start
        _end = end
        _increment = increment

    func _should_continue(current):
        return current < _end

    func _iter_init(iter):
        # Initialize the state to store the current value.
        iter[0] = _start
        return _should_continue(iter[0])

    func _iter_next(iter):
        iter[0] += _increment
        return _should_continue(iter[0])

    func _iter_get(iter):
        # The state is not wrapped in an array for `_iter_get()`.
        # The iteration value is the same as the state.
        return iter

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

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

It is possible but discouraged to store the state in a member variable. Multiple states are necessary in cases such as nested loops where the same iterator instance is used simultaneously. The iter parameter in _iter_init() and _iter_next() is a single-element array so that updates can persist. Whereas in _iter_get(), the state is is not wrapped because it is supposed to be read-only.

Returning true from _iter_init() and _iter_next() indicates that the iterator is valid. Returning false will terminate the loop.

For more details see _iter_init(), _iter_next(), and _iter_get().

덕 타이핑(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() 메서드가 있으면 호출됩니다. 상속이나 다형성이 필요하지 않습니다. 동적 타입 언어는 원하는 메서드 또는 멤버가 있는 인스턴스에만 관심이 있으며 상속 대상이나 클래스 유형이 아닙니다. 덕 타이핑의 정의는 이를 더 명확하게 만듭니다:

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

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

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

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

충돌한 물체에 smash() 함수가 없을 수도 있습니다. 일부 동적 타입 언어는 존재하지 않는 메서드 호출은 간단히 무시하지만 GDScript는 더 엄격하므로 함수가 존재하는지 확인하는 것이 바람직합니다:

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

그런 다음 메서드를 정의하기만 하면 바위가 닿는 모든 것이 박살날 수 있습니다.