본문 바로가기

IT/유니티

13. 유니티 교육 (Sebastian Lague 강의3)

1. 벽돌피하기 게임


스크립트 위주로 설명하겠다.

우선 5개의 스크립트가 필요하다.


playercontroller: 플레이어를 컨트롤하기 위한 스크립트

fallingBlock: 블럭제어를 위한 스크립트

spawner: 블럭을 생성을 제어하는 스크립트

difficulty: 난이도 조절을 위한 스크립트

gameOver: 게임오버와 재시작을 위한 스크립트




설명과 팁은 스크립트 아래를 참조



playercontroller.cs


using UnityEngine;

using System.Collections;

using System;


public class PlayerController : MonoBehaviour {


    public float speed = 7;


    //하드코딩하는건 정말 좋지 않다.

    float screenHalfWidthInWorldUnits = 4.8f;

    public event Action gameOver;


// Use this for initialization

void Start () {

        //너비의 절반

        float halfPlayerWidth = transform.localScale.x * 0.5f;

        //Camera.main.aspect * Camera.main.orthographicSize으로 화면 너비의 절반을 찾아낸다.

        //하지만 이렇게만하면 플레이어가 절반만 들어가므로 플레이어 너비의 절반을 더한다.

        screenHalfWidthInWorldUnits = Camera.main.aspect * Camera.main.orthographicSize + halfPlayerWidth;

    }

// Update is called once per frame

void Update () {

        //내가 한 방식

        /*

        Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), 0);

        Vector2 direction = input.normalized;

        Vector2 velocity = direction * speed;

        transform.Translate(velocity*Time.deltaTime);

        */


        //새바스찬방식

        float inputX = Input.GetAxisRaw("Horizontal");

        float velocity = inputX * speed;

        transform.Translate(Vector2.right * velocity * Time.deltaTime);


        //양끝단에 가면 다른쪽으로 나온다.

        if (transform.position.x < -screenHalfWidthInWorldUnits)

        {

            transform.position = new Vector2(screenHalfWidthInWorldUnits, transform.position.y);

        }


        if (transform.position.x > screenHalfWidthInWorldUnits)

        {

            transform.position = new Vector2(-screenHalfWidthInWorldUnits, transform.position.y);

        }


    }


    void OnTriggerEnter2D(Collider2D col)

    {

        if (col.tag == "FallingBlock")

        {

            if (gameOver != null)

            {

                gameOver();

            }

            Destroy(gameObject);

        }

    }

}


fallingBlock.cs

using UnityEngine;

using System.Collections;


public class FallingBlock : MonoBehaviour {


    public Vector2 speedMinMax;

    public float speed;


    float visibleHeightThreshold;


// Use this for initialization

void Start () {

        speed = Mathf.Lerp(speedMinMax.x, speedMinMax.y, Difficulty.GetDifficultyPercent());


        visibleHeightThreshold = -Camera.main.orthographicSize - transform.localScale.y;

}

// Update is called once per frame

void Update () {

        transform.Translate(Vector3.down * speed * Time.deltaTime, Space.Self);


        //만약 기준을 글로벌로 하면 아무리 회전을 하더라도 진행방향은 글로벌기준이 될것이다.

        //transform.Translate(Vector3.down * speed * Time.deltaTime, Space.World);


        if (transform.position.y < visibleHeightThreshold)

        {

            Destroy(gameObject);

        }


    }

}


Spawner.cs

using UnityEngine;

using System.Collections;


public class Spawner : MonoBehaviour {


    public GameObject fallingBlockPrefab;

    public Vector2 secondBetweenSpawnsMinMax;

    float nextSpawnTiime;

    Vector2 screenHalfSizeWorldUnits;

    public Vector2 spawnSizeMinMax;

    public Vector2 spawnAngleMinMax;


    // Use this for initialization

    void Start () {

        //잊지말자. 카메라의 중심점은 바로 정 가운데이다.

        //맨날 하드코딩만 하다보니가 왼쪽위가 0.0으로 생각하내.ㅠㅠ

        screenHalfSizeWorldUnits = new Vector2(Camera.main.aspect * Camera.main.orthographicSize, Camera.main.orthographicSize);

}

// Update is called once per frame

void Update () {

        if (Time.time > nextSpawnTiime)

        {

            float secondBetweenSpawns = Mathf.Lerp(secondBetweenSpawnsMinMax.y, secondBetweenSpawnsMinMax.x, Difficulty.GetDifficultyPercent());

            float spawnAngle = Random.Range(spawnAngleMinMax.x, spawnAngleMinMax.y);

            float spawnSize = Random.Range(spawnSizeMinMax.x, spawnSizeMinMax.y);


            nextSpawnTiime = Time.time + secondBetweenSpawns;

            Vector2 spawnPosition = new Vector2(Random.Range(-screenHalfSizeWorldUnits.x, screenHalfSizeWorldUnits.x), screenHalfSizeWorldUnits.y+ spawnSize);


            


            GameObject block =  (GameObject)Instantiate(fallingBlockPrefab, spawnPosition, Quaternion.Euler(Vector3.forward * spawnAngle));

            block.transform.localScale = block.transform.localScale * spawnSize; 

        }

        

}

}

