오늘은 플레이어 코딩 부분을 진행하려 한다.
튜토리얼 링크 : https://docs.godotengine.org/en/stable/getting_started/first_2d_game/03.coding_the_player.html
추가될 기능은 다음과 같다.
- 플레이어 움직임
- 플레이어 애니메이션
- 콜리전 충돌 설정 (다음 글에서 진행)
1. 스크립트 생성
우리가 만들어준 플레이어 오브젝트를 누르고 새 스크립트를 만드는 노드를 생성한다.
(아마 스크립트 그 자체==노드인 듯 싶다.)
이런 식으로 생성.
이 튜토리얼에선 GDScript라는 언어로 시작할거라 GDScript를 전혀 모른다면 아래 링크로 예습하는 게 좋다.
근데 난 맨땅헤딩파라서 패스.
2. GDScript - 변수 export
우리가 필요한 변수들을 Godot 에디터 안에서 편리하게 수정할 수 있도록 몇 가지 설정을 진행하자.
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
GDScript에서 변수는 앞에 var를 붙여서 선언해준다.
근데 이때, 그 앞에 @export 말머리를 붙여주면 Godot 에디터 안에서도 이 값이 보이게 된다.
Godot 에디터에서 수정한 값은 스크립트 내부 var 선언 부분을 수정하지 않더라도 알아서 적용된다.
근데 만약, 당신이 C# 유저라서 해당 언어를 사용하고 싶다면, 새로고침이 바로바로 되지 않는다.
아래 사진처럼 build 버튼을 눌러줘야 수정이 되므로 주의하자.
3. GDScript - 기본 function
당신의 스크립트엔 이미 _ready() 와 _process() function이 선언되어 있을 것이다.
만약 이 키워드가 없다면 아래 내용을 따라오며 직접 만들어줘야 한다.
_ready()
이 함수는 노드가 씬 트리에 들어갈 때(즉, 노드 첫 로딩 시, 다시말해 게임 시작 직후)에 실행된다.
그래서 게임의 스크린 사이즈를 읽어오기에 딱 적당한 때이기도 하다.
func _ready():
screen_size = get_viewport_rect().size
_process()
이 함수는 게임의 매 프레임마다 실행된다. 그래서 우리는 게임의 요소들이 플레이어와 상호작용하거나, 우리가 바라는 효과를 넣어주고 싶을 때 이 함수 안에서 진행되도록 해주면 된다.
더 예시를 들자면:
- 키보드/컨트롤러 등을 플레이어가 눌렀는지 확인할 때
- 플레이어를 특정 방향으로 움직일 때(화면의 요소가 변화할 때)
- 애니메이션을 실행할 때
- 등등
먼저, 우리는 캐릭터를 움직여야하므로, 플레이어가 키를 눌렀는지 확인부터 해봐야한다. 이 게임에선 4가지 방향의 움직임만 체크할 예정이다.
3-2. Input map
플레이어의 키 인풋은 프로젝트 세팅 > Input Map에서 설정해줄 수 있다.
여기서 커스텀 이벤트를 만들고, 새로운 키들을 적용하고, 마우스 이벤트 등등도 설정해줄 수 있다.
위 예제같은 경우 move_right, move_left, move_up, move_down 4개의 키를 먼저 지정해준 뒤, 오른쪽 플러스 버튼을 눌러 키를 할당해줬다.
내 경우엔 WASD 이동이랑 대쉬 키도 넣어줬다.
이렇게 할당을 마치면 이제 스크립트 안에서 어떤 키가 인풋되었는지를 바로바로 탐지할 수 있게 된다.
3-3. 코드 전문
extends Area2D
@export var speed = 400 # How fast the player will move (pixels/sec).
@export var dash_speed = 2.0 # 플레이어가 대시하면 몇 배속으로 빨라지게 할건지
var screen_size # Size of the game window.
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
screen_size = get_viewport_rect().size
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
var velocity = Vector2.ZERO # Player's movement vector
var dash = 1 # 나중에 기존 속력에 곱해줄 대시 값
# 플레이어 인풋 확인
if Input.is_action_pressed("move_right"):
velocity.x += 1
if Input.is_action_pressed("move_left"):
velocity.x -= 1
if Input.is_action_pressed("move_down"):
velocity.y += 1 # y축 +가 아래 방향.
if Input.is_action_pressed("move_up"):
velocity.y -= 1
if Input.is_action_pressed("move_dash"):
dash = dash_speed # 대시 키를 누르면 플레이어가 {dash_speed}배속이 된다.
# 이동 속력 계산, 애니메이션 작동 여부 계산
if velocity.length() > 0:
velocity = velocity.normalized() * speed * dash # normalized()를 통해 velocity의 길이를 1로 맞춤.
$AnimatedSprite2D.play() # 달러를 입력만 해줘도 Player 밑에 있는 노드가 자동완성으로 뜬다 :O
else:
$AnimatedSprite2D.stop()
# 상황에 따라 애니메이션 이름 지정
if velocity.x != 0:
$AnimatedSprite2D.animation = "walk"
$AnimatedSprite2D.flip_v = false
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = "up"
$AnimatedSprite2D.flip_v = velocity.y > 0
# 애니메이션 속도에도 대시 값이 적용되도록 함
$AnimatedSprite2D.speed_scale = dash
# 플레이어의 위치값을 직접 조절.
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size) #캐릭터가 화면 밖으로 못나가도록 position에 최대 최소값 지정
위에서부터 천천히 보자.
- extends Area2D는 원래 이 Player라는 노드가 이름 수정 전에 Area2D가 베이스였기 때문에, 기존 노드 기능을 상속받는 용도인듯.
- @export 키워드로 2개 변수를 선언해줬다. 각각 플레이어 기본 스피드(1초 동안의 이동거리), 플레이어가 대쉬 했을 때의 스피드(기본 스피드의 n배속)다. 이 두 값은 글로벌로 선언되어 어떤 함수에서든 참조할 수 있게한다.
- _ready()에서 게임 최초 실행 시 스크린 사이즈를 갖고 온다.
- _process()에선 모든 작업 시작 전 초기값을 지정한 뒤, 플레이어가 어떤 키를 누르고있는지 확인하고, 그에 맞는 동작을 수행한다. 자세한 건 주석 참고.
- "$"(달러 표시)는 get_node()의 약자다. 즉 $AnimatedSprite2D.play() 는 get_code("AnimatedSprite2D").play() 로도 풀어쓸 수 있다. get_node()는 해당 노드 밑에 달린 노드중에 그 이름을 가진 노드가 있다면 그 노드를 리턴하고, 그게 없으면 null을 리턴한다.
- _process() 안에서 delta 는 이전 프레임을 완료하는데 걸린 시간, 간단히 말해 프레임의 길이를 받아온다. 그래서 인게임 프레임이 일정하지 않더라도 캐릭터 속력이 유지될 수 있도록 이 값을 사용하면 좋다.
4. 결과
오른쪽 위 run 버튼으로 프로젝트를 실행해보자.
꽤나 잘 돌아가는 걸 확인할 수 있다.
전반적으로 유니티 문법을 가볍게 쓰는 느낌이다. 스크립트-인스펙터-기나긴 로딩-테스트 확인-스크립트 재확인 과정을 오가야하는 유니티에 비해 노드 구조를 쓰니까 훨씬 직관적이다. 파이썬 문법이 베이스다보니 코드가 더 깔끔해보이는 것도 덤이다. 이게 또 유니티랑 완전히 다르냐...고 묻는다면 그건 또 아니긴하지만. 이것마저도 프로젝트가 커질수록 점점 복잡하고 느려질게 분명하긴 하겠지만. (오히려 프로젝트가 커질수록 더 심각하게 느려지는 건 이쪽일수도...)
번외로 엔진 내 스크립트 에디터 기능이 생각보다 너무 좋아서 놀랐다. 자동완성이 안 되는 게 없는 수준.
다음 시간에는 이제 적 캐릭터와의 충돌처리를 진행해볼 예정이다.