강의 정리
[Unity] [빗물받는 르탄이] 빗방울 구현하기, UI 만들기, 점수 만들기, 게임 끝내기
lhgenol
2025. 2. 2. 07:43
빗방울 구현하기 - 반복 생성 로직(1-7)
Prefab
- 반복적으로 떨어지는 빗방울을 구현해 보자.
- 게임의 전반적인 진행을 위한 로직은 Game Manager에서 관리해주는 것이 관행적
- 스크립트 폴더에 Game Manager 스크립트 만들고, Game Manager 오브젝트에 적용 시킨 후, Rain 오브젝트를 Prefab화 시켜줘야 함
- 프리팹(Prefab): 일종의 붕어빵 틀. 반복적인, 똑같은 모양의 게임 오브젝트들을 생성하기 위해서 반복적으로 사용하기 위해 만들어 놓은 것
- Assets 폴더에 Prefabs 폴더 생성 -> Rain 오브젝트 -> Prefabs 폴더로 드래그
- 이제 Hierarchy 창의 Rain 오브젝트를 없애도, 프리팹에 있는 Rain 게임 오브젝트를 통해서 반복적으로 생성할 수가 있다.
public class GameManager : MonoBehaviour
{
public GameObject rain;
- 프리팹은 일종의 게임 오브젝트니 public이고, 자료형은 GameObject를 사용하자.
- 이제 GameManager에 아까까지는 없었던 Rain이라는 변수가 생겼다.
- Rain -> None에 Rain 프리팹 넣기 -> 변수 안에 들어감.
- 이제 Rain이라는 변수를 통해 프리팹의 정보를 가져와 반복적으로 생성해주면 됨
Instantiate
- 이제 함수를 만들자.
- 함수: 반복적으로 실행되는 로직을 하나로 묶어 놓은 단위. void Make이름
void MakeRain()
{
}
- 로직(함수)은 중괄호 안에다가 넣어주자.
- 게임 오브젝트 생성은 Instantiate. 이 함수에다가 생성하고 싶은 붕어빵 틀을 넣어주자.
- 이 함수를 실행시키기 위해서는 그대로 함수 이름과 소괄호를 이용해 적어주면 됨
- GameManager가 시작할 때도 호출해 주자. (void Start)
public class GameManager : MonoBehaviour
{
public GameObject rain;
void Start()
{
MakeRain();
}
void Update()
{
}
void MakeRain()
{
Instantiate(rain);
}
}
- 이러면 함수를 불러주게 된다. 콜(call)한다, 호출한다 라고 표현함
- 플레이 해보면, 이제 Hierarchy 창에 Rain 오브젝트를 지워도 Game Manager의 Rain을 통해 빗방울이 생성되는 걸 볼 수 있다.
- 이제 반복적으로 불러주면 되겠다.
InvokeRepeating
void Start()
{
InvokeRepeating();
MakeRain();
}
- InvokeRepeating은 함수를 반복적으로 실행해 주는 기능.
- 소괄호 안에는 세 가지가 들어감.
- "함수의 이름"
- 몇 초 이후에 생성할래?
- 얼마나 자주 생성할래?
void Start()
{
InvokeRepeating("MakeRain", 0f, 1f);
}
- MakeRain을 0초 이후에 1초마다 생성
- 밑줄 MakeRain();은 이제 필요 없어서 지웠다.
- InvokeRepeating을 사용하게 되면 MakeRain이라고 하는 함수를 호출할 수 있다.
- 플레이 해보면, 반복적으로 빗방울이 계속해서 생성되는 것을 볼 수 있다.
- 이제 르탄이가 빗방울을 맞았을 때 점수가 올라가게 해야 한다.
점수 올라가게 하기(1-8)
Canvas
- 이제 점수 보드를 만들어 보자.
- 점수는 UI(User Interface)를 사용해서 보여주자.
- 유니티에서 UI는 Canvas, 일종의 도화지 위에 그려지게 됨
- 카메라를 통하는 게 아닌, 바로 화면에 그려지게 하는 그래픽적인 요소
- 그래서 카메라 위치와는 관계가 없이 보여지게 됨
- 버튼, 텍스트, 순위를 보여줄 때 사용하게 된다.
- 자료 폰트를 Assets에 Fonts 폴더를 만들어 그 안에 넣어주자.
UI 만들기
- 점수를 띄워 줄 수 있는 UI를 만들자.
- 'Hierarchy' -> UI -> Legacy -> Text
- Canvas를 클릭하면 영역이 보이는데, 이것이 카메라가 찍는 게 아니라 화면에 바로 그려지는 영역.
- Rect Transform을 순서대로 0, -550, 0, 200, 200으로 맞추기
- Text는 '현재 점수', 폰트와 정렬, 컬러도 바꿔줌
- 메인 카메라에는 중앙 하단에 아무것도 안 보이지만 게임 씬에서는 보인다. (그것이 Canvas니까)
- Text를 3개 더 복제해 각각 ScoreLabel, Score, TimeLabel, Time으로 하자.
- (생략) 위 순으로 Text의 이름과 폰트 설정을 바꿔주었다.
싱글톤(Singleton)
- 이제 르탄이와 빗물이 충돌했을 때 점수를 올려주고, 그 점수를 UI에 표시해 주자.
- 게임 매니저에 점수를 올려주는 기능을 만들기 전, 게임 매니저에 만든 기능을 다른 스크립트에서 불러올 수 있게끔 사전 작업이 필요하다.
그러니까 싱글톤(Singleton)을 만들자. - 싱글톤(Singleton): 나 하나밖에 없다.
- '이 프로젝트의 GameManager라고 하는 객체는 나 하나밖에 없다.'
또한 여러 스크립트에서 접근이 가능하게 만들어주는 기능 - 싱글톤은 public suatic 키워드 사용, 변수 이름은 Instance
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
- 그리고 Instance에 '나 자신'을 넣어줘야 한다.
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
private void Awake()
{
Instance = this;
}
- Awake 변수 안에다 this라는 키워드를 사용해서 내 데이터, '나 자신'의 데이터를 넣어줄 거라는 뜻
- 사실 싱글톤은 이것보다 더 복잡한데(뭐요?) 이번 시간엔 이 정도만..
AddScore(int score), totalscore
- 이제 점수를 올려주는 기능을 만들어 보자.
public void AddScore(int score)
{
}
- public이라는 키워드와 int score라고 하는 매개변수 키워드가 있다.
- 매개변수: 해당 함수를 다른 곳에서 호출했을 때 데이터를 넘겨주기 위한 공간.
르탄이와 빗물이 충돌했을 때 점수를 넘겨줘야 하니, 점수를 넘겨줄 수 있게끔 데이터를 보관하기 위한 변수 하나를 만들어주는 것. - 우리는 이 int score 변수를 통해서 넘어온 데이터를 총 점수, totalscore라고 하는 변수를 하나 만들어 놓고 그곳에다가 더해주면 되겠다.
- 그러니 변수를 하나 더 만들어 보자.
int totalScore;
public void AddScore(int score)
{
totalScore += score;
}
- 총 점수에다가 스코어를 계속 넣어주는 것. 더해서 넣어준다(+=)
- 그러면 총 점수에 더해질 때마다 제대로 총 점수에 더해지는지 디버그 로그를 찍어보자.
public void AddScore(int score)
{
totalScore += score;
Debug.Log(totalScore);
}
- 이제 이걸 르탄이가 빗물에 맞았을 때 호출해줘야 하니,
르탄이와 빗물이 충돌할 때 호출해 줄 수 있는 로직을 작성하자. - Rain 스크립트로 이동
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.CompareTag("Ground"))
{
Destroy(this.gameObject);
}
}
- 이전에 빗물이 Ground에 충돌하는 로직은 만들어 놨다.
- 그럼 똑같이 르탄이와 맞았을 때도 어떤 로직을 수행할 수 있게끔 만들어 주면 될 것 같다.
그러기 위해 유니티로 들어가 르탄이에게 Player 태그를 하나 달아준다.
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.CompareTag("Ground"))
{
Destroy(this.gameObject);
}
if(collision.gameObject.CompareTag("Player"))
{
GameManager.Instance.AddScore(score);
}
}
- AddScore 로직을 불러오기 위해, GameManager와 아까 만들어 둔 Instance라고 하는 변수를 통해 접근. 추가로 score 변수를 통해서 거기 안에 있는 데이터를 넘겨주는 것
void MakeRain()
{
Instantiate(rain);
}
public void AddScore(int score)
{
totalScore += score;
Debug.Log(totalScore);
}
- 아까 게임 매니저 스크립트에서, AddScore에 public이라는 것을 붙여줬다.
- public은 외부에서 내 스크립트가 아닌 다른 스크립트에서 꺼낼 수 있게끔 접근을 허용해 주는 키워드.
- 이 public이 없으면 기본적으로 접근이 안 되는 private로 설정이 되기 때문에 void MakeRain()은 Rain 스크립트에서 꺼내 쓸 수 없다.
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.CompareTag("Ground"))
{
Destroy(this.gameObject);
}
if(collision.gameObject.CompareTag("Player"))
{
GameManager.Instance.AddScore(score);
Destroy(this.gameObject);
}
}
- Rain 스크립트로 돌아와서, 르탄이와 빗방울이 충돌한다면 Destroy 되게끔 코드를 작성. 추가로 AddScore, 점수가 더해지고 디버그 로그도 찍힐 것.
- 그런데 플레이 해보면? 엥~ 충돌이 일어나지 않음
- 아차차 르탄이한테도 Box Collider 2D를 쥐어주자. (Edit Collider도)
- 다시 플레이 해보면? 휴~ 충돌이 일어나고 디버그 로그에 토탈 값도 표시된다.
- 그럼 마지막으로 더해진 점수를 UI, Score라고 하는 텍스트에다가 넣어주면 되겠다.
- Text 컴포넌트를 가져오려면 GetComponent라고 하는 기능을 통해서 가지고 올 수 있다.
- 그런데 문제는 게임 매니저에서 Text 컴포넌트가 필요한데,
현재 게임 매니저가 붙어 있는 곳과 우리가 가지고 올 Text 컴포넌트가 붙어있는 곳이 같지 않다. - 이럴 때는 Text 컴포넌트, Text라고 하는 타입을 가져와 가지고 활용할 수 있다. 다시 Game Manager 스크립트로 돌아가자.
- 그런데 문제는 게임 매니저에서 Text 컴포넌트가 필요한데,
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public Text totalScoreTxt;
public void AddScore(int score)
{
totalScore += score;
totalScoreTxt.text = totalScore;
Debug.Log(totalScore);
}
}
- using UnityEngine.UI;. UI에 관련된 기능들이 using문, using이라고 하는 일종의 패키지. UI와 관련된 패키지를 가지고 올 수 있게 된 것.
- 그런데 totalScoreTxt.text = totalScore; 이렇게 해도 빨간 줄이 뜬다. 왜일까?
- 넣어줄 땐 자료형(타입)에 맞춰줘야 한다.
- text에 커서를 대보면 string이라고 되어 있다. 쌍따옴표(" ") 안에 넣어줘야 하는 string이다.
- 그런데 totalScore는 int다. 그럼 int를 string으로, 숫자를 문자열로 어떻게 바꿀까?
ToString
public void AddScore(int score)
{
totalScore += score;
totalScoreTxt.text = totalScore.ToString();
Debug.Log(totalScore);
}
- totalScore 뒤에 점(.)을 넣고, ToString이라고 하는 함수를 호출해 주면 된다.
- ToString, String으로 만들어 준다는 뜻
- 숫자 자료형을 ToString이라는 함수를 통해서 String 자료형, 문자열 자료형으로 바꿔줬고 그걸 이제 Text에다가 넣어 주게 되는 것
- 이건 꼭 int뿐만 아니라 float, 소수점 숫자 자료형도 ToString을 쓸 수 있다.
- 이제 디버그 로그는 지우고, 게임 매니저 인스펙터 -> Total Score Txt에 Score 오브젝트를 넣어주면, Score 게임 오브젝트에 들어있는 Text 컴포넌트를 가지고 올 수 있게 된다.
- 플레이 해보면 점수 UI까지 잘 반영이 된 것을 볼 수 있다.
- 다음에는 시간이 줄어드는 로직을 만들고, 0초가 되면 게임이 끝났다는 UI를 띄워주며 그 UI를 클릭하게 되면 다시 게임이 시작될 수 있도록 만들어 보자.
게임 끝내기(1-9)
게임 오버 UI 만들기
- 일단은 게임 오버 UI를 만들자.
- 캔버스에 UI 이미지 만들고, 이미지에 레거시 -> 텍스트 추가
- 텍스트의 인스펙터 창에서 stretch를 누르고, 옵션 + 시프트 누른 상태로 맨 우측 하단 클릭하면 텍스트 크기가 이미지 사이즈에 맞춰지게 된다.
Time.deltaTime
- 이제 타이머 기능을 만들어 보자.
- 기본 30초에서 계속해서 시간을 깎아야 한다. (빼서 넣어주기)
float totalTime = 30.0f;
- 30이라는 값을 totalTime이라는 변수에 넣어줬다.
- 그럼 이제 30이라는 값에 일정한 값을 빼서 넣어주자.(-=)
float totalTime = 30.0f;
void Update()
{
totalTime -= Time.deltaTime;
}
- Time.deltaTime: 시간을 가져오는 키워드. 모든 기기들이 같은 시간 값을 가질 수 있도록 조정해 줌. 성능 차이에 따른 시간 차이가 없게 프레임 값에 대비해서 만들어준 것.
- 이 deltaTime을 30초라고 하는 전체 시간에서 지속적으로 빼서 넣어주자.
float totalTime = 30.0f;
void Update()
{
totalTime -= Time.deltaTime;
Debug.Log(totalTime);
}
- 디버그 해보면, 콘솔 창에서 시간이 줄어드는 것을 볼 수 있다.
- 이제 이 줄어들고 있는 숫자를 남은 시간 UI에 넣어주자.
- totalScore Text 컴포넌트를 가져올 때 ToString이라는 메소드를 통해서 작성했었다. 똑같이 하기 위해서는 넣어줘야 될 텍스트 컴포넌트가 필요하다.
public Text timeTxt;
float totalTime = 30.0f;
void Update()
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString();
}
- 게임 매니저 인스펙터 창의 Time Txt 변수에 Time 오브젝트를 넣어주고 플레이 해보면 UI에 잘 반영된 모습을 볼 수 있다.
- 문제는 소수점 5자리까지 표시가 되고 있어서, 둘째 자리까지만 나오게 바꿔야 한다.
public Text timeTxt;
float totalTime = 30.0f;
void Update()
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString("N2");
}
- N2만 넣으면 된다.
- N2: 소수점 둘째 자리까지만 표현해달라고 하는 명령
- 하지만 30초가 지나니 마이너스까지 세고 앉았다(하..). 0초가 됐을 때 게임을 멈춰주고 미리 만들어 놓은 EndPanel(게임 오버 UI)을 켜줘야 한다.
- 위 코드는 totalTime이 0보다 클 때만 실행이 되어야 하니, 조건문을 사용하자.
void Update()
{
if(totalTime > 0f)
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString("N2");
}
else
{
}
}
- if문에 있는 조건문 외 나머지 모두를 표현하고 싶을 때는 else라고 쓰면 된다.
- 그러면 totalTime이 0이거나, 0보다 작아질 때 해당 else문 밑에 있는 로직들이 수행되게 된다.
- else에서 게임을 멈춰주자.
void Update()
{
if(totalTime > 0f)
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString("N2");
}
else
{
Time.timeScale = 0f;
}
}
- Time.timeScale = 0f;. 말 그대로 Time의 크기를 0으로 만들어 준다는 것
= 첫 번째 프레임과 그 다음 프레임까지에 시간의 차이가 없어진다는 것 - 그러면 결국 게임이, 시간이 멈추는 효과를 연출할 수가 있다.
void Update()
{
if(totalTime > 0f)
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString("N2");
}
else
{
totalTime = 0f;
Time.timeScale = 0f;
}
}
- 미세한 오차로 마이너스가 될 수도 있기에 추가로 totalTime을 0으로 세팅해 줬다.
void Update()
{
if(totalTime > 0f)
{
totalTime -= Time.deltaTime;
}
else
{
totalTime = 0f;
Time.timeScale = 0f;
}
timeTxt.text = totalTime.ToString("N2");
}
- timeText에다가 또다시 totalTime을 String으로 바꿔서 넣어줘야 하니,
timeTxt.text = totalTime.ToString("N2");은 if else 모두에게 해당되는 중복된 코드가 된다(모두 공통된 로직이어서). - 그러니 조건문을 다 끝낸 다음(if문을 탈출한 다음)에 넣어주자.
- totalTime이 0보다 커도 totalTime에 어떤 값을 빼 주고, if문을 탈출한 다음 textTime에 totalTime값을 넣어주는 것(0과 같거나 혹은 0보다 작아질 때, totalTime을 0으로 만들어 주고, 그 값을 text에다가 넣어주는 것) 두 곳에 다 필요하기 때문.
- 플레이 해보면 30초가 끝났을 때 타이머와 빗방울이 멈추는 것을 볼 수 있다.
- 이 상태에서 이제 EndPanel을 켜줘야 한다. EndPanel 변수를 만들어 주자.
public GameObject endPanel;
- 이후 유니티로 돌아가 GameManager에 End Panel을 넣어줬다.
SetActive
- 이제 타이머가 종료되었을 때 End Panel의 체크박스를 켜주는 로직을 만들자.
- 타임이 끝났을 때 시작되는 로직이니, totalTime = 0f; 밑에다가 작성하자.
void Update()
{
if(totalTime > 0f)
{
totalTime -= Time.deltaTime;
}
else
{
totalTime = 0f;
endPanel.SetActive(true);
Time.timeScale = 0f;
}
timeTxt.text = totalTime.ToString("N2");
}
- Active = 활성화 되어있는
- SetActive = 활성화 되어있는 상태를 세팅해 주겠다는 뜻
- 플레이 해보면 타임 종료 후 End Panel이 켜지는 모습을 볼 수 있다.
RetryButton
- 이제 End Panel을 누르면 게임이 다시 실행될 수 있게끔 로직을 짜보자.
- RetryButton 스크립트를 하나 만들고, EndPanel 게임 오브젝트에 Button 컴포넌트를 추가해 준다.
using UnityEngine;
public class RetryButton : MonoBehaviour
{
텅
}
- void Start, viod Update 다 지워줬다. (편안~)
- 게임을 재실행시켜 주기 위해서는 복잡한 과정 필요 없이, 현재 게임이 작업되고 있는 Main Scene을 다시 불러주면 된다.
using UnityEngine;
using UnityEngine.SceneManagement;
public class RetryButton : MonoBehaviour
{
public void Retry()
{
}
}
- Main Scene을 불러주기 위해 using UnityEngine.SceneManagement;를 적어줬다.
외부에서 쓸 수 있도록 public void Retry도 추가했다. - Main Scene을 불러오기 위해서는 SceneManagement안에 있는 SceneManager를 이용해야 한다.
public void Retry()
{
SceneManager.LoadScene();
}
- 이 소괄호 안에다가 String 값으로 MainScene이라고 하는 문자열을 넣어주자.
public void Retry()
{
SceneManager.LoadScene("MainScene");
}
- 이렇게 로직을 적어주면 Main Scene이라는 이름의 Scene을 로드하라는 뜻이 됨
- 이 로직이 제대로 동작하기 위해서는 Retry 함수를 어딘가에서 불러줘야 한다.
- 어디서? 버튼을 클릭했을 때!
- 버튼을 클릭했을 때 Retrt 함수가 호출될 수 있게 유니티로 넘어가 시도해 보자.
- EndPanel -> Button -> On Click이라는 박스가 있다. 이것이 '클릭을 눌렀을 때'라는 뜻. 클릭을 했을 때 뭘 해주고 싶으냐. 그것에 대해서 아래 + 버튼을 눌러 원하는 로직, 이벤트들을 추가해 줄 수 있다.
- 그러기 위해서 아까 만들어 놓은 RetryButton 스크립트를 EndPanel 게임 오브젝트의 컴포넌트에 넣어준다.
- 이제 + 버튼을 눌러보면 하나의 리스트가 생긴다. None에다 Endpanel 게임 오브젝트를 넣어준다. 그럼 No function 버튼이 활성화 된다. 여기서 RetryButton 안으로 들어가서 아까 만들어 놓은 Retry 함수를 등록하자.
- 그러면 EndPanel이 뜨고 그걸 눌렀을 때 이 Retry 함수가 호출이 된다. 그리고 이 Retry 함수에는 Main Scene을 불러오는 로직이 있으니까 다시 Main Scene이 로드되면서 게임이 처음으로 돌아오게 된다. (헥헥)
- 플레이 하고 Retry 버튼을 누르면, 게임이 처음으로 돌아오긴 하는데 빗물이 떨어지지 않고 시간도 흐르지 않고 있다. 왜!!!
- 왜냐면 Time.timeScale 값을 0으로 만들어 줬었는데 이걸 다시 1로 돌려 줘야 함..
- timeScale 1로 맞추러 가자. GameManager 스크립트 ㄱㄱ
private void Awake()
{
Time.timeScale = 1.0f;
}
- 이렇게 하고 플레이 해보면 Retry 버튼을 눌렀을 때 게임이 다시 정상적으로 실행되는 모습을 볼 수 있다.
빨간 빗방울 만들기(숙제)
[숙제 설명]
받으면 오히려 5점 감점되는 빨간 빗방울을 만들어봅시다!
내가 만든 빗방울의 메커닉을 살짝 수정해서 재미있는 게임을 만들 수 있어요!
게임 내 에셋을 생성하고 배치하고, 수정하는 과정을 복습해보세요.
[필수숙제]
랜덤한 빗방울을 하나 추가합니다.
빗방울의 사이즈는 0.8, 색깔은 255, 100, 255로 설정해주세요.
맞으면 -5점 감점되도록 설정해주세요!
Random.Range, renderer.color
void Start()
{
renderer = GetComponent<SpriteRenderer>();
float x = Random.Range(-2.4f, 2.4f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
int type = Random.Range(1, 4);
if(type == 1)
{
size = 0.8f;
score = 1;
renderer.color = new Color(100 / 255f, 100/255f, 1f, 1f);
}
else if(type == 2)
{
size = 1.0f;
score = 2;
renderer.color = new Color(130 / 255f, 130/255f, 1f, 1f);
}
else if(type == 3)
{
size = 1.2f;
score = 3;
renderer.color = new Color(150 / 255f, 150/255f, 1f, 1f);
}
transform.localScale = new Vector3(size, size, 0);
}
- 지난번에 작성한 Rain 스크립트 코드
- 1~3 type의 빗방울까지 만들었으니, 네 번째 타입을 만들어 주자.
int type = Random.Range(1, 5);
if(type == 1)
{
size = 0.8f;
score = 1;
renderer.color = new Color(100 / 255f, 100/255f, 1f, 1f);
}
else if(type == 2)
{
size = 1.0f;
score = 2;
renderer.color = new Color(130/255f, 130/255f, 1f, 1f);
}
else if(type == 3)
{
size = 1.2f;
score = 3;
renderer.color = new Color(150/255f, 150/255f, 1f, 1f);
}
else if(type == 4)
{
size = 0.8f;
score = -5;
renderer.color = new Color(255/255f, 100/255f, 255/255f, 1f);
}
transform.localScale = new Vector3(size, size, 0);
- 맨 위 Random.Range 값을 (1, 4);에서 (1, 5);로 수정했다(5는 반영 안되기 때문).
- 그리고 else if를 추가해, 사이즈 0.8f, 점수 -5 감점, 255, 100, 255 RGB값의 빨간색 빗방울을 만들어 줬다.
- 빗방울.. 잘 나오긴 하는데 왜 핫핑크인가요? RGB값 제대로 넣었는데..
- 힌트를 보니 컬러값이 (255 / 255f, 100 / 255f, 100 / 255f, 1f)으로 되어있다.
- 255, 100, 255 아닌가?
- 일단 기능에 문제는 없으니 제출하고 2주차 강의로 넘어가자.