Mathf.Lerp와 Mathf.Clamp01의 조합으로 난이도 조절을 정말 간편하게 할수 있다.

잊지말고 체크하고, 반드시 나중에 적용하자.


Difficulty.cs

using UnityEngine;
using System.Collections;

public static class Difficulty{

    static float secondsToMaxDifficulty = 60f;
    public static float GetDifficultyPercent()
    {
        return Mathf.Clamp01(Time.timeSinceLevelLoad / secondsToMaxDifficulty);
    }
}


GameOver.cs

using UnityEngine;

using UnityEngine.UI;

using UnityEngine.SceneManagement;

using System.Collections;


public class GameOver : MonoBehaviour {


    public GameObject gameOverScreen;

    public Text secondsSurvivedUI;

    bool gameOver;


    PlayerController player;


    // Use this for initialization

    void Start () {

        player = FindObjectOfType<PlayerController>();

        player.gameOver += OnGameOver;


    }

// Update is called once per frame

void Update () {

        if (gameOver)

        {

            if (Input.GetKeyDown(KeyCode.Space))

            {

                SceneManager.LoadScene(0);

            }

        }

}


    void OnGameOver()

    {

        gameOverScreen.SetActive(true);

        secondsSurvivedUI.text = Mathf.RoundToInt(Time.timeSinceLevelLoad).ToString();

        gameOver = true;

    }

}





Tip: 화면너비의 절반을 구하는 방법

Camera.main.aspect * Camera.main.orthographicSize;

그냥 단순히 위의 코드를 이용하면 카메라 화면의 너비절반을 구할수 있는데, 어떻게 구했는지는 알고 가자

Camera.main.aspect는 화면 비율이다. 즉 너비/높이

Camera.main.orthographicSize는 높이/2를 의미한다.

산수시간에 졸지 않았다면 바로 알수 있을것이다.

너비/높이 * 높이/2는 결국 너비/2가된다.

오키도키? 나는 영어로 봐서 그런지 한참만에 이해했다.

ㅠㅠ


Tip. Mathf.Clamp(value, min, max)

값이 최대값보다 크면 최대값만 반환

값이 최소값보다 작으면 최소값만 반환



Tip. 난이도 조절하기

우선 아래를 보자.

난이도를 조절한다 함은

일정시간동안을 기다리다가 그 일정시간이 지나면, 다음 난이도를 가는것을 의미한다.

위의 a,b의 의미는 난이도를 의미한다.

a가 낮은 b가 높은 난이도다.

p는 퍼센트를 나타낸다. 시간에 따른 퍼센트

결국 0과 1만을 반환할것이다.

0과 1일때의 차이는 위의 식을 보면, a와 b를 나눌수 있다.

별거 아닌거 같지만 단순한 수식으로 관리하기 편한 난이도 식을 만들수 있다.


위와 같은 것을 Linear Interpolation즉 한국말로는 선형보간법이라고 한다.

직선의 움직임에 대한 보간 방법이라는데 어렵다.ㅠㅠ




Tip. Mathf.Lerp사용해보자

Lerp는 Linear Interpolation의 줄인말이다.

보통 3개의 인자가 필요하다.

Mathf.Lerp(a, b, p)라고 하면 리턴하는 값은다음과 같다.

a와 b사이의 값차이가 있는데, 그것을 퍼센트만큼만 조절해서 리턴한다.

우리가 만든 게임에서 예를 들어보면 

Mathf.Lerp(secondBetweenSpawnsMax, secondBetweenSpawnsMin, Difficulty.GetDifficultyPercent());

스폰타임은 짧은수록 난이도는 올라갈것이다.

Difficulty.GetDifficultyPercent()는 게임이 시작되고 60초로 나눈값이다.

결국 60초가 될때까지 점진적으로 스폰시간이 짧아진다. 언제까지 min값까지.


아....나는 스위치문 엄청써서 시간에 맞쳐서 일일이 다 코딩했는데,

이렇게 수학적으로 딸리는데 겜개발 해야할까 모르겠다.

세상은 넓고 나보도 똑똑한 사람은 널리고 널렸다. 겸손하게 살아야 한다.



Tip. Canvas고정시키기

화면크기를 조정하다보면 캔버스의 위치도 바뀌게 되어서 고생할때가 있다.

그러면 캔퍼스를 필셀이 아니라 너비와 높이에 맞쳐서 고정시키면 된다.

캔버스 클릭하고 inspector에 보면

Screen Match Mode에서 Match Width Or Height로 두면 된다.



Tip. Time.time, Time.timeSinceLevelLoad

첨에 봤을때는 그냥 전에 봤던 time인줄 알고 현재시간을 반환하는 줄 알았지만,

반환하는 값은 플레이되고 난뒤 지난 시간을 반환한다.

그럼 위의 2개의 차이는 무었일까? 

유니티는 씬으로 화면 전환등 여러가를 할수 있기때문에 

씬이 로드된다음의 시간은 두번째 것으로 알수있다.