본문 바로가기

수업 복습

2D RogueLike

로그라이크 : 저장기능이 없고, 죽으면 여태까지의 플레이 정보가 초기화된다.

랜덤성이 짙은 게임으로 플레이를 새로 할 때 마다 맵 위치, 아이템의 기능이나 위치가 달라지는 게임

로그라이트 : 로그라이크와 유사하지만 죽은 후에 게임을 다시 시작할 때 마다 죽기 전의 게임정보를 어느정도

받아와 난이도가 쉬워져서 플레이하기 더 쉬워지는 게임

 

패키지를 다운받은 후, 유닛들의 애니메이션과 맵, 아이템들을 프리팹으로 만드는 작업을 해준다.

평소와는 다르게 애니메이션을 만들어 줄 건데, 우선 Enemy1에 Enemy1, Enemy2의 애니메이션을 넣어준다.

그다음에 스프라이트 렌더러에서 스프라이트를 Enemy2의 Idle(기본포즈) 스프라이트로 바꿔주고, Enemy2 프리팹으로 저장한다. 그럼 Enemy2는 Enemy1과 같은 애니메이터를 가지게 된다.

그 다음 Animator Override Controller를 만들어준다.  Animator Override Controller로 애니메이션을 만들겠다는 건

애니메이터의 구성은 똑같이 가져오는데, 거기 들어가는 애니메이션 클립만 바꿔주겠다는 뜻이다.

만들어두었던 애니메이션을 인스펙터창에서 넣어주고, Enemy2의 Animator - Controller에 넣어주면 끝!

#MovingObject

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingObject : MonoBehaviour
{
    public LayerMask blockingLayer;
    public float moveTime = .1f;

    private Rigidbody2D rb2D;
    private BoxCollider2D box2D;
    private float inverseMoveTime;




    // Start is called before the first frame update
    protected virtual void Start()
    {
        rb2D = GetComponent<Rigidbody2D>();
        box2D = GetComponent<BoxCollider2D>();
        inverseMoveTime = 1f / moveTime;
    }


    protected IEnumerator SmoothMovement(Vector3 end)
    {
        float sqrRemainingDistance =
            (transform.position - end).sqrMagnitude;
        while(sqrRemainingDistance > float.Epsilon)
        {
            Vector3 newPosition = Vector3.MoveTowards(transform.position,
                end, inverseMoveTime * Time.deltaTime);

            rb2D.MovePosition(newPosition);
            sqrRemainingDistance =
            (transform.position - end).sqrMagnitude;

            yield return null;
        }
    }
}

protected virtual void Start()

 > virtual(가상 메소드) : 자식 클래스에서 override해서 부모+자식꺼를 더해서 사용할 수 있게 하기 위함.

virtual을 안쓰면 부모에 있는것만 사용

new를 쓰면 자식에 있는것만 사용

통상적으로 묶어서 표현할 땐 부모한테 물려받은것만 사용한다.

 

float sqrRemainingDistance = (transform.position - end).sqrMagnitude;

 > rb2D.MovePosition으로 사용하면 되는데 왜 안썼을까?

  = 현재 Rigidbody는 2D(Vector2)고 end는 Vector3기 때문에 계산이 복잡해서

sqrMagnitude는 Magnitude(점과 점 사이의 길이)를 sqr(곱)한것으로, 루트계산이 필요할 때 루트를 없애기 위해 사용한다.

> 별 반 차이가 없을 땐 컴퓨터가 덜 힘들어하도록 sqrMagnitude를 사용 = 최적화

 

while(sqrRemainingDistance > float.Epsilon)

{

      //MoveTowards = Lerp와 비슷한 개념으로  MoveTowards(a(시작점), b(끝점) t(속도)의 빠르기로)

      //a에서 b로 t만큼의 빠르기로 이동하겠다는 메소드이다.

      //Lerp : 직선상에서의 위치를 구함

      //MoveTowards : 벡터상에서 위치를 구함.

      Vector3 newPosition = Vector3.MoveTowards(

            rb2D.position, end, inverseMoveTime * Time.deltaTime);

      rb2D.MovePosition(newPosition);

      sqrRemainingDistance = (transform.position - end).sqrMagnitude;

 

      //한 프레임 쉰다.

      yield return null;

}

 

#Wall

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Wall : MonoBehaviour
{
    public Sprite dmgSprite;
    public int hp = 4;

    private SpriteRenderer spriteRenderer;

    // Start is called before the first frame update
    void Start()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    public void DamageWall(int loss)
    {
        spriteRenderer.sprite = dmgSprite;
        hp -= loss;
        if(hp <= 0)
        {
            gameObject.SetActive(false);
        }
    }
}

