GDScript: 동적 언어 소개

정보

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

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

동적 성격

동적 타이핑의 장단점

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

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

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

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

현실로 번역된 이 말은 Godot+GDScript가 게임을 빠르고 효율적으로 만들 수 있는 조합임을 의미합니다. 매우 계산 집약적이고 엔진 내장 도구 (벡터 타입, 물리 엔진, 수학 라이브러리 등)의 이점을 누릴 수 없는 게임의 경우, C++를 사용하는 경우의 수도 존재합니다. 이걸로 GDScript에서 전체적인 게임을, 그리고 퍼포먼스 가속이 필요한 영역에 약간의 C++를 첨가하는 것이 가능합니다.

변수 & 지정

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

정적 언어:

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에서, 모든 것이 내장 타입 (정수, 실수, 때때로 문자열)이 아닌 포인터나 참조입니다. 참조는 또한 자동으로 가비지를 모으는데, 더 이상 사용되지 않으면 삭제됩니다. 동적 타입 언어 역시 이런 메모리 모델을 사용하는 경향이 있습니다. 몇 가지 예제:

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

딕셔너리

딕셔너리는 동적 타입 언어에서 강력한 도구입니다. 정적 타입 언어 (C++나 C#)에서 온 대부분의 프로그래머들이 이 존재를 무시한 채 그들의 삶을 더 어렵게 만들고 있습니다. 이 데이터 타입은 일반적으로 그러한 언어 (또는 제한된 형태)로 존재하지 않습니다.

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

딕셔너리의 예제:

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

딕셔너리는 또한 데이터 마크업이나 빠른 구조에도 사용될 수 있습니다. 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: %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

일부 정적 타입 프로그래밍 언어 예제:

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

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

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

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

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

맞은 물체가 smash() 함수를 갖지 않을 수 있습니다. 일부 동적 타입 언어는 존재하지 않는 메서드 호출은 간단히 무시합니다 (Objective C 처럼 말이죠), 하지만 GDScript는 더 엄격합니다, 그래서 함수가 존재하는 지 확인하는 것이 바람직합니다:

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

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