Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

GDScript: 동적 언어 소개

정보

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

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

동적 성질

동적 타이핑의 장단점

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

  • The language is easy to get started with.

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

  • 적은 코드 작성으로 오류와 실수를 줄일 수 있습니다.

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

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

  • 런타임이 작습니다.

  • It has duck-typing and polymorphism by nature.

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

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

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

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

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

This, translated to reality, means that Godot used with GDScript is 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.

변수 & 할당

동적 타입 언어의 모든 변수는 "변이형(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.

포인터(Pointer) & 참조:

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.

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. Classes that inherit RefCounted (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from 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()

또는 순서가 없는 집합(unordered set)이 될 수도 있습니다:

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

딕셔너리

딕셔너리는 동적 타입 언어에서 강력한 툴입니다. 정적 타입 언어(C++나 C#)를 사용하는 대부분의 프로그래머는 딕셔너리의 존재를 무시해서 불필요하게 삶을 더 어렵게 만듭니다. 이 데이터 타입은 일반적으로 그러한 언어에서 존재하지 않습니다(또는 제한된 형태로만 존재합니다).

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

딕셔너리의 예제:

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.

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

# 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

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

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;
    }

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

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.

Some examples involving C-style for loops:

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

And backwards looping done through a negative counter:

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)

스크립트에서 Variant 클래스의 _iter_init, _iter_next, _iter_get 함수를 재정의해서 기본 반복자가 필요에 맞지 않는 경우 커스텀 반복자를 만들 수 있습니다. 순방향 반복기의 구현 예는 다음과 같습니다.

class ForwardIterator:
    var start
    var current
    var end
    var increment

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

    func should_continue():
        return (current < end)

    func _iter_init(arg):
        current = start
        return should_continue()

    func _iter_next(arg):
        current += increment
        return should_continue()

    func _iter_get(arg):
        return current

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

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

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

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

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

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

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

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

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