본 게시글은 'HeartBeast'의 Make an Action RPG 파트의 내용을 다룹니다.
본 강의는 연계 강의이기 때문에 꼭 이전 편들을 보면서 진행하셔야 내용을 이해하시는데 어려움이 없습니다.

 번째 파트인 주인공의 움직임을 자연스럽게 해보기입니다.

이전 시간에 스프라이트를 제어하고 주인공의 움직임을 추가하는 작업을 거쳤습니다.

이번에는 델타(Delta)를 활용하여 프레임이 떨어져도 일정한 이동 거리 갖는 작업과

주인공의 움직임을 부드럽게 조정하도록 할 것입니다. 이론적인 내용이 많으므로 어려우시더라도

개념을 이해하고 넘어가도록 합시다.

 

우선 델타부터 다루도록 하겠습니다.

델타 타임(Delta Time), 즉 델타는 이전 프레임의 시작 시간과 현재 프레임의 시작 시간의 차이를 의미합니다.
프레임은 기본적으로 엔진에 구현되어있는 update함수에 의해 매 프레임마다 호출되는데 

그 함수에서 대부분 델타를 제공합니다. 아니면 변수 형태로 델타를 활용할 수 있습니다. 

여기서 프레임(Frame)은 정지된 하나의 화면을 의미하고 FPS(Frame Per Second)
초 당 보여주는 화면의 장수를 의미합니다. 흔히 말하는 30 프레임, 60 프레임이 바로 이 FPS를 의미하는 것이죠.

 

근데 왜 갑자기 프레임 이야기를 하냐면, 각각 클라이언트에서 다른 프레임이 나온다고 생각해 봅시다.
사용자 A는 FPS 8 프레임을 사용자 B는 FPS 16 프레임이 나온다고 가정합시다.
어떤 캐릭터가 있는데 처음 위치는 둘 다 원점에 있고 update함수에 

x축 좌표로 +10씩 상승하는 코드를 작성했을 때 5초가 지났을 때 각각
이동 값이 어떻게 될까요?

 

사용자 클라이언트 게임 구현 5초 후 각 사용자의 캐릭터의 위치 x값
사용자 A (FPS = 8) 1 프레임 마다 x축 +10씩 이동 처리 [10*8프레임*5초] x = 400
사용자 B (FPS = 16) [10*16프레임*5초] x = 800

다음과 같이 되겠죠?

 

이상하지 않나요? 프레임에 따라 캐릭터가 사용자의 컴퓨터마다 각기 다른 위치에 있으니

정상적인 상황이라고는 말할 수 없습니다. 그러면 이것을 어떻게 해결해야 할까요?

이런 것을 방지하기 위해 있는 것이 델타입니다.
델타는 각 프레임에 대해 다음과 같이 계산될 수 있습니다.

 

  FPS Delta Time
사용자 A 1초당 8프레임 처리 1/8 = 0.125
사용자 B 1초당 16프레임 처리 1/16 = 0.0625

사용자 A는 1초당 8 프레임을 처리할 때 1 프레임을 처리하는 시간은 0.125

사용자 B는 1초당 16 프레임을 처리할 때 1 프레임을 처리하는 시간은 0.0625초가 걸리게 됩니다.

매 프레임마다 처리하는 어떤 값에 델타를 곱하게 되면 시스템의 성능이 다르더라도

일정한 값을 얻을 수 있다는 것이죠. 여기서는 일정한 프레임이 나오게 된다고 가정했지만,

사용자 B가 갑작스럽게 프레임이 8~10으로 떨어져 버려도 문제없습니다.

 

  1초 뒤 이동량 5초 뒤 이동량
사용자 A (FPS = 8) 10*8*0.125 = 10 10*8*5(초)*0.125 = 50
사용자 B (FPS = 16) 10*16*0.0625 = 10 10*16*5(초)*0.0625= 50

다음과 같이 델타를 곱하게 되면 두 사용자가 다른 프레임을 나타내더라도 같은 이동 값으로 계산됨을 알 수 있습니다.
또한 프레임에서 시간 단위로 바꾸기 때문에 불 규칙 한 프레임 단위보다 용이하게 통제할 수 있습니다.

 

델타가 어느 정도 감이 오셨나요? 이다음으로 한번 직접 고도 엔진에서 활용해 봅시다.

