오늘 중간에 컴퓨터가 꺼졌다. 작업은 무사한데 메모장이 날아갔다. 메모장 프로그램 하나 구해야겠다. 시간 순서에 따라 오늘 배운 것들을 정리한다.
코드스니펫 부분부터 먼저 짚고 넘어가자면
void Start()
{
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
string rtanName = "rtan" + rtans[i].ToString();
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
}
}
4x4 크기로 게임이 만들어지기에 int 배열을 16칸(8쌍) 크기로 만들어주고
OrderBy Linq를 사용해서 랜덤하게 섞어준 후 for문으로 섞인 르탄이 사진을 불러와서 랜덤하게 배치하는 방식이다.
앞선 글에 2048에서 한 방식으로 먼저 바탕이 될 타일 위치를 Empty로 잡아주었고 태그를 tiles로 달아두었다.
그런 후 코드를 작성하였다.
public void InitGame()
{
Time.timeScale = 1.0f;
firstCard = secondCard = null;
int[] rtans;
> GameObject[] tiles; #1
> int[] nums = new int[8]; #2
for (int i = 0; i < 8; i++)
{
nums[i] = i;
}
nums = nums.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
tiles = new GameObject[16];
tiles = GameObject.FindGameObjectsWithTag("tilesnormal");
tiles = tiles.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
rtans = new int[16];
cardsLeft = cardsnormal.transform.childCount;
for (int i = 0; i < 16; i++)
{
rtans[i] = i / 2;
}
for (int i = 0; i < 8; i++)
{
> choose[i] = card.GetComponent<card>().sprites[nums[i]]; #3
}
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform;
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
}
#1에서 방금 만들어 둔 tiles를 불러오기 위해 빈 GameObject 배열을 만들어두고
#2는 choose라는 빈 Sprite 배열을 채우기 위한 난수배열을 위해 int 배열을 만들어주었다.
#3에서 for문을 통해 card프리팹에 있는 스프라이트를 가지고온다.
그런 후 밑의 for문을 통해 새로운 카드들을 복제해주면서 choose에 있는 스프라이트들을 rtans[]라는 짝으로 이루어진 배열을 통해 넣어준다.
내가 풀이한 방법은 CS만 이용하기보다 Unity를 더 활용하는 방법으로 풀이했는데
팀원 중 한분이 풀이한 방법도 배우고 싶어서 남겨본다.
public class GameManager : MonoBehaviour
{
public Sprite[] sprites;
private int[] RandomRtnas()
{
int[] rtans = new int[sprites.Length * 2];
for (int i = 0; i < sprites.Length * 2; i++)
rtans[i] = i / 2;
for (int i = 0; i < sprites.Length * 2; i++)
{
int rand = Random.Range(0, sprites.Length * 2);
int temp = rtans[i];
rtans[i] = rtans[rand];
rtans[rand] = temp;
}
return rtans;
}
}
이 분은 OrderBy를 안쓰고 하는 방향으로 CS에 집중해서 풀이하셨다.
기억나는대로 적어보자면
1. 스프라이트 길이의 2배로 rtans배열을 할당해준다.
2. rtans의 각 값을 for문으로 i/2해서 짝 맞춘 배열로 채운다.
3. 밑의 for문에서 temp는 잠시 저장하기 위한 변수로 바꿔질 부분의 값을 미리 저장해둔다.
4. 그 후 바뀌어질 부분을 뒤의 rtans[rand]로 바꿔넣은 후 rtans[rand]를 3에서 저장한 temp로 바꿔준다.
이렇게 했을 때 예시로 배열이 [0,1,2,3,4]고 i=1, rand=3이라한다면 바뀐 배열은 [3,1,2,0,4]가 된다.
int temp = rtans[i]; rtans[i] = rtans[rand]; rtans[rand] = temp;
이 부분은 대각선 법칙처럼 서로 자리가 바뀌는거라 자주 써서 익히면 좋다고 한다.
따로 생각나는 연출 효과는 없어서 예시로 있는 나선형으로 날아오는 효과를 구현해보았다.
먼저 entry 애니메이션을 한땀한땀 만들어주었다.
그 후 애니메이터에도 끼워준다.
디폴트로 만들어서 시작하자마자 바로 동작하게 한 후 끝나자마자 idle로 넘어간다. 따라서 코드도 따로 작성할 필요도 없다. 그 전에 작성한 코드로 바로 넘어가기 때문에.(Has Exit Time 끄면 안될거같아서 안껏다. > 꺼봤는데 idle로 안넘어가더라 이유 아시는 분 댓글좀)
여기까진 일사천리로 진행됬는데 그 다음이 문제였다. 이 entry라고 작성된 애니메이션(나선표창)은 Postion과 rotation이 둘 다 바뀌는 애니메이션이다.
근데 기존에 하던 방식(4주차 코드스니펫에 있던 방식인데 복제할때 부모오브젝트 Cards 밑으로 복제됨)으로 하니까 한점으로 뭉쳤다.
한시간정도 이리저리 해보다가 구글링 해보니까 "애니매이터의 자식은 상대좌표, 애니매이터 자체는 절대좌표"라고 되있었다.
그래서 애니매이션을 수정해서 끝나는 지점을 있어야될 지점으로 만들어 주고
아까 만들어둔 tiles를 cards의 자식 오브젝트로 넣은 후 복제될 때 각 tile의 자식 오브젝트로 넣어주니까 그제서야 제대로 동작했다.
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform; //여기가 포인트
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
애니메이션이 동작할 때 중심축을 잡아줄 수 있는 절대좌표가 어디에 있는지 생각해보면서 써먹자.
이 부분은 카드 부분을 만지니까 그냥 묶어서 하는게 좋아보여서 같이 했다.
오늘 이 두가지로 시간을 반 이상 사용했다.
먼저 짚고 넘어가자면 "카드 오브젝트 개수 = 카드 바리에이션"이다.
먼저 카드 프리팹에 Sprite[]를 19칸으로 늘려 팀원들의 다양한 사진을 저장했다.
원래는 card.cs에서 다 처리하려고 해봤으나 생각해보니 많은 카드가 깔리면서 배열이 계속 달라지는 걸 생각하지 못해서 GameManager.cs로 옮겨서 작성했다.
원래 코드
public void InitGame()
{
Time.timeScale = 1.0f;
firstCard = secondCard = null;
//배열 내용 랜덤하게 섞기. List에서도 써먹을 수 있을 거 같다.
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = cards.transform;
newCard.GetComponent<card>().Setup(rtans[i]);
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
//newCard.transform.position = new Vector3(1.4f * (i % 4), -1.4f * (i / 4), 0);
//string rtanName = "rtan" + rtans[i].ToString();
//newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite
// = Resources.Load<Sprite>(rtanName);
}
cardsLeft = cards.transform.childCount;
}
바꿔본 코드
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
GameObject card;
public Sprite[] choose;
int cardsLeft;
private void Awake()
{
Instance = this;
}
void Start()
{
InitGame();
}
public void InitGame()
{
Time.timeScale = 1.0f;
firstCard = secondCard = null;
int[] rtans;
GameObject[] tiles;
int[] nums = new int[19];
for (int i = 0; i < 19; i++)
{
nums[i] = i;
}
nums = nums.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
tiles = new GameObject[16];
tiles = GameObject.FindGameObjectsWithTag("tilesnormal");
tiles = tiles.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
rtans = new int[16];
cardsLeft = cardsnormal.transform.childCount;
for (int i = 0; i < 16; i++)
{
rtans[i] = i / 2;
}
for (int i = 0; i < 8; i++)
{
choose[i] = card.GetComponent<card>().sprites[nums[i]];
}
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform;
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
}
앞에 작성한 내용도 오늘까지 완성된 부분을 바탕으로 설명했기에 겹치는 부분도 있어서 간단하게 비포애프터로 설명하면
Before : 레퍼런스 된 스프라이트를 모두 사용하기에 따로 골라줄 필요가 없음
After : 19장의 사진 중 8장만 골라씀
그래서
for (int i = 0; i < 8; i++) { choose[i] = card.GetComponent<card>().sprites[nums[i]];
card의 스프라이트 중 nums(0~18까지 랜덤으로 나열된 배열)의 8번까지만 사용해서 19개중 8개만 뽑는 코드
그리고 원래는 card.cs에서 동작하는 코드지만 GameManager.cs로 옮겼으니 새로 복제된 newcard 오브젝트의 자식오브젝트 front를 참조해서 스프라이트를 바꿔준다.
여기서 설명 안된 부분이 있는데 어제 다른 팀원이 한 맞췄을 때 사진 주인 이름이 나오게 하는 부분도 있는데, 이 부분도 원래 Card.cs에 들어있었는데 간단하게 옮겨서 방법만 적어본다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum WhosCard
{
Seungjun,
Geon_o,
Geonhyeong,
Jiyoon,
Ingyu
}
public class card : MonoBehaviour
{
public WhosCard WhosCard { get; private set; }
public void Setup(int index)
{
switch (index)
{
case 0:
case 1:
case 2:
case 3:
WhosCard = WhosCard.Seungjun;
break;
case 4:
case 5:
case 6:
case 7:
WhosCard = WhosCard.Geon_o;
break;
case 8:
case 9:
case 10:
case 11:
WhosCard = WhosCard.Geonhyeong;
break;
case 12:
case 13:
case 14:
case 15:
WhosCard = WhosCard.Jiyoon;
break;
case 16:
case 17:
case 18:
WhosCard = WhosCard.Ingyu;
break;
}
}
}
enum으로 만들어진 whoscard라는 부분이 있고, case에 숫자가 다양하다.
저 케이스 부분에 알맞은 순서로 사진을 넣어준다. 이 switch 함수의 입력값은 int index이며
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
를 사용해서 nums에 rtans[i](짝으로 되어있음) 를 넣어서 입력해주면 Setup구문에는 해당하는 case숫자가 들어가서 제대로 동작한다.
for문의 제약이 카드숫자만큼 되어있기때문에 range에서 벗어나지도 않는다.
배열 증감을 위해 작업 세가지를 진행했다.
1. 증감에 따른 tile 작업
easy : 2x2 / normal : 4x4 / hard : 6x6
2. 난이도에 따른 동작 구분
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public int diff;
GameObject cardseasy;
GameObject cardsnormal;
GameObject cardshard;
private void Awake()
{
Instance = this;
}
void Start()
{
diff = PlayerPrefs.GetInt("difficulty");
cardseasy = GameObject.Find("cardseasy");
cardsnormal = GameObject.Find("cardsnormal");
cardshard = GameObject.Find("cardshard");
if (diff ==1)
choose = new Sprite[4];
else if (diff ==2)
choose = new Sprite[8];
else if (diff ==3)
choose = new Sprite[18];
InitGame();
}
public void InitGame()
{
switch (diff)
{
case 1:
tiles = new GameObject[4];
tiles = GameObject.FindGameObjectsWithTag("tileseasy");
tiles = tiles.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
rtans = new int[4];
cardsLeft = cardseasy.transform.childCount;
for (int i = 0; i < 4; i++)
{
rtans[i] = i / 2;
}
for (int i = 0; i < 2; i++)
{
choose[i] = card.GetComponent<card>().sprites[nums[i]];
}
for (int i = 0; i < 4; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform;
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
break;
case 2:
tiles = new GameObject[16];
tiles = GameObject.FindGameObjectsWithTag("tilesnormal");
tiles = tiles.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
rtans = new int[16];
cardsLeft = cardsnormal.transform.childCount;
for (int i = 0; i < 16; i++)
{
rtans[i] = i / 2;
}
for (int i = 0; i < 8; i++)
{
choose[i] = card.GetComponent<card>().sprites[nums[i]];
}
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform;
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
break;
case 3:
tiles = new GameObject[36];
tiles = GameObject.FindGameObjectsWithTag("tileshard");
tiles = tiles.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
rtans = new int[36];
GameObject.Find("Main Camera").GetComponent<Camera>().orthographicSize = 9;
cardsLeft = cardshard.transform.childCount;
for (int i = 0; i < 36; i++)
{
rtans[i] = i / 2;
}
for (int i = 0; i < 18; i++)
{
choose[i] = card.GetComponent<card>().sprites[nums[i]];
}
for (int i = 0; i < 36; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = tiles[i].transform;
newCard.transform.position = new Vector3(tiles[i].transform.position.x, tiles[i].transform.position.y, 0);
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = choose[rtans[i]];
newCard.GetComponent<card>().Setup(nums[rtans[i]]);
}
break;
}
}
}
switch와 if로 값을 분리해서 노가다해준게 다라서 설명할 부분은 별로 없으나, hard 난이도의 경우 6x6이 되면서 화면이 꽉 차게되어서 카메라 컴포넌트를 참조해 사이즈를 변경해준 점 참고하면 되겠다.
버튼 작업+씬 변경에 따른 데이터 전달
2번까지 하고 다됬다 생각했는데 스타트씬에서 메인씬으로 넘어올 때 데이터가 소실된다는걸 처음 알았다. 그 전까지 참조도 해보고 이래저래 불러봤는데 안되서 구글링을 해본 결과 3가지 방법이 있었다.
1) Class 새로 선언해서 Publci static으로 값 넘기기 > 팀원분이 이렇게 해서 설명해주셨는데 내꺼 한다고 바빠서 잘 못들었다.
2) DontDestoryLoad() 사용하기 > 이 방법은 사용해봤는데 오류가 떴다.
내용은 "이 함수는 root에서만 사용할 수 있다." 이게 무슨 뜻이냐면 자식오브젝트에서 사용 못한단다.
억울했다.
3) 로컬저장 활용하기
이번 작업에 사용한 방법이 3번이다. 우리가 게임을 할 때 점수가 있다고 치면 최고 점수를 기록할 때 흔히 사용하는 방법이다. 로컬데이터에 저장되기 때문에 데이터가 소실되지 않고 편하게 불러올 수 있었다. 예시로 하나만 보자면
startBtn.cs
public void gameStarteasy() { difficulty = 1; PlayerPrefs.SetInt("difficulty", difficulty); SceneManager.LoadScene("MainScene"); }
easy로 시작할 때 난이도 값을 1로 저장해둠.
GameManager.csvoid Start() { diff = PlayerPrefs.GetInt("difficulty"); }
이런 식으로 다른 씬에 있는 데이터 값을 호출할 수 있었다.
오늘은 어제랑 다르게 엄청 많이 했다. 앞에 두분이 너무 잘하시는데다가 스킬이 고급져서 뭔가 후져보이긴 하지만, 해냈다는게 뿌듯하다. 오늘 배운것도 많아서 TIL 길이도 길어졌는데 내일도 많이 기억났으면 좋겠다. 오늘도 재밌었다.
//추가
enum == 상태값 만드는데 최적화 되어있다(ex: 상태이상같은거)
쓰면 쓸수록 좋다
내일 어떻게 하면 잘 써먹을 수 있을 지 찾아볼 것.