DamageWall은 플레이어가 벽을 허물었을 때 벽의 이미지를 바꾸기 위한 코드로, 저렇게 작성한 후에 스크립트를 넣어주면 아래와 같이 바뀔 sprite를 넣는 공간이 생긴다. 거기에 무너진 벽 이미지를 넣어주면 됨!

 

#BoardManager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoardManager : MonoBehaviour
{
    #region 선언부
    [System.Serializable]
    public struct Count
    {
        public int min, max;
        public Count(int min, int max)
        {
            this.min = min;
            this.max = max;
        }
    }

    public int cols = 8, rows = 8;

    public Count wallCount = new Count(5, 9);
    public Count foodCount = new Count(1, 5);

    public GameObject exit;
    public GameObject[] floorTiles, wallTiles, foodTiles, enemyTiles, outwallTiles;

    private Transform boardHolder;

    private List<Vector3> gridPositions = new List<Vector3>();
    #endregion

    //맵의 구조를 기억하기 위함
    private void InitailizedList()
    {
        for(int x= 1; x< cols -1; x++)
        {
            for(int y = 1; y < rows -1; y++)
            {
                gridPositions.Add(new Vector3(x, y, 0));
            }
        }
    }

    private void BoardSetup()
    {
        boardHolder = new GameObject("Board").transform;

        for(int x = -1; x < cols+1; x++)
        {
            for (int y = -1; y < rows + 1; y++)
            {
                GameObject a;
                if (x == -1 || x == cols||
                    y == -1 || y == rows)
                {
                    a = outwallTiles[Random.Range(0, outwallTiles.Length)];
                }
                else
                {
                    a = floorTiles[Random.Range(0, floorTiles.Length)];
                }

                GameObject go = Instantiate(a,
                    new Vector3(x, y, 0f), Quaternion.identity);

                go.transform.SetParent(boardHolder);
            }
        }
    }

    private Vector3 RandomPosition()
    {
        int randomIndex = Random.Range(0, gridPositions.Count);

        Vector3 randomPosition = gridPositions[randomIndex];

        gridPositions.RemoveAt(randomIndex);

        return randomPosition;
    }
    
    private void LayoutObjectAtRandom(
        GameObject[] tileArray, int min, int max)
    {
        int objectCount = Random.RandomRange(min, max + 1);

        for(int i = 0; i < objectCount; i++)
        {
            Vector3 randomPosition = RandomPosition();
            GameObject tileChoice = tileArray[Random.Range(0, tileArray.Length)];
            Instantiate(tileChoice, randomPosition, Quaternion.identity);
        }
    }

    public void SetupScene(int level)
    {
        BoardSetup();
        InitailizedList();
        LayoutObjectAtRandom(wallTiles, wallCount.min, wallCount.max);
        LayoutObjectAtRandom(foodTiles, foodCount.min, wallCount.max);

        int enemyCount = (int)Mathf.Log(level, 2f);
        LayoutObjectAtRandom(enemyTiles, enemyCount, enemyCount);
        Instantiate(exit, new Vector3(cols - 1, rows - 1, 0f),
            Quaternion.identity);
    }
}

[System.Serializable]

public struct Count

{

      public int min, max;

      public Count(int min, int max)

      {

            this.min = min;

            this.max = max;

      }

}

원래는 public으로 변수를 선언하면 인스펙터창에서 값을 넣는 칸이 생기는데(아웃렛)

아래와 같이 작성을 하게 되면 내가 정의한걸로 사용하기 때문에 밖에서 볼 수 없게 된다

= 정규화

> 구조체 정의로 최소값, 최대값 두개를 하나의 개념으로 묶기 위해 사용

 

private Transform boarHolder;   //바닥 타일을 정리해서 넣어놓으려고 Transform의 매개변수로 생성

 > 하이어아키 정리용 빈 게임오브젝트(게임오브젝트를 Transform 매개변수로 사용하다니..쬠 신기했다)

 

private List<Vector3> gridPositions = new List<Vector3>();

> 배열(Array)가 아닌 리스트(List)를 사용한 이유 : 리스트는 삽입과 삭제가 쉽기때문

> 로그라이크 특성상 맵을 클리어하면 기존에 있던 오브젝트를 삭제하고 기존 맵과 달리 랜덤으로 위치를 변경하거나 해야하기 때문에 기존맵과 중복되지 않도록 하려고 기존의 정보를 저장해놓기 위함!

 

private void InitializedList()

