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 파일에서만 사용하거나, 모든 곳에서 사용하거나, 모두 사용하지 않을 수도 있습니다.
정적 타입은 변수, 상수, 함수, 매개변수, 반환 타입에 사용될 수 있습니다.
정적 타이핑에 대한 간단한 설명
타입형 GDScript를 사용하면 Godot는 코드를 작성할 때 더 많은 오류를 감지할 수 있습니다! 메서드를 호출할 때 인수 타입이 표시되기 때문에 작업하는 동안 여러분과 팀원에게 더 많은 정보를 제공합니다.
인벤토리 시스템을 프로그래밍하고 있다고 상상해보세요. Item 노드를 코딩한 다음 Inventory를 코딩합니다. 인벤토리에 항목을 추가하려면 코더들이 항상 Item타입만 Inventory.add 메서드에 전달해야 합니다. 타입을 사용하면 다음을 강제할 수 있습니다:
class_name Inventory
func add(reference: Item, amount: int = 1):
var item := find_item(reference)
if not item:
item = _instance_item_from_db(reference)
item.amount += amount
정적 타입은 더 나은 코드 완성(code completion) 옵션도 제공합니다. 아래에서 PlayerController라는 클래스에 대한 동적 및 정적 타입 코드 완성 옵션의 차이점을 볼 수 있습니다.
점 뒤에 자동 완성 제안이 부족한 경우가 있을 수 있습니다.
이것은 동적 코드 때문입니다. Godot는 함수에 전달하는 노드 또는 값 타입을 알 수 없습니다. 그러나 타입을 명시적으로 작성하면 노드에서 모든 공용 메서드와 변수를 가져옵니다:
팁
정적 입력을 선호하는 경우 텍스트 편집기 > 완성 > 유형 힌트 추가 편집기 설정을 활성화하는 것이 좋습니다. 또한 기본적으로 비활성화되어 있는 `일부 경고 <경고 시스템>`_을 활성화하는 것도 고려해보세요.
또한 형식화된 GDScript는 컴파일 타임에 피연산자/인수 유형이 알려진 경우 최적화된 opcode를 사용하여 성능을 향상시킵니다. 앞으로 JIT/AOT 컴파일과 같은 더 많은 GDScript 최적화가 계획되어 있습니다.
전반적으로 타입형 프로그래밍은 보다 구조화된 경험을 제공합니다. 오류를 방지하고 스크립트의 자체 문서화 부분을 개선하는 데 도움이 됩니다. 이는 팀과 작업하거나 장기 프로젝트를 작업할 때 특히 유용합니다. 연구에 따르면 개발자는 대부분의 시간을 다른 사람의 코드나 과거에 작성하고 잊어버린 스크립트를 읽는 데 보낸다고 합니다. 코드가 더 명확하고 구조화될수록 더 빨리 이해할 수 있고 더 빨리 앞으로 나아갈 수 있습니다.
정적 타입 사용법
변수나 상수의 타입을 정의하려면 변수 이름 뒤에 콜론을 쓰고 그 뒤에 타입을 씁니다. 예) var health: int. 이렇게 하면 변수의 타입이 항상 동일하게 강제할 수 있습니다:
var damage: float = 10.5
const MOVE_SPEED: float = 50.0
func sum(a: float = 0.0, b: float = 0.0) -> float:
return a + b
Godot는 여러분이 콜론을 입력했지만 타입을 생략한 경우 타입을 추론하기 위해서 노력합니다:
var damage := 10.5
const MOVE_SPEED := 50.0
func sum(a := 0.0, b := 0.0) -> float:
return a + b
참고
내보내기에서 버퍼 섀도우(buffer shadow)와 레이 섀도우(ray shadow)간의 차이는 없습니다.
상수의 경우 Godot가 할당된 값에서 자동으로 설정하기 때문에 타입 힌트를 작성할 필요가 없습니다. 그러나 여전히 코드의 의도를 더 명확하게 하기 위해 그렇게 할 수 있습니다.
유형 힌트가 될 수 있는 것
이 가이드라인을 기반으로 한 전체 클래스 예제입니다:
Variant. 모든 유형. 대부분의 경우 이는 형식이 지정되지 않은 선언과 크게 다르지 않지만 가독성이 향상됩니다. 반환 유형으로서 함수가 명시적으로 일부 값을 반환하도록 합니다.(반환 유형만 해당)
void. 함수가 어떤 값도 반환하지 않음을 나타냅니다.핵심 클래스와 노드 (
Object,Node,Area2D,Camera2D, 등등)스크립트 클래스
스크립트 클래스
전역, 기본 및 사용자 지정 명명된 열거형입니다. 열거형 유형은 단지 ``int``일 뿐이며 값이 열거형 값 집합에 속한다는 보장은 없습니다.
미리 로드된 클래스나 열거형이 포함된 경우 상수(로컬 상수 포함)입니다.
여러분이 만든 클래스를 포함한 모든 클래스를 타입으로 사용할 수 있습니다. 스크립트에서 사용할 수 있는 두 가지 방법이 있습니다. 첫 번째는 사용하려는 스크립트를 상수 타입으로 미리 불러오는 방법입니다:
const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle
두 번째는 생성할 때 class_name 키워드를 사용하는 방법입니다. 위의 예제에서 Rifle.gd는 다음과 같습니다:
class_name Rifle
extends Node2D
class_name을 사용하면 Godot는 Rifle 타입을 편집기에 전역적으로 등록하기 때문에 상수에 미리 불러올 필요 없이 어디서나 사용할 수 있습니다:
var my_rifle: Rifle
-> 화살표로 함수의 반환 타입 정의하기
함수의 반환 타입을 정의하려면 함수 선언 뒤에 대시와 오른쪽 꺾쇠 괄호 ->를 쓰고 그 뒤에 반환 타입을 적으세요:
func _process(delta: float) -> void:
pass
void 타입은 함수가 아무것도 반환하지 않는 것을 의미합니다. 변수와 마찬가지로 어떤 타입도 반환 타입으로 사용할 수 있습니다:
func hit(damage: float) -> bool:
health_points -= damage
return health_points <= 0
또한 여러분의 노드를 반환 타입으로 사용할 수 있습니다:
# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
var item: Item = find_item(reference)
if not item:
item = ItemDatabase.get_instance(reference)
item.amount += amount
return item
공분산과 반공분산
기본 클래스 메서드를 상속할 때 `Liskov 대체 원칙 <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`__을 따라야 합니다.
공분산: 메소드를 상속할 때 상위 메소드보다 더 구체적인 반환 유형(하위 유형)을 지정할 수 있습니다.
반공변성: 메소드를 상속할 때 상위 메소드보다 덜 구체적인 매개변수 유형(수퍼타입)을 지정할 수 있습니다.
예시:
class_name Parent
func get_property(param: Label) -> Node:
# ...
class_name Child extends Parent
# `Control` is a supertype of `Label`.
# `Node2D` is a subtype of `Node`.
func get_property(param: Control) -> Node2D:
# ...
-> 화살표로 함수의 반환 타입 정의하기
``Array``의 유형을 정의하려면 ``[]``에 유형 이름을 넣으십시오.
배열 유형은 for 루프 변수뿐만 아니라 [], [...] =``(할당) 및 ``+``와 같은 일부 연산자에도 적용됩니다. 배열 메서드(예: ``push_back) 및 기타 연산자(예: ==)는 아직 유형이 지정되지 않았습니다. 내장 유형, 기본 및 사용자 정의 클래스, 열거형을 요소 유형으로 사용할 수 있습니다. 중첩된 배열 유형(예: Array[Array[int]])은 지원되지 않습니다.
var scores: Array[int] = [10, 20, 30]
var vehicles: Array[Node] = [$Car, $Plane]
var items: Array[Item] = [Item.new()]
var array_of_arrays: Array[Array] = [[], []]
# var arrays: Array[Array[int]] -- disallowed
for score in scores:
# score has type `int`
# The following would be errors:
scores += vehicles
var s: String = scores[0]
scores[0] = "lots"
Godot 4.2부터 for 루프에서 루프 변수의 유형을 지정할 수도 있습니다. 예를 들어 다음과 같이 작성할 수 있습니다.
var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
pass
배열은 유형이 지정되지 않은 상태로 유지되지만 for 루프 내의 name 변수는 항상 String 유형입니다.
-> 화살표로 함수의 반환 타입 정의하기
``Dictionary``의 키 및 값 유형을 정의하려면 유형 이름을 ``[]``로 묶고 키와 값 유형을 쉼표로 구분하세요.
사전의 값 유형은 for 루프 변수뿐만 아니라 [] 및 [...] =``(할당)와 같은 일부 연산자에도 적용됩니다. 값을 반환하는 사전 메서드와 기타 연산자(예: ``==)는 아직 형식화되어 있지 않습니다. 내장 유형, 기본 및 사용자 정의 클래스, 열거형을 요소 유형으로 사용할 수 있습니다. 중첩된 유형의 컬렉션(예: Dictionary[String, Dictionary[String, int]])은 지원되지 않습니다.
var fruit_costs: Dictionary[String, int] = { "apple": 5, "orange": 10 }
var vehicles: Dictionary[String, Node] = { "car": $Car, "plane": $Plane }
var item_tiles: Dictionary[Vector2i, Item] = { Vector2i(0, 0): Item.new(), Vector2i(0, 1): Item.new() }
var dictionary_of_dictionaries: Dictionary[String, Dictionary] = { { } }
# var dicts: Dictionary[String, Dictionary[String, int]] -- disallowed
for fruit in fruit_costs:
# `fruit` has type `String`
# The following would be errors:
fruit_costs["pear"] += vehicles
var s: String = fruit_costs["apple"]
fruit_costs["orange"] = "lots"
타입 캐스팅
타입 캐스팅(Type casting)은 타입 언어의 핵심 개념입니다. 캐스팅은 어떤 값의 타입을 다른 타입으로 변환하는 것입니다.
게임에 Area2D를 확장하는 적이 있다고 상상해보세요. 적이 PlayerController 스크립트가 붙은 플레이어 KinematicBody2D에 충돌하게 하고 싶습니다. on_body_entered 시그널을 사용하면 콜리전을 감지할 수 있습니다. 타입형 코드에서, 감지한 물체는 _on_body_entered 콜백의 PlayerController가 아니라 일반 PhysicsBody2D가 됩니다.
as 캐스팅 키워드로 PhysicsBody2D가 플레이어인지 확인할 수 있으며, 콜론 :을 다시 사용해서 변수가 이 타입을 사용하도록 강제할 수 있습니다. 이렇게 하면 변수가 PlayerController 타입에 고정됩니다:
func _on_body_entered(body: PhysicsBody2D) -> void:
var player := body as PlayerController
if not player:
return
player.damage()
커스텀 유형을 다룰 때 body가 PlayerController를 확장하지 않으면 player변수가 null로 설정됩니다. 이를 사용해 바디가 플레이어인지 확인할 수 있습니다. 우리는 이 캐스트 덕분에 플레이어 변수에 대한 완전한 자동완성도 얻습니다.
참고
as 키워드는 런타임에 유형이 일치하지 않는 경우 오류/경고 없이 변수를 null``로 자동 캐스팅합니다. 어떤 경우에는 이것이 편리할 수도 있지만 버그가 발생할 수도 있습니다. 이 동작을 의도한 경우에만 ``as 키워드를 사용하십시오. 더 안전한 대안은 is 키워드를 사용하는 것입니다.
if not (body is PlayerController):
push_error("Bug: body is not PlayerController.")
var player: PlayerController = body
if not player:
return
player.damage()
is not 연산자를 사용하여 코드를 단순화할 수도 있습니다.
if body is not PlayerController:
push_error("Bug: body is not PlayerController")
한 줄 case 명령문에서 콜론 이후.
assert(body is PlayerController, "Bug: body is not PlayerController.")
var player: PlayerController = body
if not player:
return
player.damage()
참고
내장 타입으로 캐스트를 시도하다가 실패한다면 Godot는 오류를 내보낼 것입니다.
안전한 라인(safe line)
안전한 라인임을 보장하기 위해 캐스팅을 사용할 수도 있습니다. 안전한 라인은 코드의 모호한 줄이 타입 안전한지 알려주는 Godot 3.1의 새로운 도구입니다. 때때로 여러분이 타입형 코드와 동적 코드를 혼합하고 일치시킬 수 있기 때문에 Godot에는 명령어가 실행 시간에 오류를 유발하는지 여부를 알 수 있는 충분한 정보가 없습니다.
이는 자식 노드를 가져올 때 발생합니다. 타이머를 예로 들어 보겠습니다. 동적 코드를 사용하면 $Timer로 노드를 가져올 수 있습니다. GDScript는 duck-typing을 지원하므로 타이머가 Timer 유형인 경우에도 Node와 Object의 두 개의 클래스를 확장합니다. 동적 GDScript를 사용하면 호출에 필요한 메서드가 있는 한 여러분은 노드 타입을 신경 쓰지 않아도 됩니다.
캐스팅을 사용하면 ($Timer as Timer), ($Player as CharacterBody2D) 등 노드를 가져올 때 예상되는 유형을 Godot에게 알릴 수 있습니다. Godot는 유형이 작동하는지 확인하고 만약 그렇다면, 스크립트 편집기 왼쪽 행 번호가 초록색으로 바뀝니다.
안전하지 않은 라인(line 7) vs 안전한 라인(line 6 and 8)
참고
안전한 라인이 항상 더 좋고 신뢰할 수 있는 코드를 의미하는 것은 아닙니다. as 키워드에 대한 위의 참고 사항을 참조하세요. 예를 들면:
@onready var node_1 := $Node1 as Type1 # Safe line.
@onready var node_2: Type2 = $Node2 # Unsafe line.
node_2 선언이 안전하지 않은 라인으로 표시되어 있더라도 node_1 선언보다 더 안정적입니다. 씬에서 노드 유형을 변경하고 실수로 스크립트에서 변경하는 것을 잊어버린 경우 씬이 로드될 때 오류가 즉시 감지됩니다. ``node_1``와 달리 자동으로 ``null``로 캐스팅되고 나중에 오류가 감지됩니다.
참고
편집기 설정에서 안전한 라인을 끄거나 색상을 바꿀 수 있습니다.
타입형 혹은 동적: 하나의 스타일을 고수하기
타입형 GDScript와 동적 GDScript는 같은 프로젝트에 공존할 수 있습니다. 하지만 코드베이스의 일관성과 동료들을 위해, 둘 중 하나의 스타일을 고수하는 것을 권장합니다. 같은 가이드라인을 따른다면 모두가 함께 일하기 더 쉬워지고, 다른 사람의 코드를 더 빠르게 읽고 이해하게 될 것입니다.
타입형 코드는 조금 더 작성해야 할 것이 많지만 위에서 말한 이득을 얻을 수 있습니다. 여기에 동일한 빈 스크립트 예제가 있습니다. 동적 스타일은 이렇습니다:
extends Node
func _ready():
pass
func _process(delta):
pass
그리고 정적 타입형 스타일은 이렇습니다:
extends Node
func _ready() -> void:
pass
func _process(delta: float) -> void:
pass
보시다시피, 엔진의 가상 메서드로 타입형 스타일을 사용할 수도 있습니다. 시그널 콜백도 다른 메서드처럼 타입형 스타일을 사용할 수 있습니다. 동적 스타일의 body_entered 시그널입니다:
func _on_area_2d_body_entered(body):
pass
그리고 타입 힌트를 쓴 같은 콜백입니다:
func _on_area_2d_body_entered(body: PhysicsBody2D) -> void:
pass
경고 시스템(Warning system)
참고
GDScript의 경고 시스템에 대한 관한 문서는 GDScript 내보낸 속성로 옮겨졌습니다.
타입형 GDScript의 또 다른 중요한 장점은 새로운 경고 시스템입니다. 버전 3.1부터 Godot는 코드를 작성할 때 코드에 대한 경고를 제공합니다. 엔진은 실행 시간에 문제를 일으킬 수 있는 코드 섹션을 식별하지만 여러분이 코드를 그대로 둘지 여부를 결정할 수 있습니다. 잠시 후에 자세히 알아봅시다.
우리는 특히 형식화된 GDScript 사용자를 겨냥한 여러 가지 경고를 가지고 있습니다. 기본적으로 이러한 경고는 비활성화되어 있으며 프로젝트 설정에서 활성화할 수 있습니다(디버그 > GDScript, **고급 설정**이 활성화되어 있는지 확인).
항상 정적 유형을 사용하려면 UNTYPED_DECLARATION 경고를 활성화할 수 있습니다. 또한 더 읽기 쉽고 안정적이지만 더 자세한 구문을 선호하는 경우 INFERRED_DECLARATION 경고를 활성화할 수 있습니다.
UNSAFE_* 경고는 안전하지 않은 라인보다 안전하지 않은 작업을 더 눈에 띄게 만듭니다. 현재 UNSAFE_* 경고는 안전하지 않은 라인에 포함되는 모든 경우를 포함하지는 않습니다.
일반적으로 안전하지 않은 작업과 이에 상응하는 안전한 작업
전역 범위
다음 전역 범위 메서드는 정적으로 형식화되지 않지만 사용 가능한 형식화된 대응 항목이 있습니다. 이러한 메서드는 정적으로 유형이 지정된 값을 반환합니다.
메서드 |
정적으로 유형이 지정된 해당 항목 |
|---|---|
Sprite |
|
정적 유형 지정을 사용할 때는 가능하면 유형이 지정된 전역 범위 메서드를 사용하세요. 이를 통해 안전한 회선을 확보하고 더 나은 성능을 위해 입력된 지침의 이점을 누릴 수 있습니다.
UNSAFE_PROPERTY_ACCESS 및 UNSAFE_METHOD_ACCESS 경고
이 예에서는 ``class_name MyScript``가 첨부된 스크립트 및 ``extends Node2D``가 있는 객체에 대해 속성을 설정하고 메서드를 호출하는 것을 목표로 합니다. 객체에 대한 참조가 ``Node2D``인 경우(예를 들어 물리 시스템에 의해 전달된 경우) 먼저 속성과 메서드가 존재하는지 확인한 다음 존재하는 경우 설정하고 호출할 수 있습니다.
if "some_property" in node_2d:
node_2d.some_property = 20 # Produces UNSAFE_PROPERTY_ACCESS warning.
if node_2d.has_method("some_function"):
node_2d.some_function() # Produces UNSAFE_METHOD_ACCESS warning.
그러나 이 코드는 참조된 유형(이 경우 Node2D)에 속성과 메서드가 없기 때문에 UNSAFE_PROPERTY_ACCESS 및 UNSAFE_METHOD_ACCESS 경고를 생성합니다. 이러한 작업을 안전하게 수행하려면 먼저 is 키워드를 사용하여 객체가 MyScript 유형인지 확인한 다음 해당 속성을 설정하고 메소드를 호출할 수 있는 MyScript 유형의 변수를 선언하면 됩니다.
if node_2d is MyScript:
var my_script: MyScript = node_2d
my_script.some_property = 20
my_script.some_function()
또는 변수를 선언하고 as 연산자를 사용하여 개체를 캐스팅해 볼 수 있습니다. 그런 다음 변수가 할당되었는지 확인하여 캐스트가 성공했는지 확인하고 싶을 것입니다.
var my_script := node_2d as MyScript
if my_script != null:
my_script.some_property = 20
my_script.some_function()
UNSAFE_CAST 경고
이 예에서는 충돌 영역에 들어오는 객체에 연결된 레이블을 사용하여 해당 영역의 이름을 표시하려고 합니다. 객체가 충돌 영역에 들어가면 물리 시스템은 Node2D 객체와 함께 시그널를 전송하며, 우리가 원하는 작업을 수행하는 가장 간단한(정적으로 유형이 지정되지 않은) 솔루션은 다음과 같이 달성될 수 있습니다.
func _on_body_entered(body: Node2D) -> void:
body.label.text = name # Produces UNSAFE_PROPERTY_ACCESS warning.
label``가 ``Node2D``에 정의되어 있지 않기 때문에 이 코드 조각은 ``UNSAFE_PROPERTY_ACCESS 경고를 생성합니다. 이 문제를 해결하려면 먼저 label 속성이 존재하는지 확인하고 텍스트 속성을 다음과 같이 설정하기 전에 Label 유형으로 캐스팅할 수 있습니다.
func _on_body_entered(body: Node2D) -> void:
if "label" in body:
(body.label as Label).text = name # Produces UNSAFE_CAST warning.
그러나 body.label``는 ``Variant 유형이므로 UNSAFE_CAST 경고가 생성됩니다. 원하는 유형의 속성을 안전하게 가져오려면 객체를 Variant 값으로 반환하거나 속성이 존재하지 않는 경우 null``를 반환하는 ``Object.get() 메서드를 사용할 수 있습니다. 그런 다음 is 키워드를 사용하여 속성에 올바른 유형의 개체가 포함되어 있는지 확인하고 마지막으로 개체를 사용하여 정적으로 유형이 지정된 변수를 선언할 수 있습니다.
func _on_body_entered(body: Node2D) -> void:
var label_variant: Variant = body.get("label")
if label_variant is Label:
var label: Label = label_variant
label.text = name
타입을 지정할 수 없는 경우
이 소개를 마무리하며, 타입형 힌트를 사용할 수 없는 몇몇 경우를 알아봅시다. 밑의 모든 예제는 오류를 유발합니다.
배열에서 각 멤버의 타입을 지정할 수 없습니다. 그렇게 할 경우 오류를 유발합니다:
var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
var character: Dictionary = {
name: String = "Richard",
money: int = 1000,
inventory: Inventory = $Inventory,
}
중첩된 유형은 현재 지원되지 않습니다:
var teams: Array[Array[Character]] = []
요약
타입형 GDScript는 강력한 툴입니다. Godot 3.1 버전부터 이용할 수 있으며, 일반적인 오류들을 방지하면서 더 구조화된 코드와 확장성 있는 체계를 만드는 것을 도와줍니다. 미래에는 정적 타입도 향후 컴파일러 최적화로 좋은 퍼포먼스 향상을 제공할 것입니다.