hueam아카이브
Godot 1주차 수업 본문
Godot 기본
Node
모든 씬 오브젝트의 기본 클래스이다.
Node를 유니티로 따지만 Component와 비슷하다.
허나, 비슷하다 일분
Godot의 Node == Unity의 Component는 아니다.
엄밀히 따지만 필요한 기능 한 가지만 붙어있는 GameObject와 비슷하다.
Node는 다른 노드의 자식으로 할당하여 트리를 만들 수 있습니다.
이런식으로
Player 라는 Characterbody2D Node 밑으로 Sprite2D, CollisionShape2D, AnimationPlayer를 넣어 사용한다.
Scene
유니티의 Scene이라고 생각하면 안된다.
유니티에서 비슷한 기능이라고 하면 Prefab이라고도 할 수 있다
위 Node를 설명할 때 한 Node 자식으로 여러 Node들을 붙었는데
이 때 만들어진 트리를 Scene이라고 하며 이 Scene을 인스턴스로 만들어 다른 Scene에 Node의 자식으로 넣어줄 수 있다.
이 버튼을 누른 뒤 원하는 Scene을 선택하면 인스턴스해서 자식으로 넣어준다.
게임 만들기
수업에서는 플랫포머 게임을 만들기로 하였다
TileMap
Godot에는 TileMap이라는 Node가 존재한다.
TileMap은 유니티와 크게 다를 바가 없다.
이 노드를 넣고 선택을 하게되면
밑에 TileMap이라고 생기면서 창이 올라온다.
보면
타일맵 노트에 타일셋 리소스가 없습니다.
라고 나오는데 Tile Set은 실제 인스펙터 창을 보면
타일셋이 존재하지 않는다.
<비어있음> 옆 화살표를 눌르고 새 Tile Set을 통해 새로운 Tile Set을 만들 순 있다.
허나 이런 식으로 타일셋을 만들게 되면 Scene에만 Tile Set이 저장되어 재활용이 불가능하다.
여러 씬에서 Tile Set을 쓰고싶으면
파일시스템 -> 우클릭 -> 새 리소스 -> TileSet을 찾고 만들기
Texture든 모든 마찬가지로 Node에서 새로 만들기를 하면 씬에 저장된다.
어떤 식으로든 Tile Set을 넣어주고 Tile Set을 클릭해주면
타일셋이라는 탭이 생겨나며 이곳에서 타일맵을 만들 타일들을 넣어줄 수 있다.
타일 이미지를 넣어주면
이런 창이 뜨는데 예 를 누르면 자동으로 타일을 만들어 준다.
다시 씬에서 TileMap을 누르고 밑 창에서도 TileMap창을 열어주면
이런식으로 나온다.
이 툴을 이용해 실제로 타일들을 그릴 수 있는데
왼쪽부터
- 선택
오른쪽에 있는 타일중에서 어떤 타일을 그릴지 선택하는거다. - 칠하기
마우스 위치에 그려주는 툴이다.
Shift를 누르면 직선을 그리고,
Shift + Ctrl을 누르면 직사각형을 그린다. - 행
직선을 그린다. - 직사각형
직사각형을 만든다. - 페인트 통
비어있는 곳을 채운다. - 선택기
Scene화면에서 타일을 클릭하면 알맞는 타일을 선택해준다. - 지우게
키면 그린 타일들을 지운다. - 랜덤
여러 타일을 선택하고 랜덤을 키면 선택된 타일 중 랜덤으로 나온다.
이런 툴을 이용해 타일로 맵을 찍고 나면 확대 해봤을 때 타일이 흐릿하게 보이는 것을 확인 할 수 있는데 이는
프로젝트 설정 -> 렌더링 -> 텍스처 -> 기본 텍스처 필터를 Nearest로 바꿔주면 된다.
Collision TileMap
적용 되어있는 tile set을 클릭하면
나오는 창에서
Physics Layers 요소 추가를 해준다
그럼 이러한 창이 될텐데
Layer하고 Mask로 나뉘는데
- Layer
내가 가지고 있는 충돌 레이어 - Mask
내가 감지할 충돌 레이어
이며 Physics Layers를 추가한뒤
타일셋 창으로 가서
칠하기 -> 속성 편집기 -> 물리 -> 물리 레이어0 선택 하면 오른쪽에 타일들에 콜라이더를 적용 시킬 수 있다.
Camera
Ctrl + n 눌러 빈 새 Scene을 만들어 주고
루트 노드를 다른 노드를 눌러 Camera 2D로 만들어준다.
이 씬을 저장하고 Level씬에서 인스턴스로 자식으로 넣어준다.
디버깅
F6을 누르면 현재 씬 실행이 되는데
그렇게 되면
에디터에 씬창이 이런 식으로 바뀐다.
로컬은 에디터에서 작업하던 씬이고
원격은 실행시킨 씬이 어떤 식으로 돌아가나 보는 것이다.
원격은 이런식으로 root가 존재하는데 게임을 키면 root가 생성이 되고 그 밑으로 내가 만든 씬이 들어가는 식이다.
씬을 교체한다하면 root밑의 씬을 제거하고 추가하는 식이다.
GameManager
파일 시스템에서 우클릭 -> 새 스크립트를 통해 game_manager.gd라는 이름의 스크립트를 만들면 되는데 만들 때 여러 설정들이 있지만 기본으로 설정으로 만들었다.
extends Node
일단은 게임 메니저한테 _ready(유니티 Awake)와 _process(유니티 update)는 필요 없기에 지워줬다.
게임 메니저는 싱글톤 방식으로 어디서든 사용하게 만들건데
고도는 유니티에서 하는 방식과는 다르다.(애초에 개발 언어부터 다르니…)
유니티에 경우
public class GameManager: MonoBehaviour
{
public static GameManger Instance;
private void Awake()
{
Instance = this;
}
}
이런 식으로 사용 하지만 고도는 다르다
프로젝트 설정-> 자동 로드
여기서 경로를 설정하고 노드 이름을 설정한 뒤 추가 버튼을 누르면
설정한 이름 가주고 다른 코드에서 마음대로 사용이 가능하다.
사운드
사운드 에셋을 관리하는 사운드 매니저를 만들어서
string(키 역할)을 통해 사운드를 관리하며
AudioStreamPlayer2D와 string(키)를 받아
받은 AudioStreamPlayer2D에 키와 알맞는 사운드를 실행시켜준다.
extends Node
const SOUND_LASER = "laser"
const SOUND_CHECKPOINT = "checkpoint"
const SOUND_DAMAGE = "damage"
const SOUND_KILL = "kill"
const SOUND_GAMEOVER = "gameover1"
const SOUND_IMPACT = "impact"
const SOUND_LAND = "land"
const SOUND_MUSIC1 = "music1"
const SOUND_MUSIC2 = "music2"
const SOUND_PICKUP = "pickup"
const SOUND_BOSS_ARRIVE = "boss_arrive"
const SOUND_JUMP = "jump"
const SOUND_WIN = "win"
# C# 딕셔너리
var SOUNDS = {
SOUND_CHECKPOINT: preload("res://assets/sound/checkpoint.wav"),
SOUND_DAMAGE: preload("res://assets/sound/damage.wav"),
SOUND_KILL: preload("res://assets/sound/pickup5.ogg"),
SOUND_GAMEOVER: preload("res://assets/sound/game_over.ogg"),
SOUND_IMPACT: preload("res://assets/sound/impact.wav"),
SOUND_JUMP: preload("res://assets/sound/jump.wav"),
SOUND_LAND: preload("res://assets/sound/land.wav"),
SOUND_LASER: preload("res://assets/sound/laser.wav"),
SOUND_MUSIC1: preload("res://assets/sound/Farm Frolics.ogg"),
SOUND_MUSIC2: preload("res://assets/sound/Flowing Rocks.ogg"),
SOUND_PICKUP: preload("res://assets/sound/pickup5.ogg"),
SOUND_BOSS_ARRIVE: preload("res://assets/sound/boss_arrive.wav"),
SOUND_WIN: preload("res://assets/sound/you_win.ogg")
}
# key : value
func play_clip(player: AudioStreamPlayer2D, clip_key: String):
if SOUNDS.has(clip_key) == false:
return
player.stream = SOUNDS[clip_key]
player.play()
Player
CharacterBody2D를 이용해 Player를 만들거여서
빈 새 씬을 만들고 루트 노드를 CharacterBody2D로 만들어준다.
그 후 만든 노드를 Player로 바꿔준다.
Sprite2D
모양이 없기에 Ctrl + A 또는 부모 노드를 누르고 왼쪽 위 노드 추가를 누른 뒤 Sprite2D를 추가 해준다.
그 후 Sprite2D 에 Texture를 넣어준다.
그럼 Texture 그대로 나오게 되는데
Texture 밑에 Animation에
Hframes는 수평으로 몇 개의 프레임의 갯수를 적어주면 되고
Vframes는 수직으로 몇 개의 프레임의 갯수를 적으면 되는데
나 같은 경우는 수평으로 19 수직으로 1으로 넣어줬다.
그럼
이런식으로 하나만 나오게된다.
CollisionShare
씬 창을 보니 루트 노트에 노란 느낌표가 떠있다.
마우스를 가져다 대보면
대충 충돌을 검사할 수가 없다는 내용이다.
충돌을 검사해주기 위해 CollisionShape2D를 Player자식으로 넣어준다.
추가를 해주면 CollisionShape2D에서도 노란 느낌표가 떠있는데 CollisionShape2D에는
이런 경고가 뜬다.
모양이 없다고 뜨는데
CollisionShape2D를 클릭하고 인스펙터를 보면
Shape가 비어있음을 알 수 있다. 나 같은 경우는 Rectangle Shape2D를 만들어 줬다.
AnimationPlayer
플레이어에서 애니메이션을 넣기 위해 AnimationPlayer 노드를 사용할거다.
AnimationTree 노드도 존재한다 AnimationTree가 유니티에서 쓰던 메카님 애니메이션 이다.
일단 그냥 AnimationPlayer를 사용할 것 이다.
노드를 만들고 밑을 보면
이런 창이 뜨게 되는데
위쪽에 애니메이션 버튼을 눌러 새 애니메이션을 통해 Idle 애니메이션을 만들었다.
그럼
이러한 창으로 바뀌게 되는데
이런 식으로 구성되어 있다
그런 뒤
애니메이션 창을 띄어둔 채 인스팩터로 가면
기존에는 없던 속성 옆에 키 모양이 생겼는데
애니메이션에 키를 추가하는 거다.
누르면
이런 창이 뜨는데 그냥 일단 재설정 트랙 만들기 켜져있는 상대로 만들기를 누를거다.
이렇게 만들게 되면
Reset애니메이션이 추가 되는데
Reset
만약에 크기가 커지는 애니메이션과 움직이는 애니메이션이 있을 때
크기가 커지는 애니메이션 -> 움직이는 애니메이션 이 순서 대로 실행 시키면 어떻게 될까?
- 유니티
유니티 같은 경우는 이전 값을 들고 간다.
즉, 크기가 커진 상태로 움직이는 애니메이션이 실행된다. - 고도 같은 경우는 RESET값을 이용한다.
즉, 크키가 커졌다가 움직이는 애니메이션으로 바뀔 때 애니메이션에 크기에 관한 키가 없다면 RESET값으로 바뀌고 실행된다는 것이다.
이런 것들을 이용해서 Idle, Run, Jump, Fall, Hurt 애니메이션을 만들어 준다.
일단 이렇게 만든 플레이어를 Level씬으로 넣고 실행을 시켜도 떨어 지지는 않는다.
왜냐면 우리가 만들 플레이어는 RigidBody가 아닌 CharacterBody기 때문이다.
플레이어 따라 움직이는 카메라
Level에 붙이든 Camera에 붙이든 상관은 없는데
고도에서 Script를 붙일때 가장 중요한 것이 있다.
바로 하나의 노드에는 하나의 스크립트가 붙는다.
일단 Level에 스크립트를 붙이도록하겠다.
씬 창에서 노드 필터 오른쪽에 있는 걸 누르면 스크립트를 붙일 수 있다.
Level을 선택하고 Level.gd 라는 스크립트를 만든다.
카메라가 플레이어를 따라 움직일려면
카메라와 플레이어의 transform이 필요하다.
에디터의 씬 창에서 스크립트 창으로 Ctrl을 누르고 드래그를 하면 해당 노드를 가져오는 코드를 자동으로 써준다
코드는 이렇게 써줬다
extends Node2D
#Node2D를 상속받아 만들어줘
#ready를 실행할 준비가 되면 이걸 수행한다
@onready var player_cam = $PlayerCamera
@onready var player = $Player
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta):
player_cam.position = player.position
카메라와 플레이어를 가져와서 카메라 위치를 플레이어 위치로 바꿔주었다.
입력
프로젝트 설정 -> 입력 맵
입력 맵에서 입력을 설정 할 수 있다.
원하는 이름의 액션을 만들고 이벤트(키)를 추가해준다.
그럼 입력받을 준비는 된거고 스크립트에서 입력을 확인 해주면 된다.
플레이어 스크립트
플레이어에도 스크립트를 넣어준다.
다만 이번에 플레이어한테 스크립트를 넣어줄 때 보면 Node2D를 상속 받을게 아니라 CharacterBody2D다.
아무튼 만들기 하여 스크립트를 작성해 주었다.
extends CharacterBody2D
#이 클래스의 이름은 Player이다
class_name Player
@onready var sprite_2d: Sprite2D = $Sprite2D
@onready var player_audio = $PlayerAudio
@export var gravity: float = 1000.0
@export var run_speed: float = 120.0
@export var max_fall: float = 400.0
@export var hurt_time: float = 0.3
@export var jump_velocity: float = -400.0
enum PLAYER_STATE {
IDLE, RUN, JUMP, FALL, HURT
}
# C# 의 Action
signal state_changed(old_state: PLAYER_STATE,new_state: PLAYER_STATE)
var _state: PLAYER_STATE = PLAYER_STATE.IDLE
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
func state_change(state: PLAYER_STATE) -> void:
if _state == state :
return
state_changed.emit(_state,state)
_state = state
func _physics_process(delta):
apply_gravity(delta)
get_input();
move_and_slide()
calculate_status()
func apply_gravity(dt: float)-> void:
if is_on_floor() == false :
#CharacterBody2D 안에 velocity가있어서 들고 올 수있다.
velocity.y += gravity * dt;
func get_input()->void:
velocity.x = 0
# 입력 맵에서 적용 시켜놓았던 Action이름
if Input.is_action_pressed("left"):
velocity.x = -run_speed
sprite_2d.flip_h = true;
elif Input.is_action_pressed("right"):
velocity.x = run_speed
sprite_2d.flip_h = false;
if Input.is_action_pressed("jump") && is_on_floor() :
velocity.y = jump_velocity
SoundManager.play_clip(player_audio,SoundManager.SOUND_JUMP)
velocity.y = clampf(velocity.y, jump_velocity,max_fall)
if Input.is_action_just_pressed("shoot"):
var dir : Vector2 = Vector2.LEFT if sprite_2d.flip_h else Vector2.RIGHT
func calculate_status()->void:
if _state == PLAYER_STATE.HURT:
return
if is_on_floor():
if velocity.x == 0:
state_change(PLAYER_STATE.IDLE)
else:
state_change(PLAYER_STATE.RUN)
else:
if velocity.y >= 0:
state_change(PLAYER_STATE.FALL)
else:
state_change(PLAYER_STATE.JUMP)
if (dir > 0 && sprite_2d.flip_h) || (dir < 0 && !sprite_2d.flip_h):
shooter.position.x *= -1
enum으로 플레이어의 상태를 지정해주고
signal(C#의 Action)으로 이후 상태에 따라 실행되게 만들었다.
또한 Player 자식 노드로 AudioStreamPlayer2D 노드를 추가한 뒤 스크립트로 가져와 점프할 때 사운드가 재생되게 만듬
상태에 따른 플레이어 애니메이션
Player Scene에서 자식 노드로 Node를 추가해준다.
이름을 PlayerAnimation로 바꿔준 뒤
스크립트를 붙이고
이러한 코드를 작성하였다
extends Node
@onready var animator = $"../AnimationPlayer"
#var로 받아 타입이 뭔지 몰라서 class_name인 Player라고 알려주긴함
@onready var player: Player = $".."
#func _ready():
# 이런 식으로 타입 캐스트
# var p :Player = player
# player.state_changed.connect(_on_player_state_changed)
func _on_player_state_changed(old_state:Player.PLAYER_STATE, new_state:Player.PLAYER_STATE):
var state_str:String = Player.PLAYER_STATE.keys()[new_state].to_lower()
animator.play(state_str)
_on_player_state_changed 함수에 경우
를 눌러주면
오른쪽 창의 노드 창이 이런식으로 바뀌는데 state_changed를 더블 클릭해
자식인 PlayerAnimation을 누르고 연결 해주면 자동으로 함수가 생성되며
몸통을 구현 해주면 된다.
Enemy
적 만들기
플레이어 가져오기
플레이어를 가져오기 위해
플레이어에게 태그를 달고
플레이어 선택 후 노드 -> 그룹 -> 원하는 그룹 이름 -> 추가
이후 추가한 그룹을 이용해 게임 메니저를 살짝 수정해 주었다
extends Node
const GROUP_PLAYER : String = "Player"
var _player_ref : Player
func get_player() ->Player:
if _player_ref == null:
_player_ref = get_tree().get_first_node_in_group(GROUP_PLAYER)
if _player_ref == null:
print("Warning : player does not exist on this scene")
return _player_ref
EnemyBase
모든 적들이 공용으로 쓰는 상위 계층이다
extends CharacterBody2D
class_name EnemyBase
enum FACING{LEFT = -1, RIGHT = 1}
#화면 밖 1000.0픽셀 까지 벗어나면 삭제함
const OFF_SCREEN_KILL_DISTANCE: float = 1000.0
#기본 스프라이트가 바로보는 방향
@export var default_facing: FACING = FACING.LEFT
#적 잡으면 주는 점수
@export var point :int =1
@export var speed : float = 30.0
@export var gravity: float = 800.0
var _facing: FACING = default_facing
var _player_ref : Player
var _is_dead: bool = false
func _ready():
_player_ref = GameManager.get_player()
func _physics_process(delta):
fallen_off()
func fallen_off()->void:
if global_position.y > OFF_SCREEN_KILL_DISTANCE:
queue_free() #메모리 해제(Destroy)
func die():
if _is_dead:
return
_is_dead = true
# 화면 안 에서 죽을때는 다른 행동 못하게 업데이드 막아주고 재거했다.
set_physics_process(false)
hide()
queue_free()
func _on_screen_entered():
pass # Replace with function body.
func _on_screen_exited():
pass # Replace with function body.
VisibleOnScreenNotifier2D
화면에 들어왔냐 나갔냐 하는 signal을 가주고 있음
EnemyBase스크립트에서 시그널을 보냈을 때 실행시킬 함수를 만들어둠
Sprite2D 대신 AnimationSprite2D
Sprite2D 대신 AnimationSprite2D사용해 주었다.
AnimationSprite2D는 AnimationPlayer와 Sprite2D가 합쳐진 녀석으로 애니메이션도 넣을 수 있고
스프라이트도 넣을 수 있는 친구다.
Snail
새 상속 씬으로 EnenyBase를 선택하고 만들어준다.
그 후
를 통해 스크립트를 제거해 주고 다시 스크립트를 만드는데 이번엔 상속을
이런 식으로 만들어준다.
또한 EnemyBase인 CharacterBody2D가 CollisionShape2D가 없어 오류가 뜸으로 생성 해준다
AnimationSprite2D
상속을 받아 씬을 받으면 이런 오류가 뜰것이다.
AnimationSprite2D를 누르고 인스펙터에 가면 SpriteFrame이 비어있음을 볼 수 있는데
새로 만들어 넣어주고 들어간 SpriteFrame을 누르면
이런식으로 밑이 스프라이트 프레임이 생기는데
이걸로 알잘딱 애니메이션 만들면 된다.
RayCast
바닥인지 아닌지 확인하기 위해 레이를 쏜다.
이런 식으로 유니티와 다르게 Node로 되있어 화살표로 어디로 어디까지 쏠지 나온다.
스크립트
extends EnemyBase
@onready var animator = $AnimatedSprite2D
@onready var floor_dectection: RayCast2D = $FloorDetection
# Called when the node enters the scene tree for the first time.
func _ready():
super._ready()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta):
super._physics_process(delta)
apply_gravity_and_move(delta)
check_turn()
move_and_slide()
func apply_gravity_and_move(dt: float)->void:
if is_on_floor() == false:
velocity.y += gravity * dt
else:
velocity.x = speed* _facing
func check_turn()->void:
if(is_on_floor() == false):
return
# floor_dectection.is_colliding() 을 쓸려면 floor_dectection의 타입을 제대로 캐스팅 해줘야만 한다.
if(is_on_wall() ||floor_dectection.is_colliding() == false):
filp_me()
#is_on_wall()
#floor_dectection.is_colliding()
func filp_me()->void:
animator.flip_h = !animator.flip_h
_facing *= -1
floor_dectection.position.x *= -1;
계속 움직이다가 floor_dectection.is_colliding() 실행 했을 시 false가 나오면 바닥이 없다는 뜻으로 방향을 전환 해준다.
고도 꽤 재밌는 거 같다.
또한 유니티도 버전을 올라가면서 속도가 점점 느려져서 쓰면서 살짝 힘들었지만
고도는 그런게 없어서 좋았던거 같다.
한국에서는 고도는 별로 안뽑는거 같아서 아쉬웠다.
'오래남는 공부 > Godot' 카테고리의 다른 글
Godot을 배워보자 (0) | 2023.11.25 |
---|