스크립트(Script) 편집기로 넘어갑니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extends KinematicBody2D
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    
    if input_vector != Vector2.ZERO:
        velocity = input_vector
    else:
        velocity = Vector2.ZERO
        
    move_and_collide(velocity * delta)

바로 'move_and_collide'부분에 Delta를 곱해주시면 됩니다.

 

한번 결과를 볼까요?

 

움직이기는 하는데 너무 느리게 움직입니다. 사실상 당연한 결과인데요,

input_vector에 들어오는 값 즉, get_action_strength은 입력값에 따라 0~1의 값을 반환하기 때문이죠.

여기에 델타를 곱해버리면 당연히 값이 엄청 작을 수밖에 없겠죠?

우리는 여기에 변수를 하나 더 추가해 속력(Speed)을 곱하도록 할 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extends KinematicBody2D
 
const MAX_SPEED = 100
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
 
    if input_vector != Vector2.ZERO:
        velocity = input_vector
    else:
        velocity = Vector2.ZERO
        
    move_and_collide(velocity * delta * MAX_SPEED)

다음과 같이 변수를 만들고, 값은 한 '100' 정도 주면 되겠네요. 그리고 move_and_collide에 속력을 곱해봅시다. 

결과를 봅시다.

 

잘 움직이긴 하는데 대각선으로 움직일 때 무언가 더 빠른 느낌입니다. 뭐 때문일까요?

여기서 또 개념적인 부분을 한번 더 짚고 가야 합니다.

 

무수한 선들은 벡터(Vector)를 나타냅니다.

우선 벡터(Vector)는 크기와 방향을 갖고 있습니다. 그렇죠?

아까 전에 get_action_strength0~1의 값을 받는다고 했습니다.

 

파랑선 테두리는 1의 값 경계입니다

따라서 제가 ↑↓←→키를 각각 따로 눌렀을 때 값은 이렇게 되겠죠?

여기까지는 문제가 없습니다. 그런데 대각선으로 움직일 때 문제가 발생했었습니다.

 

오른쪽 위 대각선(↗) 방향으로 움직인다 가정해봅시다.

대각선으로 움직인다는 것은 x축y축 값이 동시에 들어온다는 것을 의미하죠, 즉

움직일 때는 값이 두 개나 들어왔으므로 수직, 수평으로 움직일 때보다

대각선이 더 많은 이동량을 갖는다는 이야기죠.

 

하지만 우리는 어느 방향으로 움직이던 일정한 이동량을 갖게 하고 싶습니다.

여기서 벡터의 정규화(Normalize)를 사용하게 됩니다. 정규화된 벡터를 흔히,

법선벡터, 단위 벡터라고 말합니다. 용어가 어렵죠? 우선 특징부터 알고 갑시다.

이 벡터들은 다음과 같은 특징을 가지고 있습니다.

1. 크기가 1이고 방향을 가진 벡터

2. 모든 벡터는 정규화를 통해 단위 벡터가 될 수 있음

 

크기가 1인데 방향을 가진 벡터 가지고 무엇을 하느냐?

단편적인 예로 여러분의 캐릭터와 몬스터의 방향을 구할 때도 사용합니다.

몬스터가 여러분을 추적할 때 바로 이 벡터를 활용합니다.

자세한 건 나중에 다룰 기회가 있으니 우선은 여기까지만 짚고 넘어갑시다.

 

스크립트 편집기에서 우측 상단을 보시면 Search Help가 있는데

Search Help를 통해 고도 엔진에서는 어떤 식으로 설명이 되어있는지 확인할 수 있습니다.

 

설명을 확인해보니 벡터 v / 벡터 v의 크기로 나눠서 나온 단위 벡터 v를 벡터를 반환한다고 쓰여있네요.

 

이제 이것을 활용하여, 대각선으로 움직여도 일정한 이동량을 계산할 수 있습니다.

원래 게임에서는 이동 속도라는 게 방향에다가 크기를 얼마큼 넣어주냐에 따라 달라지는 거니까 말이죠.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extends KinematicBody2D
 
const MAX_SPEED = 100
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        velocity = input_vector
    else:
        velocity = Vector2.ZERO
        
    move_and_collide(velocity * delta * MAX_SPEED)

코드를 다음과 같이 수정해봅시다.

만약 여러분의 값을 보고 싶다면 다음과 같이 사이에 print를 써서 볼 수도 있습니다.

 

print(input_vector)

input_vector=input_vector.normalized()

print(input_vector)

 

