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.
Checking the stable version of the documentation...
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()
그런 다음 메서드를 정의하기만 하면 바위가 닿는 모든 것이 박살날 수 있습니다.