11
10

오늘은 플레이어 코딩 부분을 진행하려 한다.

튜토리얼 링크 : https://docs.godotengine.org/en/stable/getting_started/first_2d_game/03.coding_the_player.html

 

추가될 기능은 다음과 같다.

  • 플레이어 움직임
  • 플레이어 애니메이션
  • 콜리전 충돌 설정 (다음 글에서 진행)

 

1. 스크립트 생성

 

우리가 만들어준 플레이어 오브젝트를 누르고 새 스크립트를 만드는 노드를 생성한다.

(아마 스크립트 그 자체==노드인 듯 싶다.)

 

 

이런 식으로 생성.

 

이 튜토리얼에선 GDScript라는 언어로 시작할거라 GDScript를 전혀 모른다면 아래 링크로 예습하는 게 좋다.

https://docs.godotengine.org/en/stable/getting_started/step_by_step/scripting_languages.html#doc-scripting

 

근데 난 맨땅헤딩파라서 패스.

 


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

이 함수는 게임의 매 프레임마다 실행된다. 그래서 우리는 게임의 요소들이 플레이어와 상호작용하거나, 우리가 바라는 효과를 넣어주고 싶을 때 이 함수 안에서 진행되도록 해주면 된다.

더 예시를 들자면:

  1. 키보드/컨트롤러 등을 플레이어가 눌렀는지 확인할 때
  2. 플레이어를 특정 방향으로 움직일 때(화면의 요소가 변화할 때)
  3. 애니메이션을 실행할 때
  4. 등등

먼저, 우리는 캐릭터를 움직여야하므로, 플레이어가 키를 눌렀는지 확인부터 해봐야한다. 이 게임에선 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에 최대 최소값 지정

 

위에서부터 천천히 보자.

 

  1. extends Area2D는 원래 이 Player라는 노드가 이름 수정 전에 Area2D가 베이스였기 때문에, 기존 노드 기능을 상속받는 용도인듯.
  2. @export 키워드로 2개 변수를 선언해줬다. 각각 플레이어 기본 스피드(1초 동안의 이동거리), 플레이어가 대쉬 했을 때의 스피드(기본 스피드의 n배속)다. 이 두 값은 글로벌로 선언되어 어떤 함수에서든 참조할 수 있게한다.
  3. _ready()에서 게임 최초 실행 시 스크린 사이즈를 갖고 온다.
  4. _process()에선 모든 작업 시작 전 초기값을 지정한 뒤, 플레이어가 어떤 키를 누르고있는지 확인하고, 그에 맞는 동작을 수행한다. 자세한 건 주석 참고.
  5. "$"(달러 표시)는 get_node()의 약자다. 즉 $AnimatedSprite2D.play() 는 get_code("AnimatedSprite2D").play() 로도 풀어쓸 수 있다. get_node()는 해당 노드 밑에 달린 노드중에 그 이름을 가진 노드가 있다면 그 노드를 리턴하고, 그게 없으면 null을 리턴한다.
  6. _process() 안에서 delta 는 이전 프레임을 완료하는데 걸린 시간, 간단히 말해 프레임의 길이를 받아온다. 그래서 인게임 프레임이 일정하지 않더라도 캐릭터 속력이 유지될 수 있도록 이 값을 사용하면 좋다.

 


 

4. 결과

오른쪽 위 run 버튼으로 프로젝트를 실행해보자.

꽤나 잘 돌아가는 걸 확인할 수 있다.

 


 

전반적으로 유니티 문법을 가볍게 쓰는 느낌이다. 스크립트-인스펙터-기나긴 로딩-테스트 확인-스크립트 재확인 과정을 오가야하는 유니티에 비해 노드 구조를 쓰니까 훨씬 직관적이다. 파이썬 문법이 베이스다보니 코드가 더 깔끔해보이는 것도 덤이다. 이게 또 유니티랑 완전히 다르냐...고 묻는다면 그건 또 아니긴하지만. 이것마저도 프로젝트가 커질수록 점점 복잡하고 느려질게 분명하긴 하겠지만. (오히려 프로젝트가 커질수록 더 심각하게 느려지는 건 이쪽일수도...)

 

번외로 엔진 내 스크립트 에디터 기능이 생각보다 너무 좋아서 놀랐다. 자동완성이 안 되는 게 없는 수준.

 

 

다음 시간에는 이제 적 캐릭터와의 충돌처리를 진행해볼 예정이다.

COMMENT