4. 트리거 작동 시 바뀐 값과 현재 유저의 device id가 같은지 확인, 다르면 중복 로그인 팝업과 함께 게임 종료 처리
private void LogoutFunction(object sender, ValueChangedEventArgs args)
{
if (args.DatabaseError != null)
{
Debug.LogError(args.DatabaseError.Message);
return;
}
//device id와 받은 값이 다른 경우
if (SystemInfo.deviceUniqueIdentifier != (string) args.Snapshot.Value)
{
//로그아웃 처리
}
}
해당 설명을 들어가기 전에 앞서 스크립터블 오브젝트라는 것에 대한 이해를 하고 있어야 한다.
스크립터블 오브젝트는 추후에 설명하는 글을 써야겠다. 일단은 모른다면 친절한 글이 많으니까 이해하고 오도록 하자
TestData라는 오브젝트의 스크립트를 열어보면 크게 두 부분으로 나뉜다.
테스트 데이터 객체에 대한 정의
public class TestData : ScriptableObject
{
public string associatedSheet = "";
public string associatedWorksheet = "";
public List<string> items = new List<string>();
public List<string> Names = new List<string>();
//1.3 긁어온 행 데이터를 조회하는 부분
internal void UpdateStats(List<GSTU_Cell> list, string name)
{
items.Clear();
int math=0, korean=0, english=0;
for (int i = 0; i < list.Count; i++)
{
switch (list[i].columnId)
{
case "Math":
{
math = int.Parse(list[i].value);
break;
}
case "Korean":
{
korean = int.Parse(list[i].value);
break;
}
case "English":
{
english = int.Parse(list[i].value);
break;
}
}
}
Debug.Log($"{name}의 점수 수학:{math} 국어:{korean} 영어:{english}");
}
}
생성된 테스트 객체의 커스텀 에디터(스크립터블 오브젝트의 인스펙터 부분이라고 생각하면 된다.)
[CustomEditor(typeof(TestData))]
public class DataEditor : Editor
{
TestData data;
void OnEnable()
{
data = (TestData)target;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
GUILayout.Label("Read Data Examples");
if (GUILayout.Button("Pull Data Method One"))
{
UpdateStats(UpdateMethodOne);
}
}
//1.1 GSTU_Search 객체를 생성하는 부분
void UpdateStats(UnityAction<GstuSpreadSheet> callback, bool mergedCells = false)
{
SpreadsheetManager.Read(new GSTU_Search(data.associatedSheet, data.associatedWorksheet), callback, mergedCells);
}
void UpdateMethodOne(GstuSpreadSheet ss)
{
//1.2 행 데이터를 긁어오기
foreach (string dataName in data.Names)
data.UpdateStats(ss.rows[dataName], dataName);
EditorUtility.SetDirty(target);
}
}
DataEditor부분에서 생성된 Pull Data Method One 버튼을 클릭하면 TestData의 UpdateMethodOne을 호출한다고 생각하면된다.(정확히는 TestDataEditor의 UpdateStats 함수를 호출하고 결과 콜백을 TestData의UpdateMethodOne으로 넘겨준다가 맞다. 해당 강좌의 주제는 엑셀이라서 이런 건 몰라도 크게 상관없다.)
data.Names에는 Jim, Jay, Jack이라는 행 데이터의 키 역할을 하는 값들이 들어있어서 ss.rows[dataName] 을 통해 행중에서 Jim, Jay, Jack 즉 3 개행의 데이터를 뽑아낸 것이다.
1.3 행 단위로 쪼갠 데이터를 열 단위로 다시 쪼개기
뽑아낸 행 데이터를 다시 열 단위로 쪼개서 조회해야 한다.
열 단위 데이터 조회하는 부분은 총 세 가지의 샘플 코드가 있다.
세 가지 형태의 샘플 코드를 한번 봐보자.
//1번 방식
internal void UpdateStats(List<GSTU_Cell> list)
{
items.Clear();
for (int i = 0; i < list.Count; i++)
{
switch (list[i].columnId)
{
case "Health":
{
health = int.Parse(list[i].value);
break;
}
case "Attack":
{
attack = int.Parse(list[i].value);
break;
}
case "Defence":
{
defence = int.Parse(list[i].value);
break;
}
case "Items":
{
items.Add(list[i].value.ToString());
break;
}
}
}
}
//2번 방식
internal void UpdateStats(GstuSpreadSheet ss)
{
items.Clear();
health = int.Parse(ss[name, "Health"].value);
attack = int.Parse(ss[name, "Attack"].value);
defence = int.Parse(ss[name, "Defence"].value);
items.Add(ss[name, "Items"].value.ToString());
}
//3번 방식
internal void UpdateStats(GstuSpreadSheet ss, bool mergedCells)
{
items.Clear();
health = int.Parse(ss[name, "Health"].value);
attack = int.Parse(ss[name, "Attack"].value);
defence = int.Parse(ss[name, "Defence"].value);
//I know that my items column may contain multiple values so we run a for loop to ensure they are all added
foreach (var value in ss[name, "Items", true])
{
items.Add(value.value.ToString());
}
}
방법은 다르지만 결국은 똑같은 시트의 Health, Attack, Defence, Items를 긁어오는 것이다.
그냥 이중에 하나만 잘 써도 충분하다. 나는 사실 첫 번째 방법만 계속 써서 다른 두 가지 방법이 있는지 몰랐을 정도로 잘 썼다.
internal void UpdateStats(List<GSTU_Cell> list, string name)
{
items.Clear();
int math=0, korean=0, english=0;
for (int i = 0; i < list.Count; i++)
{
switch (list[i].columnId)
{
case "Math":
{
math = int.Parse(list[i].value);
break;
}
case "Korean":
{
korean = int.Parse(list[i].value);
break;
}
case "English":
{
english = int.Parse(list[i].value);
break;
}
}
}
Debug.Log($"{name}의 점수 수학:{math} 국어:{korean} 영어:{english}");
}
위의 코드는 첫 번째 방법을 조금 변형한 것이다.
행 단위 데이터를 이루고 있는 열 단위의 데이터 중에서 switch문을 통하여 칼럼 값이 내가 필요한 Math, Korean, English 부분만을 긁어오는 것이다.
2. 데이터 매칭을 위한 키
이쯤에서 데이터 매칭을 위한 키가 필요하다는 부분을 한번 짚고 넘어가도록 하겠다.
당신이 엑셀 아래와 같은 데이터 시트가 있다고 하자.
체력
공격력
방어력
원숭이
99
9
9
군인
99
10
10
고릴라
9999
9999
9999
위 시트의 데이터를 긁어오는 것은 어렵지 않다 충분히 구현할 수 있다.
문제는 캐릭터를 생성할 때마다 시트를 긁어와서 넣어주면 너무 비효율적이기 때문에 한번 긁어와 놓고 어딘가에 저 데이터를 저장해놓고 사용해야 한다는 점이다.
어떻게 할 것인가? 한번 고민해보도록 하자.
우리 강의는 시청자가 고민할 시간을 제공한다.
바로 스크롤을 슉슉 내렸는가 사실 나라도 그랬을 것 같다.
여하튼 나의 경우에는 스크립터블 오브젝트를 만들어놓고 해당 스크립터블 오브젝트에 엑셀에서 긁어온 값들을 넣어놓고 사용한다. 그리고 그 스크립터블 오브젝트를 전부 다 들고 있고 관리하는 로컬 데이터베이스 클래스가 있다.
요 구조에 대해서는 나중에 또 실습을 해볼 것이다.
그전에 지금 얘기하고 있는 중요한 부분 데이터 매칭을 위한 키를 정해야 한다는 점에 대해서 얘기하겠다.
엑셀에서 데이터를 긁어왔는데 긁어온 데이터는 스스로 목적지를 찾아서 들어가 주지 않는다.
즉 유니티 내부에서 저장해놓을 곳과 엑셀 데이터의 매칭을 위한 약속을 해야 한다는 것이다.
이 부분을 깔끔하게 정하지 않으면 더러운 하드코딩으로 악순환이 돼버릴 수 있다.
기본적으로는 절대로 변하지 않는 데이터에 대한 키 값을 정해야 한다.(또한 키가 키 외에 다른 역할을 하지 않도록 하자)
아래 표를 보면 원숭이, 군인, 고릴라는 키 역할을 하면서 동시에 게임 내부에서 오브젝트 명까지 맡고 있다. 충분히 바뀌지 않을 수 있지만 오브젝트 명은 언제든지 바뀔 수 있다. 그렇기 때문에 분리를 해야 한다.
체력
공격력
방어력
원숭이
99
9
9
군인
99
10
10
고릴라
9999
9999
9999
↓
이름
체력
공격력
방어력
char_monkey
숭숭이
99
9
9
char_solider
군인
99
10
10
char_gorilla
고륄라
9999
9999
9999
이름
체력
공격력
방어력
001
숭숭이
99
9
9
002
군인
99
10
10
003
고륄라
9999
9999
9999
이런식으로 분리를 하자는 것이다. 기왕이면 키만 봐도 어떤 데이터인지 알 수 있는 char_monkey 형태가 좋은 것 같다. 나는 001 형식으로 인덱스 값을 부여해서 사용하는데 문제는 딱히 없다.
이미 구글에 관련 게시물들이 올라와있는데 왜 또 쓰는 거냐?(이 게시물은 다른 게시물들과 뭐가 다른 거냐?)
나는 원래 이미 다뤄진 이슈는 잘 안 다루지만 구글 스프레드 시트 연동하는 포스팅을 봤는데 대부분이 정말 초기 세팅 부분만 정리해놓고 그 이후 활용에 대한 내용이 없었다. 그 부분을 널리 알리고자 쓰게 되었다.
어떤 경우에 쓰는가?
게임을 혼자서 만들면 굳이 구글 엑셀 시트까지 연동할 필요 없이 넣어주면 된다. 협업 환경에서 기획자가 밸런스나 수치 데이터를 조절할 때마다 내가 할 수는 없으니까, 이를 쉽게 조절할 수 있도록 만드는 경우에 사용한다.
어디까지 구글 시트로 연동해야 하는가?
행여나 구글 시트로 연동한다고 했을 때 게임의 수치적인 모든 것을 스프레드 시트로 관리하는 것이 꼭 능사가 아닐 수도 있다.
선별적으로 생각해서 정말로 시트로 관리할 가치가 있는 부분만 시트로 관리하도록 하자. 별로 수정할 일도 없는 부분을 구글 시트로 빼느라 고생하지 말자는 뜻이다.
참고사항
참고로 이번 주제에서는 데이터 읽기만 할 것이다. 쓰기는 안한다.
주변의 조언을 종합해서 짧게 말하자면 엑셀 데이터를 긁어오는 것은 개발단계, 관리수준에서 데이터를 쉽게 관리하기 위함이지 게임이 출시되고 나서 인게임에서 엑셀로 데이터를 긁어오는 상황을 위한 것이 아니다.(보안 취약) 따라서 엑셀 시트와 관련된 부분을 전처리하는것이 좋겠다.
구글에서 unity ragdoll strech 라고 검색해보면 대부분관절에 붙어있는 Enable proejction을 켜주라고 말한다.
Enable projection을 하면 무슨 원리로 안정적으로 되는 건지 찾아보았으나 정확한 정보는 얻지 못했고 디컴파일러로 열어보니 해당 불린 변수에 대한 주석으로 위와 같이 나와있었다. 아마 정상적이지 못한 래그돌의 흔들림처리를 이전 상태로 다시 되돌려주는 것이 아닐까 추측해본다.
2. 래그돌 재생성
나의 경우 래그돌이 두 개의 문제가 있었다. 하나는 래그돌의 양팔이 벌려지고나서부터는 접혀지지가 않는 문제였고 다른 문제는 신체의 일부가 가만히 있고 연결부위가 엿처럼 쭈욱 늘어나는 문제였는데 둘다 래그돌 생성 시 올바른 오브젝트를 매칭 시키지 않아서 발생한 문제였다.
첫 번째 방법은 래그돌의 rigidbody를 kinematic으로 바꾸고 현재 가해져 있는 힘을 전부 없애버리는 것이다.
두 번째 방법은 래그돌을 꺼버리는 방법이다.
첫 번째 방법은 자연스러운 멈춤이고 두 번째 방법은 인위적인 멈춤이라고 생각한다.
왜 이렇게 생각이 드는지는 자세한 부분을 듣다 보면 알게 될 것이다.
또한 둘 다 장단점이 있는데 자세한 부분은 설명하면서 기재하도록 하겠다.
목차
1. 첫 번째 방법(자연스러운 멈춤)
2. 두 번째 방법(인위적인 멈춤)
3. 각 멈춤에 대한 설명
1. 첫 번째 방법(자연스러운 멈춤)
먼저 구현해보고자 하는 멈춤 방식은 다음과 같다.
public Rigidbody body;
public void Freeze()
{
body.isKinematic = true;
body.velocity = Vector3.zero;
body.angularVelocity = Vector3.zero;
}
public void UnFreeze()
{
body.isKinematic = false;
}
Freeze 함수를 호출하면 동영상처럼 멈추게 되는 것을 알 수 있다.
추가로 알아야 하는 점이라면 물체가 가진 힘을 전부 없애고 싶은 경우(동영상의 세 번째 테스트처럼 래그돌이 날아가다가 모든 힘을 잃고 현재 자리에서 멈추도록 하는 것) Freeze함수를 호출하고 바로 UnFreeze함수를 호출하면 아무 일도 일어나지 않는다.
곰곰이 생각해봤는데 Freeze함수를 호출해서 힘을 전부 다 잃고 나서 프레임이 지나서 해당 물리가 적용되기도 전에 UnFreeze로 다시 원상복구가 돼서 그런 것이 아닐까 생각이 든다.
만약 해당 사항을 스크립트로 적용하고 싶다면 아래처럼 하면 된다.
Freeze();
Invoke("UnFreeze",0.1f);
Freeze에 대한 물리가 적용되고 나서(아마 한 프레임밖에 소요되지 않을 것이라고 생각한다.(UnFreeze에 대한 내용을 FixedUpdate에다가 넣어줘도 되지 않을까 싶다.)
2. 두 번째 방법(인위적인 멈춤)
위와 같은 멈춤은 매우 간단하다. 위의 멈춤은 뭐랄까 메쉬는 남겨두고 래그돌을 꺼준다고 생각하면 된다.
body.gameObject.SetActive(false);
3. 각 멈춤에 대한 설명
각 멈춤은 매우 차이가 크다.
자연 멈춤과 인위 멈춤이라고 명칭을 정하겠다.
자연 멈춤은 멈추고 다시 복원해도 힘을 다 잃어서 연속성이 없다. 그리고 인위 멈춤에 비해서 완전히 얼음!!! 처럼 멈춘다는 느낌은 부족하다. 아마도 모델의 모든 rigidbody를 배열로 받아서 모든 포지션을 저장하고 코루틴으로 해당 rigidbody들의 위치를 계속해서 유지하도록 하면 비슷하게나마 구현할 수 있지 않을까 생각 든다.
인위 멈춤은 멈추고 다시 복원하면 이전의 힘을 그대로 유지한다. 잠깐 래그돌을 껐다가 켜주는 거라서 물리 사항에는 변동이 없다. 굉장히 좋다고 생각할 수 있지만 아래 사진을 보면 인위 멈춤의 부작용이 하나 있다.
아래 사진은 각각의 멈춤을 계속해서 진행하고 나서 래그돌의 상태를 캡처한 것이다.
몇 번밖에 멈춤을 반복하지 않았다. 자연 멈춤은 아무 문제가 없고 인위 멈춤은 완전히 뒤틀려있음을 볼 수 있다.
인위 멈춤의 경우 충돌로 본이 꼬였을 때 멈추면 해당 상태가 그대로 저장이 된다. 마치 고무가 탄성을 잃는 것이다.
만약에 래그돌을 한두 번만 사용하는 경우라면 인위멈춤을 사용해도 괜찮지만 계속해서 사용해야 하는 경우 비정상적인 결과를 초래하기 때문에 전자의 자연 멈춤을 사용할 수밖에 없을 것이다.
인위 멈춤 방식을 사용해도 계속해서 부위가 꼬이지 않도록 하는 방법을 찾아봤으나 정보가 없었다.