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 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: %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
맞춤 반복자(Iterator)¶
기본 반복자가 필요에 맞지 않는다면 변수 클래스의 스크립트의 _iter_init
, _iter_next
, 그리고 _iter_get
함수를 오버라이딩하여 맞춤 반복자를 생성하실 수 있습니다. forward 반복자의 예제 구현은 다음과 같습니다:
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() 함수를 갖지 않을 수 있습니다. 일부 동적 타입 언어는 존재하지 않는 메서드 호출은 간단히 무시합니다 (Objective C 처럼 말이죠), 하지만 GDScript는 더 엄격합니다, 그래서 함수가 존재하는 지 확인하는 것이 바람직합니다:
func _on_object_hit(object):
if object.has_method("smash"):
object.smash()
그런 다음, 간단히 바위에 닿는 모든 것들을 메서드로 정의하여 박살날 수 있도록 합니다.