로그라이크 : 저장기능이 없고, 죽으면 여태까지의 플레이 정보가 초기화된다.
랜덤성이 짙은 게임으로 플레이를 새로 할 때 마다 맵 위치, 아이템의 기능이나 위치가 달라지는 게임
로그라이트 : 로그라이크와 유사하지만 죽은 후에 게임을 다시 시작할 때 마다 죽기 전의 게임정보를 어느정도
받아와 난이도가 쉬워져서 플레이하기 더 쉬워지는 게임
패키지를 다운받은 후, 유닛들의 애니메이션과 맵, 아이템들을 프리팹으로 만드는 작업을 해준다.
평소와는 다르게 애니메이션을 만들어 줄 건데, 우선 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에 추가해준다.
정상적으로 코드를 작성했다면 위와 같이 게임을 시작할 때 마다 맵 구조, 몬스터의 위치 등 랜덤하게 배치되는걸 볼 수 있다.
'수업 복습' 카테고리의 다른 글
Zombie - 1인칭시점, 크로스헤어 - (0) | 2023.09.10 |
---|---|
Zombie - MultiPlay - (0) | 2023.08.30 |
Zombie - 좀비(Enemy) 스크립트, 게임오버 UI - (0) | 2023.08.29 |
Zombie 복습2 - 플레이어 스크립트 - (2) | 2023.08.27 |
Zombie - 좀비, 플레이어 UI - (0) | 2023.08.22 |