노마드 코더 (Nomad Coders) 강의 따라하기 - KIMCHI-Run 게임


 노마드 코더 Nomad Coders (https://www.youtube.com/@nomadcoders) 님의

Unity 6 로 횡스크롤 게임 만들기 강의가 있어서 따라해본다....


Kimchi Run 만들기

https://www.youtube.com/watch?v=A58_FWqiekI


6.1 로 따라서 만든 최종 버전: https://github.com/sgchoi5/Unity_Study_Projects/blob/main/Kimchi-Run.zip

Unzip 후에 Unity Hub 에서 Add Project 에서 Add from repository 를 이용해서 import



Installation 부터 완전 초보자 대상 강의라 자잘하게 배울 게 많으네..

Kimchi-Run Project 생성

Project Tab 에서 Assets - Scenes - SampleScene 을 main 으로 변경

                             Assets 폴더에 Create Folder - Sprites 생성

                             * Sprites 는 게임에 사용할 이미지

assets.zip 내의 파일들을 Sprites 폴더에 드래그 & 드랍

폴더 내의 저해상도 이미지들은 유니티에서 보기가 좋지 않은데, 

픽셀이 보이지 않도록 최적화해주는 Unity 때문



Filter Mode 에는 세 가지가 있는데, Point 로 변경

- Point (No filter) : 픽셀 하나만 참고. 뚜렷하지만 계단현상 심함

- Bilinear : 4개 픽셀 참고. 부드럽고 자연스럽지만 약간 흐림

- Trilinear : Bilinear + Mipmap 까지 부드럽게 보간. (특히 축소할 때 더 부드러움)


--- Background


3D Object > Quad 추가 (Width / Height 만 존재)


Quad 에는 Sky 이미지를 지정해주지만, 이미지가 반복되도록 하기 위해서 Repeat 으로 변경


Repeat : 텍스처 반복

Clamp : 늘려서

Mirror : 반복될 때 뒤집힘

Mirror Once : 뒤집힘, 이후는 늘림

Per-axis : 


Repeat 으로 설정한 이후에는 Materials 폴더가 생기고, 어떻게 반복할지에 대한 설정을 해줄 수가 있다


1 -> 2 로 변경 (가로를 2장으로 반복)

Offset 의 X 값을 변경해서 횡스크롤을 표현할 수 있다 (나중에 코드로 처리)


Building png 이미지의 투명 영역을 이용할 수 있도록

Building materials 의 속성을 Transparent 로 바꿔준다



Surface Type 은

- Opaque : 불투명

- Transparent : 투명


Sky, Building, Platform 순으로 보이게 하기 위해서

Transform 의 Z 값을 100, 90, 80 으로 설정한다


화면에서 카메라 background 색을 변경해서 (Sky 이미지에서 제일 위쪽 색을 스포이드로 지정)

빈 공간을 채우는 효과





--- Scrolling Script


Assets 폴더에 Scripts 폴더 만들고, Create - New - MonoBehaviour script 로 BackgroundScroll script 를 만들자

Header tag 를 이용하면 inspector 에 추가되는 public 변수에 대한 제목이 달린다

Tooltip tag 를 이용하면 Scroll Speed 에 마우스 오버하면 도움말이 출력됨

    [Header("Settings")]
    [Tooltip("How fast should the texture scroll?")]
    public float scrollSpeed;




Mesh Renderer 에 대한 설명


Mesh Renderer 를 script 에서 조정할 수 있도록 한다

    [Header("Reference")]
    public MeshRenderer meshRenderer;


Inspector 에서 추가된 meshRenderer 에 드래그 앤 드랍으로 지정해준다


Sky, Buildings, Platform 에도 동일하게 적용후에


    void Update()
    {
        meshRenderer.material.mainTextureOffset += new Vector2(scrollSpeed * Time.deltaTime, 0);
    }

offset 값을 수정해서 횡스크롤이 발생하도록 처리

Update() 는 frame 단위로 호출이 되기 때문에 더해지는 값에 Time.deltaTime 을 곱해주어야 한다

Time.deltaTime : The interval in seconds from the last frame to the current one

이전 프레임에서 현재 프레임까지의 초 단위 간격으로, Time.deltaTime 을 곱해서 더해주게 되면 1초에 mainTextureOffset 값 단위로 이동하게 된다


--- Player Jump


player_run 이미지를 선택해서 유니티가 제대로 처리했는지 확인해보고, 필요시 Inspector 에서 Open Sprite Editor 를 통해서 조정이 가능


player_run 의 이미지를 드래그 & 드랍으로 Scene 에 두면, animation 으로 저장을 할 수가 있게 된다. 이후에 화면에 아주 작게 보이게 되는데, 이 때에는 Pixels Per Unit 값을 수정해서 크게 보이도록 한다

✅ 하나의 스케일 단위에 몇 개의 픽셀을 넣어야 하는 의미


player 에게 점프 효과를 주기 위해서 Rigidbody 2D component 를 추가하고, 

영원히 추락하지 않도록 Box Collier 2D (물체가 서로 부딪히게 하고, 충돌을 감지)를 추가한다


platform 의 경우에는 이미 Mesh Collier componet 가 있는데 삭제하고, Box Collier 2D component 를 추가


Edit Collier 를 통해서 크기 조정 가능

유니티 매뉴얼에서의 Collider 2D 설명

When you attach a collider 2D component to a GameObject, the collider 2D defines which area of the GameObject has collision and can interact with other colliders in the scene. The collider 2D is invisible, and it’s shape is about the same shape as the GameObject for more accurate collisions. You can adjust a collider’s shape and other properties in its Inspector window properties.


바닥에 달리고 있을 때에만 점프하기 위해서 isGrounded 변수를 추가하고

아래 메소드를 통해서 바닥에 착지했을때를 처리 

MonoBehaviour.OnCollisionEnter2D(Collision2D)

입력된 Collider 가 플레이어의 Collider 와 부딪히면 호출


Player.cs

public class Player : MonoBehaviour
{
    [Header("Settings")]
    public float jumpForce;

    [Header("Reference")]
    public Rigidbody2D playerRigidbody;

    private bool isGrounded = true;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
       
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded) {
            playerRigidbody.AddForceY(jumpForce, ForceMode2D.Impulse);
            isGrounded = false;
        }
    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.name == "Platform") {
            isGrounded = true;
        }
    }
}