이렇게 쓰게 되면 단위 벡터로 나오기 전 값과 나온 후의 값을 둘 다 확인해볼 수 있습니다.

코드 작성이 끝났다면 결과를 확인해봅시다.

 

이제 대각선으로 움직일 때 더 빨라지는 느낌이 없네요! 일정하게 움직입니다.

 

아까 언급한 코드로 print를 넣었을 때 다음과 같이 확인할 수 있습니다.

0.707107이라는 값은, 대각선으로 움직였을 때 정규화 처리된 값이라고 할 수 있네요.

다음으로 진행합시다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extends KinematicBody2D
 
const ACCELERATION = 400
const MAX_SPEED = 100
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        velocity += input_vector * ACCELERATION * delta
    else:
        velocity = Vector2.ZERO
        
    move_and_collide(velocity * delta)

상수를 하나 더 추가하고 'ACCELERATION'라고 정의한 뒤 값은 '400'정도 주도록 합시다.

 

velocity=input_vector 이 부분은

다음과 같이 바꿔 주세요.

velocity+=input_vector*ACCELERATION*delta

이렇게 하면 매 프레임마다 속도를 더해서 속도가 빨라집니다.

 

그다음  move_and_collide(velocity * delta)이렇게 MAX_SPEED는 지우도록 합니다.

결과를 함 볼까요?

 

마치 스케이트 타는 것처럼 움직이네요. 이대로 쓰기에는 부족하니 좀 더 코드를 바꿔줍시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extends KinematicBody2D
 
const ACCELERATION = 400
const MAX_SPEED = 100
const FRICTION = 400
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        velocity += input_vector * ACCELERATION * delta
    else:
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    move_and_collide(velocity * delta)

상수를 또 추가해줍니다. 이름은 'FRICTION'로 정의합니다. 값은 똑같이 '400'을 줘 봅시다.

FRICTION를 적용하려면, 기존에 velocity=Vector2.ZERO 부분은 더 이상 필요하지 않게 됩니다.

따라서 move_toward를 사용합니다. 

 

move_toward는 벡터를 델타 값만큼 특정 벡터로 이동시키는 함수입니다.

코드에서는 다음과 같이 사용하였는데

velocity=velocity.move_toward(Vector2.ZERO, FRICTION * delta)

Vector2.ZERO로 인자를 줬기 때문에 FRICTION * delta 값에 의해서 천천히 멈추게 됩니다.

다시 게임으로 넘어가서 확인해 봅시다.

 

아까보다는 낫지만 아직 좀 더 손을 봐야 할 것 같습니다.

이번에는 기존에 있던 상수 MAX_SPEED를 사용하도록 합시다.

이것을 사용하기 위해서는 다른 함수를 사용해야 하는데요. clamped는 말 그대로

값이 설정된 값보다 초과하지 않도록 해줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extends KinematicBody2D
 
const ACCELERATION = 400
const MAX_SPEED = 100
const FRICTION = 400
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.= Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.= Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        velocity += input_vector * ACCELERATION * delta
        velocity = velocity.clamped(MAX_SPEED)
    else:
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    move_and_collide(velocity * delta)

다음과 같이 코드를 작성합시다.

 

결과를 보니 부드럽게 잘 움직입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extends KinematicBody2D
 
const ACCELERATION = 400
const MAX_SPEED = 100
const FRICTION = 400
 
var velocity = Vector2.ZERO
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    move_and_collide(velocity * delta)

다른 방법으로 아래에 썼던 move_toward를 쓰면. clamped를 할 필요가 없습니다.

이렇게 되면, 더 깔끔하게 쓸 수 있습니다.

 

ACCELERATIONMAX_SPEEDFRICTION이렇게 세 개의 상수들은 여러분들이 자유롭게 조정을 해도 됩니다.

전체 속력을 낮추고 싶으면 MAX_SPEED의 값을 내리면 되고요, 그 반대는 빨라집니다.

초기 속도를 제어하고 싶으면 ACCELERATION 멈추기 전 속도를 제어하고 싶으면 FRICTION의 값을 수정하면 됩니다.

 

주인공 캐릭터를 부드럽게 하는 이동은 이것으로 끝났습니다.

다음 파트는 충돌(collision)에 대해 이야기하고

실제로 적용해보는 시간을 갖도록 하겠습니다. 

 

+) 03/29 게시글 내용 일부 수정

 

참고한 글:

blog.naver.com/gaf0zero/140164218998

blog.naver.com/pxkey/221294809584

728x90
728x90