Path2D노드를 사용하면 단순히 움직이는 캐릭터들(Enemy 나 NPC 등)의 단순 이동동작을 최소한의 코딩으로 구현할 수 있는 유용한 노드입니다.
1.적당한 Enemy 노드가 있을 경우, 전체 동작하는 Scene에서 아래와 같이 구성을 하였습니다. Path2D노드는 하위에 항상 PathFollow2D를 자식으로 담고 있어야 합니다. Path2D노드가 포인트를 이용하여 라인을 생성하는 노드라면, PathFollow2D노드는 그 선을 어떤 방식으로 따라가는지를 설정하는 노드입니다.
2. Path2D노드를 다시 선택하면 아래 그림과 같이 에디터 상단에 포인터 추가/삭제/편집할 수 있는 버튼이 활성화됩니다. 적당히 이동 경로를 그려줍니다. 이 중 5번째 아이콘은 경로를 닫아주는 버튼입니다. 라인을 완전히 그리지 말고, 적당히 그린 후 이 버튼을 누르면 라인을 닫으면서 닫힌 도형으로 만들어줍니다.
3. PathFollow2D노드를 선택한 후 Rotate를 체크 해제합니다. (경우에 따라 다릅니다만, 저는 라인을 따라 회전하지 않고, 좌우 평행이동만 하려고 합니다.
4. Enemy.gd스크립트 내부에 아래의 라인을 추가해줍니다. (_process(delta)가 없다면, _physics_process(delta)함수에 추가해주시기 바랍니다.)
# 변수로 선언하여 PathFollow2D를 불러옴
onready var path_follow = get_parent()
var speed = 20 # 움직이는 속도
func _process(delta):
path_follow.set_offset(path_follow.get_offset() + speed *delta)
처음 캐릭터의 애니메이션을 만들면 AnimationPlayer노드를 많이 사용하는데요, 이 노드만 이용해서 각종 상태를 코드로 컨트롤하려면 좀 힘듭니다. 많이 힘듭니다. 머리는 쥐가나고, 코드는 스파게티가 되고, 뭐 하나라도 추가하려면 완전...
그래서 여기서도 똑똑하신분이 상태 관리하라고 노드를 하나 주셨는데, 그게 바로 AnimationTree노드입니다. AnimationTree노드를 사용하려면 각각의 동작 Animation은 사전에 구현이 되어있어야합니다. 이미 존재하는 Animation을 컨트롤하는 상태관리자라고 보시면 됩니다. 참고로 저는 코로나 격리기간 중에 아래와 같은 게임을 7살 딸과 함께 만드는 중이었습니다. 탑다운 방식이다보니 상하좌우방면의 idle/walk(run)/attack의 애니메이션이 모두 필요한 상태입니다.
1. AnimationTree노드 추가
우선은 해당 노드를 한번 추가해줍니다. 그리고 인스펙터에서 Tree Root에서 "새 AnimationNodeStateMachine"을 선택합니다. 다른 강좌 영상을 봐도 거의 이거 하나만 씁니다. 아직 아래의 작업공간에는 아무것도 나타나지 않습니다. (제 화면의 idle박스와 walk박스는 미리 작업해 놓은 것이므로 무시하세요.)
내부의 분홍색 라인은 카메라 때문에 나타납니다. 무시하세요.
2. Add BlendSpace2D
화면 하단의 AnimationTree작업공간에서 빈 공간에 마우스 우클릭 후 Add BlendSpace2D를 선택/추가해줍니다. 그리고 이름을 idle이라고 바꾸도록 하겠습니다. 동일한 방법으로 하나 더 추가하여 walk도 만듭니다.
3. Animation 노드 연결
메뉴에서 화살표를 선택하고 idle박스에서 walk박스로 드래그해주면 그 방향으로 화살표 선이 나타나 연결됩니다. 동일한 방법으로 반대쪽도 수행해줍니다.
다음으로 idle박스를 선택한 후 빨간색으로 표시된 버튼을 눌러서, idle이 게임 시작의 최초 "시작"상태임을 인식시켜줍니다.
(ps) 각 화살표를 클릭하면 인스펙터에서 설정 가능한 부분들이 나타납니다. 여기서 다룰 부분이 아니므로, 이번에는 패스하도록 하겠습니다.
4. BlendSpace편집
idle박스의 편집버튼을 누르면 BlendSpace편집창이 나타납니다.
이미지상에 하단부분 클릭도 빼놓지 않고 추가해줍니다.
X축 및 Y축이 -1 ~ +1까지 있는 좌표계가 나타납니다. 이는 마우스, 키보드 등의 입력값을 말합니다. 즉 캐릭터의 현재 위치를 (0, 0)으로 보고, 이를 중심으로 마우스가 어디에서 눌렸는지를 받아들여 이에 대한 반응을 보여줍니다. (키보드일 경우는 중간이 없고 상/하/좌/우만 있겠죠?). 주의할 점은 Y값의 1이 화면에서는 아래쪽을 의미합니다. 즉, 위 그림에서 보이는 것처럼 (0, 1) 지점에 idle_down 애니메이션을 지정해아 합니다.
메뉴 아이콘의 3번째 버튼이 포인트 추가버튼입니다. 선택 후 위의 그림과 같이 마름모꼴의 각 지점을 클릭하면 에니메이션 추가를 할 수 있습니다. 각각의 좌표계에 애니메이션을 아래와 같이 추가합니다. 또한 혼합 버튼을 ...으로 변경합니다.
(-1, 0) : idle_left
(+1, 0): idle_right
(0, -1) : idle_top
(0, +1): idle_down
여기까지 진행하면 BlendSpace2D지정은 완료가 됩니다.
5. 테스트
이번엔 잘 지정이 되었는지 테스트를 해보겠습니다. 메뉴의 위쪽에 경로:root를 선택합니다.
그럼 애니메이션 박스를 만들었던 화면으로 전환되는데, 여기서 idle버튼을 플레이합니다.(박스 안쪽에 작은 실행버튼을 클릭합니다.) 그리고 다시 편집 버튼을 눌러 아까의 BlendSpace편집 화면으로 들어갑니다.
그리고 마커 버튼을 선택한 다음 마름모 영역 안에서 마우스를 드래그 하면 캐릭터가 해당 방향으로 애니메이션을 전환하는 모습을 볼 수 있습니다.
동일한 방법으로 walk 애니메이션도 추가해 줍니다. (attack애니메이션도 있으면 추가합니다.)
6. 스크립트 코딩
그럼 이렇게 만든 애니메이션을 어떻게 사용하는지 보겠습니다. 우리가 사용할 idle애니메이션은 "AnimationTree"노드의 "parameters/playback"이라는 속성에 존재합니다.
이 속성을 활용하기 위해 anim_mode = anim_tree.get("parameters/playback")을 사용합니다. 그리고 실제 애니메이션을 구현하는 것은 anim_mode.travel 함수입니다. anim_mode.travel("idle")과 같이 사용하면 idle애니메이션을 실행합니다.
extends KinematicBody2D
const SPEED = 120
var direction = Vector2(0,0)
var motion = Vector2(0,0)
onready var anim_tree = $AnimationTree
onready var anim_mode = anim_tree.get("parameters/playback")
func _physics_process(delta):
controls()
movement()
anim_control()
func controls():
# 키보드 입력을 받아서 방향을 정하는 함수
var LEFT = Input.is_action_pressed("ui_left")
var RIGHT = Input.is_action_pressed("ui_right")
var UP = Input.is_action_pressed("ui_up")
var DOWN = Input.is_action_pressed("ui_down")
direction.x = -int(LEFT) + int(RIGHT)
direction.y = -int(UP) + int(DOWN)
func movement():
# 방향을 크기 1인 벡터 motion으로 만들고, 이를 이용해 동작을 구현
motion = direction.normalized()
move_and_slide(motion * SPEED, Vector2(0,0))
func anim_control():
# 방향에 따라 애니메이션 지정. 애니메이션의 방향 값만 설정해 놓음
match direction:
Vector2(-1,0):
anim_tree.set('parameters/walk/blend_position', Vector2(-1,0))
anim_tree.set('parameters/idle/blend_position', Vector2(-1,0))
Vector2(1,0):
anim_tree.set('parameters/walk/blend_position', Vector2(1,0))
anim_tree.set('parameters/idle/blend_position', Vector2(1,0))
Vector2(0,-1):
anim_tree.set('parameters/walk/blend_position', Vector2(0,-1))
anim_tree.set('parameters/idle/blend_position', Vector2(0,-1))
Vector2(0, 1):
anim_tree.set('parameters/walk/blend_position', Vector2(0, 1))
anim_tree.set('parameters/idle/blend_position', Vector2(0, 1))
# 방향이 설정된 상태에서 애니메이션을 선택
if motion.length()!=0: # motion의 크기가 0이 아니면 이동중이므로 walk 애니
anim_mode.travel("walk")
else: # motion의 크기가 0이면 멈춤이므로 idle 애니
anim_mode.travel("idle")
이상으로 AnimationTree노드의 BlendSpace2D속성을 이용한 애니메이션 구현을 알아보았습니다.
HUD는 Head Up Display의 약자인데 게임의 요소와는 상관없는 UI 작성을 위한 부분입니다. Scene을 하나 추가한 뒤 CanvasLayer노드를 추가합니다. HUD에서 표시할 내용으로는 Score, "Game Over" 또는 "Get Ready!" 같은 문구, "Start" 버튼입니다. 즉 레이블과 버튼 요소가 필요합니다. 아래의 노드들을 CanvasLayer노드 하위에 자식노드로 추가해줍니다.
ScoreLabel (Label)
Message (Label)
StartButton (Button)
MessageTimer (Timer)
2. 폰트
폰트를 추가합니다. 경로는 인스펙터 창에서 Control > Theme Overrides > Fonts > 새 DynamicFont를 선택하고, 한번 더 클릭하여 Font > Font Data부분에 dodge_assets > font에 있는 "Xolonium-Regular.ttf"폰트를 넣어줍니다.(드래그 앤 드랍)
Font > Settings > Size는 64로 설정합니다.
font >Dyna....의 아래쪽 화살표를 누르면 여러 메뉴가 나오고, "복사" 메뉴가 나옵니다. 이걸 누르면 복사가 가능한데, 만들어두었던 Message 라벨로 가서 동일한 위치에서 "붙여넣기"를 해주면 지금 했던 설정을 복사해 넣게됩니다.
3. 레이아웃
우선 ScoreLabel을 설정해보겠습니다. 상단 오른쪽에 나타나는 레이아웃을 선택하고 '위쪽 넓게'를 클릭합니다. 그리고 인스펙터 창에서는 Text: 0, Align: Center, Valign: Center로 설정해줍니다.
MessageTimer노드에서 Wait Time을 2초로 설정해주고, One Shot속성을 "On"으로 합니다.
4. HUD.gd 스크립팅
start_game 시그널은 버튼이 눌렸을 때 Main노드에게 알려주는 시그널로 쓰일 예정입니다.
extends CanvasLayer
signal start_game
전달되는 메시지를 표시하기위한 함수를 작성합니다.
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
Player가 충돌하여 죽었을 때 텍스트를 보여주는 함수를 아래와 같이 작성합니다.
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
yield($MessageTimer, "timeout")
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
yield(get_tree().create_timer(1), "timeout")
$StartButton.show()
그리고 시간이 변할때마다 Main 씬에의해 아래의 코드가 지속적으로 호출되어 점수(시간)을 증가시킵니다.
game_over() 함수가 실행될 때 HUD에 표시되는 메시지를 실행하도록 아래 라인을 추가해줍니다.
$HUD.show_game_over()
_on_ScoreTimer_timeout() 함수에 아래 코드를 추가하여 점수 변경시 업데이트되도록 합니다.
$HUD.update_score(score)
6. 오래된 크리프 제거
아직은 Player가 죽고나서 다시 시작할 때 Mob들이 살아있습니다. 그래서 시작버튼을 누르면 전부 사라지도록 수정할 필요가 있습니다.
Mob 씬에서 루트노드를 선택하고 노드탭 > Groups탭 으로 갑니다. 그리고 이름을 "mobs"라고하여 Add버튼을 눌러 Mob들의 그룹을 지정합니다. 그리고는 game_over()함수에 아래의 코드를 추가합니다. 해당 그룹에 있는 객체들을 한번에 제거하는 명령입니다.
get_tree().call_group("mobs", "queue_free")
7. 마무리
Background
Main노드 최상단 자식노드로 ColorRect 노드를 추가한다. "Layout" -> "Full Rect" 으로 전체 화면 커버가 가능하다.TextureRect노드를 사용하면 이미지를 불러올 수도 있다.
Sound effects
Main신의 하위로 AudioStreamPlayer노드를 추가하고 이름을 Music이라고 변경합니다. 하나더 추가하여 DeathSound라고 이름을 변경합니다. Stream속성의 Load를 클릭하여 해당되는 오디오 파일을 지정해줍니다.
new_game()함수 아래 아래 코드를 추가합니다.
$Music.play()
game_over() 함수 아래 아래코드를 추가합니다.
$Music.stop()
$DeathSound.play()
Keyboard shortcut
HUD씬에서 StartButton을 클릭하고 Shortcut속성을 찾아서 "New Shortcut" > "Shortcut"을 클릭하면 두번째 속성이 나타납니다. "New InputEventAction">"InputEventAction"을 입력하면 Action속성이 나타나는데, ui_select라고 타이핑 입력합니다. 이게 스페이스바와 연동되는 지시어라고 합니다.
모든 요소들을 모을 Main Scene 을 생성합니다. 새 씬 생성후 "Node"라는 이름의 노드를 추가해줍니다.
그리고 "인스턴스화"아이콘을 클릭하여 Player.tscn을 불러옵니다.
그 외에는 자식노드 추가 기능에서 Timer 3개, Position2D 노드를 추가해주고, 각각의 이름을 아래와 같이 수정해줍니다.
MobTimer - 가장자리에서 Mob이 생성되는 시간 통제 (0.5초) ScoreTimer - 매초마다 점수 증가시킴 (1.0초) StartTimer - 시작하기 전에 지연 시간 부여 (2.0초) StartPosition - Player의 시작 위치(240, 450)
2. Mob 생성
Main 노드가 Mob을 가장자리를 따라 랜덤한 위치에서 생성합니다. 이를 위해서는 Main 노드 하위에 Path2D 노드를 추가해줍니다. Path2D노드를 선택하면 아래와 같은 화면으로 변경되는데, 그 전에 "스마트 스냅 사용", "격자 스냅 사용" 을 클릭합니다. 그리고, 새로 생긴 Path관련 버튼 중 가운데 버튼(1: 점추가)을 누른다음 화면의 네 꼭지점을 시계방향(2~5)으로 클릭해줍니다. 마지막으로 마지막 버튼(6: 곡선닫기)을 클릭해주면 경로가 완성됩니다.
다음으로 PathFollow2D 노드를 Path2D노드 하위에 추가해줍니다. 이 노드는 Path를 따라서 어떤 작업을 해줍니다. 이 게임에서는 Mob 생성이겠죠?
그리고 각각의 이름을 MobPath, MobSpawnLocation으로 바꿔줍니다.
3. Main.gd 스크립팅
우선 아래의 코드로 시작하겠습니다.
extends Node
export (PackedScene) var Mob
var score
func _ready():
randomize()
export (PackedScene) var Mob 구문으로 선언하면, 인스펙터 창에서 Mob이라는 속성이 나타나는 것을 확인할 수 있습니다. 여기에는 Scene을 넣어줄 수 있는데, 우리가 구성한 Mob.tscn을 넣어줍니다.(드래그 앤 드랍)
다음으로 Player가 Mob과 부딛혔을 때(hit signal) 게임오버를 나타내는 함수를 작성/연결하겠습니다. 조금 헷갈릴 수도 있는데요, Main Scene 하위에 있는 Player Scene을 선택(한번 클릭)하면, 우측의 "노드" 창에 시그널들이 나타납니다. 우리가 만든 hit 시그널도 보입니다. hit()시그널을 더블클릭하면 메서드 연결창이 나타납니다. 작성할 스크립트가 Main인 것을 확인하고, 받는 메서드의 이름을 game_over로 하여 "연결"을 클릭합니다.
func _on_MobTimer_timeout():
# Choose a random location on Path2D.
$MobPath/MobSpawnLocation.offset = randi()
# Create a Mob instance and add it to the scene.
var mob = Mob.instance()
add_child(mob)
# Set the mob's direction perpendicular to the path direction.
var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = $MobPath/MobSpawnLocation.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Set the velocity (speed & direction).
mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
mob.linear_velocity = mob.linear_velocity.rotated(direction)
4. 테스트
func _ready():
randomize()
new_game()
플레이어 이동, 몹 생성, 충돌시 사라지기가 정상 작동되면 다음 HUD 구성을 위해 new_game()부분은 제거해줍니다.
프로젝트 > 프로젝트 설정을 클릭한 후, display > window메뉴로 가서 width480, height720으로 설정해줍니다.
그리고, Stretch로 내려가서 Mode: 2d, Aspect: keep으로 변경해줍니다.
2. Resource 폴더 추가
dodge_assets 파일은 링크 에서 다운받을 수 있습니다. Godot엔진 홈페이지의 Your First Game 챕터에서 찾을 수도 있습니다. 압축을 풀어서 생성된 프로젝트 폴더에 집어넣으면 아래와 같이 자동으로 프로젝트에 폴더가 추가됩니다.
3. Player 씬 추가
왼쪽 씬 탐색기에서 다른노드 > Area2D검색 >Area2D 를 선택합니다.
노드를 더블클릭하여 이름을 Player로 바꿔줍니다. 그리고 Player.tscn으로 저장합니다.
그리고 자물쇠 아이콘 옆의 아이콘을 클릭해줍니다. 추후 자식 노드들이 추가될텐데, 자식노드들만 따로 선택되는 것을 방지해주는 기능이라고 합니다.
Player 노드 하위에 AnimatedSprite를 검색하여 추가합니다.
AnimatedSprite노드가 추가되면 우측의 Frames속성이 아직 [비었음] 상태입니다. [비었음]을 클릭하여 새 SpriteFrames를 선택합니다.
그리고 선택된 SpriteFrame를 한번 더 눌러주면 아래의 애니메이션 패널이 나타납니다.
4. 애니메이션 프레임 추가
Default를 walk라고 이름을 바꾸고, 새 애니메이션 추가 버튼을 클릭하여 up으로 이름으로 만들어줍니다. 아까 추가한 dodge_assets 폴더로 가보면 art라는 하위폴더가 있는데, 여기에서 PlayerGrey_walk1, 2는 walk프레임에, PlayerGrey_up1, 2는 up프레임에 드래그앤드랍으로 추가해줍니다.
다시 AnimationSprite노드를 클릭하고, 인스펙터 창에서 Node2D > Scale에서 x와 y를 각각 0.5로 입력하여 사이즈를 반으로 줄여줍니다.
5. 충돌감지노드 추가
Player노드에서 우클릭하여 새 노드를 추가합니다.검색창에 CollisionShape2D를 검색하여 추가해줍니다.
이 노드는 현재의 노드가 장애물(Mob)과 충돌을 감지할 수 있는 영역을 만들수 있게 해 줍니다. 즉, 이 노드만으로는 안되고, 실제 감지되는 영역을 지정해줘야합니다. 인스펙터 창에서 Shape > [비었음] 클릭 > 새 CapsuleShape2D 를 클릭하여 추가해줍니다.
그러면 캐릭터에 캡슐형 감지영역이 나타나게 됩니다. 감지영역의 핸들을 조정하여 적당히 커버될 수 있도록 조정합니다. 약간 이미지보다 작게 나타나도록 설정하는 것을 추천합니다.
6. Player 방향키 설정
적당히 진행하며 저장해주시고, 이제 Player의 움직임을 설정해줘야합니다. 여기서부터 코딩이 들어갑니다. Attach Script 버튼을 클릭합니다.
우선 필요한 변수를 선언합니다. (speed, screen_size)
export 키워드는 작업을 굉장히 편리하게 해 줍니다. 변수 앞에 export를 붙여주면 inpector 창에서 나타나게 됩니다. 그럼 inpector에서 값을 직접 설정하여, 테스트할 때 일일이 코드를 보지 않고도 테스트할 수 있도록 도와줍니다. 물론 스크립트에 설정된 기본값은 무시됩니다.
처음 게임이 시작되면 _ready()함수를 실행하게 되는데, 게임 창의 크기값을 가져와 screen_size라는 변수에 지정하도록 하겠습니다. 이 값은 나중에 활용할 계획입니다. 기존에 있던 pass 부분은 지워줍니다. 아무것도 안하는 함수일 때 필요한 지시어입니다.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
우선 $AnimatedSprite.play() 의$는 get_node()의 단축 표기형입니다. get_node("AnimatedSprite").play()와 동일합니다. 위 코드는 아직 오류가 있는 것이 상하키와 좌우키를 동시에 누르면 1+1=2의 속도로 원래 움직임보다 더 빠르게 움직입니다. 다음으로 화면 밖으로 나가는 것을 방지하기 위해 clamp()함수로 영역을 지정해줍니다. func _process(delta)함수 하부에 이어서 아래 코드를 추가해줍니다.
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
<전체 코드:Player.gd>
extends Area2D
signal hit
export var speed = 400
var screen_size
# Called when the node enters the scene tree for the first time.
func _ready():
screen_size = get_viewport_rect().size
hide()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
# position
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
# 방향 역전
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
$CollisionShape2D.set_deferred("disabled", true)