AddForceY() 로 점프 효과

OnCollisionEnter2D 로 바닥에 착지 처리


--- Animations


Player GameObject 를 선택하고, 메뉴에서 Window - Animation - Animation 선택

(단축키 Ctrl - 6)


Play 버튼을 눌러야 설정된 image 들이 보인다.. 처음에는 안 보여..


Player Run 오른쪽에 드롭다운 버튼을 누르면 Create New Clip... 을 선택해서 새로운 Animation 을 생성할 수 있다

Player Run
Player Jump
Player Land

점프 동작을 위해서 생성한 animation 을 Window - Animation - Animator 에서 처리


Entry 는 Player 의 시작점

Game 을 실행하게 되면, Animator window 에서 실행되는 상황을 볼 수도 있다



Make Transition 을 통해서 순서를 지정

Has Exit Time 을 통해서 자동 진행 여부 결정

Parameters 에서 Trigger 나 Condition 추가를 통해서 상태 진행 조정
Assets - Animations 에서 Player Jump/Land animation 은 Loop Time 을 Uncheck 해서 반복없이 1 회만 수행하게 하고, Player Run 만 반복 수행


❗애니메이션 동작을 확인하기 위해서 게임 자체의 속도를 조정하는 방법도 있다

Edit - Project Settings - Time


Time Scale 값을 0.5 로 하게 되면 게임 플레이 속도가 절반



--- Obstacles part 1


빌딩들의 경우에는 아래 쪽 기준으로 위치할 수 있도록 Center 에서 Bottom 으로 바꿔준다



building 이미지를 추가하고, 실행시 Player 와 이미지가 겹치는 문제 해결을 위해서

Main Camera 의 Transform - Position - Z 값을 -50

Player 의 Transform - Position - Z 값을 -10 으로 설정함