{

      //리스트 비우기

      gridPositions.Clear();

 

      //실제 플레이되는 맵의 크기를 gridPositions에 저장.

      for(int x = 1; x < cols-1; x++)

      {

            for(int y = 1; y < rows-1; y++)

            {

                  gridPositions.Add(new Vecotr3(x, y, 0f);      

            }

      }

}

 

private void BoardSetup()

{

      //하이어아키에 "Board"라는 빈 게임오브젝트를 생성

      boardHolder = new GameObject("Board").transform;

 

      for(int x = -1; x < cols+1; x++)

      {

            for(int y = -1; y < rows+1; y++)

            {

                  GameObeject a;

                  

                  //바깥쪽 라인이라면 바깥벽 생성

                  if(x == -1 || x == cols

                        y == -1 || y == rows)

                  {

                        a = outwallTiles[Random.Range(0, outwallTiles.Length)];

                  }

                  else //그게 아니라면 바닥 생성

                  {

                        a = floorTiles[Random.Range(0, floorTiles.Length)];

                  }      

                  GameObject go = Instanciate(a,

                  new Vector3(x, y, 0f), Quaternion.identity)

 

                  //부모가 자식을 정하는 경우는 없음. 부모를 정해주는 것

                  //만들어진 프리팹을 boardHolder의 자식오브젝트로 넣음(= 하이어아키창 정리)

                  go.transform.SetParent(boardHolder);

            }

      }

}

 

//Vector3를 return함

//타일 깐데에다가 또 안깔기 위해서! = 중복방지

private Vector3 RandomPosition()

{

      //초기화 해 준 리스트에서 하나를 고름

      //랜덤 순번 정하기

      int randomIndex = Random.Range(0, gridPositions.Count)

 

      //해당 순번에 있는 좌표 가져오기

      Vector3 randomPosition = gridPositions[randomIndex];

 

      //해당 좌표를 리스트에서 삭제

      //Remove : 값을 확인하고 일치한다면, 그 값을 지우는 것

      //RemoveAt : 순번을 확인하고 그 자리에 있는 값을 지움

      gridPositions.RemoveAt(randomIndex);

 

      //해당 좌표값 리턴

      return randomPosition;

}

 

//랜덤한 위치에 최소값 ~ 최대값 중 랜덤한 갯수의 타일을 배열에서 랜덤하게 골라서 생성

private void LayoutObjectAtRandom(GameObject[] tileArray, int min, int max)

{

      //몇개 생성할지

      int objectCount = Random.Range(min, max+1);

 

      for(int i = 0; i < objectCount; i++)

      {

            //randomPosition에 아까 만들었던 랜덤포지션 메소드 사용

            Vector3 randomPosition = RandomPosition();

            //타일까는 위치

            GameObject tileChoice =

            tileArray[Random.Range(0, tileArray.Length)];

            Instantiate(tileChoice, randomPosition, Quaternion.identity)

      }

}

 

public void SetupScene(int level)

{

      BoardSetup();

      InitializedList(); //리스트 초기화

      LayoutObjectAtRandom(wallTiles, wallCount.min, wallCount.max); //벽 타일 생성

      LayoutObjectAtRandom(foodTiles, foodCount.min, foodCount.max); //음식 타일 생성

 

      //적의 숫자를 구하는데 로그를 씀. (int)로 강제변환

      int enemyCount = (int)Mathf.Log(level, 2f);

      LayoutObejctAtRandom(enemyTiles, enemyCount, enemyCount);

      

      //출구는 랜덤한 위치가 아닌 우측 상단 모서리에 생성할거

      Instantiate(exit, new Vector3(cols-1, rows-1, 0f), Quaternion.identity); // 출구타일 생성

}

 

#GameManager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager gm = null;
    
    private BoardManager boardManager;
    private int level = 4;

    private void Awake()
    {
        if(gm == null)
        {
            gm = this;
        }
        else if(gm != null)
        {
            Destroy(gameObject);
        }

        DontDestroyOnLoad(gameObject);
        boardManager = GetComponent<BoardManager>();


    }

    private void InitGame()
    {
        boardManager.SetupScene(level);
    }
}

//새 씬을 불러올 때, 파괴가 안되도록 막아주는것 = 씬 변경시에도 데이터 유지

DontDestoryOnLoad(GameObject gameObject);

 

#Loader

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Loader : MonoBehaviour
{
    public GameObject gameManager;


    private void Awake()
    {
        if(GameManager.gm == null)
        {
            Instantiate(gameManager);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

//게임을 시작했을 때 gm(게임매니저 인스턴스)이 없다면 == 새 씬을 불러왔을 때 gm이 없다면 gameManager(GameManager 오브젝트)을 생성

if(GameManager.gm == null)

{

      //GameManager를 생성

      Instantiate(gameManager);

}

 

스크립트 작성 후에 Loader와 BoardManager를 MainCamera에 추가해준다.

 

정상적으로 코드를 작성했다면 위와 같이 게임을 시작할 때 마다 맵 구조, 몬스터의 위치 등 랜덤하게 배치되는걸 볼 수 있다.