❗다른 강의에서는 겹치는 경우에 Order in Layer 값을 조정했음 (크면 앞)


< 배경 빌딩 처리 방법 >

Building 하나에 Mover.cs 와 Destroyer.cs 를 component 로 추가하고

생성되면 이동하다가 화면을 나가면 파괴도록 처리

Prefabs 에 저장한 후에 복사해서 5개를 만든 후에 이미지를 다르게 지정

Empty GameObject 를 추가해서 Building Spawner 로 하고 Spawner.cs 를 component 로 추가하고, 일정 시간마다 GameObject 를 랜덤하게 생성 처리

Mover.cs

public class Mover : MonoBehaviour
{
    [Header("Setting")]
    public float moveSpeed = 1f;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
       
    }

    // Update is called once per frame
    void Update()
    {
        transform.position += Vector3.left * moveSpeed * Time.deltaTime;
    }
}

Destroyer.cs

public class Destroyer : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
       
    }

    // Update is called once per frame
    void Update()
    {
        if (transform.position.x < -15)
        {
            Destroy(gameObject);
        }
    }
}

Spawner.cs (Inspector 에서 gameObjects 에 빌딩 5개를 prefabs 에서 끌어와서 지정)

public class Spawner : MonoBehaviour
{
    [Header("Settings")]
    public float minSpawnDelay;
    public float maxSpawnDelay;

    [Header("Referece")]
    public GameObject[] gameObjects;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
    }

    void Spawn() {
        GameObject randomObject = gameObjects[Random.Range(0, gameObjects.Length)];
        Instantiate(randomObject, transform.position, Quaternion.identity);
        Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
    }
}



--- Obstacles part 2



바이크 장애물도 기존에 만들어둔 Mover.cs 와 Destroyer.cs 를 재활용하고,
충돌을 위한 Collider 를 추가할때에 Polygon Collider 2D component 를 추가해서 좀 더 정확한 충돌 영역을 설정한다

❗ Physical Interaction 을 피하기 위해서 Is Trigger 를 check

할머니 장애물의 경우에도 동일하게 하고 Capsule Collider 2D componet 를 사용

배추, 마늘, 고추, 황금배추도 동일하게 처리

Enermy, Food, Golden Tag 를 각각 지정해서 Player 의 충돌처리를 준비한다

Player.cs 에 추가
    void OnTriggerEnter2D(Collider2D collider) {
        if (collider.gameObject.tag == "Enermy") {

        } else if (collider.gameObject.tag == "Food") {

        } else if (collider.gameObject.tag == "Golden") {

        }
    }


--- Player Hit and Heal


Player.cs 에 생명과 충돌 처리시 해야할 코드를 넣는다
    public BoxCollider2D playerCollider;
    public int lives = 3;
    public bool isInvincible = false;

    void KillPlayer() {
        playerCollider.enabled = false;
        playerAnimator.enabled = false;
        playerRigidbody.AddForceY(jumpForce, ForceMode2D.Impulse);
    }

    void Hit() {
        lives -= 1;
        if (lives == 0) {
            KillPlayer();
        }
    }

    void Heal() {
        lives = Mathf.Min(3, lives + 1);
    }

    void StartInvincible() {
        isInvincible = true;
        Invoke("StopInvincible", 5f);
    }

    void StopInvincible() {
        isInvincible = false;
    }

    void OnTriggerEnter2D(Collider2D collider) {
        if (collider.gameObject.tag == "Enermy") {
            if (!isInvincible) {
                Destroy(collider.gameObject);
            }
            Hit();
        } else if (collider.gameObject.tag == "Food") {
            Destroy(collider.gameObject);
            Heal();
        } else if (collider.gameObject.tag == "Golden") {
            Destroy(collider.gameObject);
            StartInvincible();
        }
    }

playerCollider reference 변수에 Box Collider 2D 를 할당하지 않았는데, (자주 하는 실수라던데..)
Run 해 보면 crash 가 난다거나 하지는 않고, Console 창에 Warning 이 떠 있어서 빨리 고침



--- Game Manager


GameManager 인스턴스를 게임 어느 곳에도 이용할 수 있도록 하기 위해서
아래와 같이 처리한다

Awake() 는 Start() 보다 먼저 실행

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;
    void Awake()
    {
        if (Instance == null) {
            Instance = this;
        }
    }


Game Intro 와 Game 실행시에 동작하는 것을 GameObject 의 SetActive(true/false) 로 분리해서 처리
GameManager.cs
    [Header("Reference")]
    public GameObject IntroUI;
    public GameObject EnemySpawner;
    public GameObject FoodSpawner;
    public GameObject GoldenSpawner;

    // Update is called once per frame
    void Update()
    {
        if (State == GameState.Intro && Input.GetKeyDown(KeyCode.Space)) {
            State = GameState.Playing;
            IntroUI.SetActive(false);

            EnemySpawner.SetActive(true);
            FoodSpawner.SetActive(true);
            GoldenSpawner.SetActive(true);
        }
    }

Spawner.cs 에서는 OnStart() 대신에 OnEnable() 로 시작하도록 변
    void OnEnable()
    {
        Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
    }
Enable / Disable 로 Spawner 를 처리하기 때문에 Invoke 를 취소하는 처리도 추가 필요
    void OnEnable() {
        Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
    }

    void OnDisable() {
        CancelInvoke();
    }


GameManager 생성해서 게임 상태 관리하는 것이 기본인 듯... 다른 강의에서도 GameManager 로 처리...


--- High Scores


생명 표시를 위한 Heart GameObject 생성

Heart.cs
public class Heart : MonoBehaviour
{
    public Sprite OnHeart;
    public Sprite OffHeart;
    public int LiveNumber;
    public SpriteRenderer spriteRenderer;

    // Update is called once per frame
    void Update()
    {
        if (GameManager.Instance.Lives >= LiveNumber)
        {
            spriteRenderer.sprite = OnHeart;
        } else {
            spriteRenderer.sprite = OffHeart;
        }
    }
}

점수를 표시하기 위해서 
Canvas component 를 추가하고 아래와 같이 설정
그리고, Render Camera 에 Main Camera 를 드래그 앤 드롭 해준다.


Text Componet 추가해서 점수 표


GameManager.cs 에서 score 관련 코드 추가
Score 를 Time.time 값을 기준해서 시작과 차이로 처리

    void Update()
    {
        if (State == GameState.Intro && Input.GetKeyDown(KeyCode.Space)) {
          State = GameState.Playing;
...
            PlayStartTime = Time.time;
        }

    float CalculateScore() {
        return Time.time - PlayStartTime;
    }

    void SaveHighScore() {
        int score = Mathf.FloorToInt(CalculateScore());
        int currentHighScore = PlayerPrefs.GetInt("highScore");
        if(score > currentHighScore) {
            PlayerPrefs.SetInt("highScore", score);
            PlayerPrefs.Save();
        }
    }


게임의 난이도를 위해서 속도 처리는 정적인 moveSpeed 값 대신에
GameManager 가 제공하는 값으로 처리

GameManager.cs
    public float CalculateGameSpeed() {
        if (State != GameState.Playing) {
            return 3f;
        }
        float speed = 3f + (0.5f * Mathf.Floor(CalculateScore() / 10f));
        return Mathf.Min(speed, 20f);
    }

Mover.cs
    void Update()
    {
        transform.position += Vector3.left * GameManager.Instance.CalculateGameSpeed() * Time.deltaTime;
    }

BackgroundScroll.cs 에서도 동일하게 속도를 변경
    void Update()
    {
        meshRenderer.material.mainTextureOffset += new Vector2(scrollSpeed *
            GameManager.Instance.CalculateGameSpeed() / 20 * Time.deltaTime, 0);
    }


마지막으로 Web 빌드로 Publish 하는 과정으로 마침...


2 시간 넘는 강의 과정인데, 내용이 아주 알차게 되어 있어서 따라하다 보면 배우는 게 엄청 많다
추천 👍



댓글

이 블로그의 인기 게시물

나도코딩 - 유니티 무료 강의 (Crash Course) 따라하기

Unity Tip 2 : Visual Studio Code 로 변경