레이블이 Unity인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Unity인 게시물을 표시합니다. 모든 게시물 표시

2024년 4월 12일 금요일

Unity 안드로이드 환경에서의 (adb 없이)효과적인 디버깅 방법

 

가끔씩 PC 환경과는 다르게 안드로이드에서 애플리케이션이 제대로 동작하지 않는 경우가 있습니다. 이때는 로그를 보면서 디버깅을 진행해야 합니다.

아래는 adb가 없을때 삼성 휴대폰에서 정리한 안드로이드 환경에서 Unity 애플리케이션을 디버깅하는 방법입니다.


  1. 문제가 의심되는 위치에 다음과 같은 형태로 로그를 추가합니다.

    Debug.Log("bgmEffect null");
    
  2. 휴대폰용으로 APK 빌드를 하여 문제를 재현합니다.

  3. ADB 프로그램이 있다면, 아래의 과정을 수행하지 않아도 됩니다. 그렇지 않은 경우에는 다음과 같은 절차를 따릅니다.

    *#9900#  화면 (두번째 메뉴 Run dumpstate/logcat, 다섯번째 Copy to sdcard 실행)

    • 휴대폰의 *#9900# 덤프 획득 모드로 진입
    • Run dumpstate/logcat 실행
    • Copy to sdcard 메뉴 실행
  4. 휴대폰을 PC와 연결한 후 log 폴더를 찾아서 dumpstate_모델명_날짜.log 파일을 PC로 복사합니다.

  5. 편집기 프로그램을 열고 "Unity :"로 검색합니다.

  6. 이렇게 로그를 확인하면 call stack이 나옵니다.

  7. 04-12 21:57:16.992 10610 31520 31564 I Unity   : bgmEffect null
    04-12 21:57:16.992 10610 31520 31564 I Unity   : GameManager:FadeOutToScene(String)
    04-12 21:57:16.992 10610 31520 31564 I Unity   : UnityEngine.Events.UnityEvent:Invoke()
    04-12 21:57:16.992 10610 31520 31564 I Unity   : UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
    04-12 21:57:16.992 10610 31520 31564 I Unity   : UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)
    04-12 21:57:16.992 10610 31520 31564 I Unity   : UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()
    04-12 21:57:16.992 10610 31520 31564 I Unity   : UnityEngine.EventSystems.StandaloneInputModule:Process()
    


  8. 로그를 활용하여 필요한 경우 더 많은 곳에 로그를 추가하고 위의 절차를 반복하여 문제를 해결합니다.


이와 같은 방법을 통해 안드로이드 환경에서 Unity 애플리케이션의 문제를 신속하게 해결할 수 있습니다.





2024년 4월 10일 수요일

Unity에서 데이터 저장 최적화하기: 중복 저장 방지


Unity에서 데이터 저장 최적화하기: 중복 저장 방지

Unity 게임 개발 중, 플레이어의 진행 상황, 설정, 또는 게임 데이터를 저장하는 것은 필수적입니다. 하지만, 불필요하게 같은 데이터를 반복해서 저장하는 것은 효율적이지 못합니다. 이 글에서는 저장 전에 기존에 저장된 데이터와 내용이 같은지 비교하여, 같다면 저장하지 않는 방법을 소개합니다.


저장 데이터 비교 로직

먼저, 저장할 데이터와 기존에 로드된 데이터를 비교하는 함수 CompSaveData를 사용합니다. 이 함수는 SaveDataStruct 타입의 두 데이터 객체를 매개변수로 받아, 내부적으로 BinaryFormatter를 사용하여 객체를 바이트 배열로 직렬화한 후, 두 바이트 배열이 서로 같은지를 비교합니다.

csharp

private static bool CompSaveData(SaveDataSturct data1, SaveDataSturct data2)
{
    BinaryFormatter formatter1 = new BinaryFormatter();
    BinaryFormatter formatter2 = new BinaryFormatter();
    byte[] bytes1;
    byte[] bytes2;
    using (MemoryStream m = new MemoryStream())
    {
        formatter1.Serialize(m, data1);
        bytes1 = m.ToArray();
    }
    using (MemoryStream m = new MemoryStream())
    {
        formatter2.Serialize(m, data2);
        bytes2 = m.ToArray();
    }
    return bytes1.SequenceEqual(bytes2);
}


데이터 저장 최적화

저장할 데이터가 기존에 저장된 데이터와 다를 때만 데이터를 저장하는 로직을 구현합니다. 이를 위해, 먼저 기존의 데이터를 로드하고, 로드된 데이터가 없거나 (loaddata==null) 로드된 데이터가 있지만 현재 데이터와 다른 경우 (!CompSaveData(tempData, loaddata))에만 새로운 데이터를 저장합니다.

csharp

var loaddata = LoadData();
if ((loaddata==null) || (loaddata != null && !CompSaveData(tempData, loaddata)))
{
    WriteSaveDataToDisk(tempData);
    saveData = tempData;
}


데이터 저장 함수

데이터를 저장하기 위한 WriteSaveDataToDisk 함수는 BinaryFormatter를 사용하여 데이터를 직렬화하고, 지정된 경로에 파일로 저장합니다. 이 과정에서 데이터가 실제로 디스크에 기록되는 경로를 로그로 남겨, 디버깅에 도움을 줍니다.

csharp

private static void WriteSaveDataToDisk(SaveDataSturct data)
{
    BinaryFormatter formatter = new BinaryFormatter();
    string path = Application.persistentDataPath + "/game.savegame";
    FileStream stream = new FileStream(path, FileMode.Create);
    formatter.Serialize(stream, data);
    stream.Close();
    Debug.Log("Data saving path : " + path);
}


 




2024년 4월 7일 일요일

Unity 프로젝트 의존성 관리와 리소스 복사


Unity 프로젝트 의존성 관리와 리소스 복사


Unity 프로젝트를 진행하면서 외부 프로젝트를 가져오거나 Package Manager를 통해 다양한 리소스를 사용하게 되는 경우, 의존성(dependency) 관리에 어려움을 겪을 수 있습니다. 이를 해결하기 위한 몇 가지 방법을 소개합니다.


Select Dependencies 사용하기

  • 기능 설명: 프로젝트 내에서 특정 폴더를 선택한 후, 오른쪽 마우스 클릭 메뉴에서 'Select Dependencies'를 선택하면, 해당 폴더가 의존하고 있는 모든 파일들의 목록을 볼 수 있습니다. 이를 통해 어떤 파일들이 실제로 사용되고 있는지 파악할 수 있습니다.
  • 장점: 사용 중인 의존성 파일들을 한눈에 확인할 수 있으며, 파일을 클릭하면 상세 내용을 볼 수 있습니다.

Export Package 사용하기

  • 기능 설명: 'Export Package' 메뉴를 사용하면, 필요한 리소스만을 선택하여 Tree 형태로 확인할 수 있습니다. 이를 통해 불필요한 파일들을 쉽게 식별하고 삭제할 수 있습니다. 
  • 장점: 폴더 단위로 불필요한 파일들을 편리하게 삭제할 수 있으며, 프로젝트의 의존성을 보다 명확하게 관리할 수 있습니다.

Unity 프로젝트를 관리하면서 의존성 문제를 해결하는 것은 프로젝트의 효율성과 유지보수성을 높이는 데 중요합니다. 'Select Dependencies'와 'Export Package' 기능을 적절히 활용하여, 프로젝트의 의존성을 체계적으로 관리해 보세요. 🙂





2024년 3월 21일 목요일

Unity Text scroll credits 만들기

 Unity로 Text가 위로 스크롤 되는 credits 입니다.

완성된 영상 입니다.


동작 원리

텍스트 스크롤 영역을 정하고 그곳에 텍스트를 주기적으로 갱신합니다.
스크롤은 Update에서 적절히 진행하며 특정 크기(텍스트 높이)만큼 스크롤이 되면 한줄을 추가하고 스크롤된 만큼 다시 스크롤 영역을 빼서 내려줍니다. 
모든 텍스트가 스크롤 되어 올라갈때까지 이 과정을 반복합니다.

소스 코드

https://drive.google.com/file/d/1XnTsYZyoe1uWSm1CW4SVWuKI_TBQg3KV/view?usp=sharing


조정 인자


MaxLinesOnScreen

화면에 Text가 몇 줄이 나타나는지 나타내는 숫자

Text 폰트나 화면 크기에 따라 화면에 표시되는 글자의 수가 다르므로 적절히 변경해야합니다.

줄 수를 알 수 있도록 Credits_Text 에 숫자를 넣어 놓았습니다. 여기에서는 25줄입니다만 더 작을 수도 늘어날 수도 있습니다.

CreditsFile

text 형태로 된 파일

LineHeight

텍스트의 높이

ScrollSpeed

스크롤 속도



2024년 3월 16일 토요일

OnScreen Controls not working correctly in Unity

 

Unity On Screen 사용시 컨트롤이 되지 않는 문제

증상은 

Player Input에 Auto-Switch off 상태에서 Debug 모드상 Keyboard&Mouse 가 고정되어 On screen game pad를 사용할 수 없는 증상


댓글에 유사 증상 언급이 있음

On Screen Controls not working correctly. - Unity Forum


해결 방법

Runtime에서 Player Input 컴포넌트를 disable/enable 했을때 정상적으로 동작되는지 확인 필요


정상 동작된다면 Player Input 컴포넌트를 off된 상태에서 시작해서 Start에서 enable 해줍니다.

다음과 같은 형태로 start 시 enable 시켰음
void Start()
{
PlayerInput playerInput = GetComponent<PlayerInput>();
playerInput.enabled = true;
}





2024년 3월 10일 일요일

Vector2 Serialize 할때 오류 발생하는 경우

Unity에서 Vector2 Serialize 를 사용하게 되면 아래와 같은 오류가 발생합니다.

SerializationException: Type 'UnityEngine.Vector2' in Assembly 'UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

아래와같은 Vector2S 와 같은 Class로 변경해주면 사용이 가능합니다.

Vector2 로 된 변수를 Vector2S 에 넣는 것이 가능하기 때문에 Serialize 하는 변수를 Vector2S 변경하고 Vector2 값을 Vector2S 로 복사 하게끔 구현해주면 됩니다.


[Serializable]
public struct Vector2S
{
public float x;
public float y;
public Vector2S(float x, float y)
{
this.x = x;
this.y = y;
}
public override bool Equals(object obj)
{
if (!(obj is Vector2S))
{
return false;
}
var s = (Vector2S)obj;
return x == s.x &&
y == s.y;
}
public Vector2 ToVector2()
{
return new Vector2(x, y);
}
public static bool operator ==(Vector2S a, Vector2S b)
{
return (a.x == b.x) && (a.y == b.y);
}
public static bool operator !=(Vector2S a, Vector2S b)
{
return (a.x != b.x) || (a.y != b.y);
}
public static implicit operator Vector2(Vector2S x)
{
return new Vector2(x.x, x.y);
}
public static implicit operator Vector2S(Vector2 x)
{
return new Vector2S(x.x, x.y);
}
}


2024년 2월 18일 일요일

unity c# callback 함수

callback 함수

 콜백 함수(callback function)란 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말합니다. 콜백 함수는 특정 이벤트가 발생하거나 특정 조건이 발생할때 실행될 수 있습니다. 


사용처

버튼을 클릭하면 콜백 함수가 호출되어 원하는 기능을 수행할 수 있습니다. 콜백 함수는 프로그래밍에서 비동기적인 작업을 처리하거나 코드의 재사용성을 높이는 데 유용합니다.


yes/no 를 선택하는 다이얼 로그 예제

다이얼 로그를 띄워주는 class를 만들었습니다. 그런데 해당 class가 닫힐때 특별한 처리를 원하는 경우가 있습니다. 즉 사용자가 yes, no를 선택했는가에 따라 다른 처리를 하고 싶은 경우 입니다. yes no dialog를 여러가지 용도로 사용을 하고자 한다면 그때마다 yes no를 선택할때 다른 코드를 넣어 줘야 합니다.

그런데 그때 호출되어야 하는 함수를 callback 함수 형태로 제작해서 dialog를 생성할때 인자를 넘겨주게 되면 코드 재사용성을 높일 수 있습니다.


unity c# 에서 사용방법

c 언어에서는 보통 함수 포인터라는 것이 있습니다. 함수의 포인터를 함수의 인자로 넘겨서 해당 함수를 호출하는 방식입니다. 그러나 c# 의 경우 포인터라는 개념이 없기 때문에 다른 방법을 이용하게 됩니다. interface라는 방법입니다. 

아래와 같은 형태로 만듭니다. OnYesNo 이름은 나중에 호출을 위한 함수가 됩니다.

public interface DialogYesNoCB
{
    public void OnYesNo(int yesNo);
}

그리고 아래와 같이 다이얼로그가 닫힐때 CloseDialog 함수가 호출되도록 연결해줍니다.  

init 에서는 cb 인자로 interface 함수를 연결합니다. 그리고 실제는 CloseDialog() 함수는 다이얼로가 닫힐때 호출되도록 작업은 따로 해주어야 합니다.

public class DialogYesNo : MonoBehaviour
{
    private DialogYesNoCB cb;
    public void Init(string text, DialogYesNoCB cb)
    {
        GetComponentsInChildren<TMP_Text>(true)[0].enabled = true;
        GetComponentsInChildren<TMP_Text>(true)[0].text = text;
        GetComponentsInChildren<TMP_Text>(true)[1].enabled = false;
        GetComponentsInChildren<Image>(true)[2].enabled = false;
        this.cb = cb;
    }
    public void CloseDialog(int yesNo)
    {
        cb.OnYesNo(yesNo);
    }
}


실제 호출은 아래와 같은 형태로 불리게 됩니다.

아래 코드는 예제 코드로 참고만 하시기 바랍니다. 작업중인 코드에서 가져왔기 때문에 실행 가능한 코드는 아닙니다.

ButtonLevelUp() 함수를 호출하면 YesNo dialog가 뜨고 Yes no 버튼을 누르면 만들어 놓은 OnYesNo 함수가 호출 되도록 되어 있습니다.

public class FieldLevelUp : MonoBehaviour, DialogYesNoCB
{
    public void ButtonLevelUp()
    {
        ShowDialogYesNo("필드를 레벨업 하시겠습니까?", this);
    }
    public void OnYesNo(int yesNo)
    {
        if(yesNo == 1) {
            GameManager.instance.quest.fieldClearCountAfterLUP[(int)fieldName] = 0;
            GameManager.instance.quest.fieldLevel[(int)fieldName]++;
            LeftListUpdate();
        }
    }
    ...
    public void ShowDialogYesNo(string text, DialogYesNoCB cb)
    {

        yesNoDialogPanel.GetComponent<DialogYesNo>().Init(text, cb);
        yesNoDialogPanel.GetComponent<PanelHandler>().Show();
    }
    public void CloseDialogYesNo(int yesNo)
    {
        yesNoDialogPanel.GetComponent<DialogYesNo>().CloseDialog(yesNo);
        yesNoDialogPanel.GetComponent<PanelHandler>().Hide();
    }
}

CloseDialogYesNo는 object yes, no 버튼의 button on click() 함수에 넣도록 합니다.




2024년 1월 24일 수요일

GameObject (creation order) 생성 순서에 대한 고찰

#unity change order GameObject

 Unity 를 사용하면서 Awake나 Start 함수에서 다른 GameObject 상태를 살피는 경우가 종종 있습니다.

그런데 간혹 잘되던 상태가 종종 오동작하는 경우가 있어서 Object 생성 순서와 관계가 있을것 같아서 조사해 보았습니다.

구글링을 했을때는 마땅한 결론이 없어서 이것 저것 테스트 해보았습니다.

Hierarchy 에서 게임 Object를 순서대로 만들었습니다. 이름은 혹시 몰라 A,B,C,1,2,3,c,b,a 순서로 지었습니다.

그리고 Test Code에서는 Awake 진입하면 Object 이름이 출력되도록 로그를 넣었습니다.


실제 테스트를 하게되면 아래와 같은 형태로 나옵니다.

순서는 오래전 생성된것이 처음에 나오고 마지막에 추가된 Obejct가 마지막에 나옵니다.

Hierarchy 에서 순서를 마구 섞어 보았습니다.


순서는 바뀌지 않았습니다.


혹시 몰라서 Alphanumeric Sorting 기능도 건들어봤습니다.


전혀 변화가 없습니다.


지금부터는 순서를 변화 하는 방법입니다.

동일한 기능을 하는 새 스크립트를 만들어서 추가했습니다.

수정된 Object가 가장 처음으로 올라오는 변화가 일어납니다.

그렇다면 여기에서 다시 예전 스크립트로 돌린다면 순서가 예전으로 돌아가는지 확인해 보았습니다.


한번 변화된 순번은 원복되지 않습니다.

이번에는 script component 를 삭제했다가 다시 추가해 보았습니다.


처음으로 올라갑니다.



결론 : 변화가 있는 GameObject가 제일 마지막에 생성됩니다. (여기에서는 컴포넌트 변화를 주었습니다.)
순서를 변경하려면 최악의 경우 위와 같이 해야하지만, 실제는 Object 생성 순서에 영향이 없도록 Awake와 Start를 조합해서 잘 설계하는 것을 추천합니다.



2024년 1월 12일 금요일

DOTween Time.timeScale = 0 인 경우 동작하는 방법, SetUpdate(true) 추가해도 동작이 안될때

 DOTween 에서 Time.timeScale = 0 일때 animation 을 사용 하는 방법

동작 뒤에 SetUpdate(true추가해 주면 됩니다.

그런데 추가해도 안되는 경우가 있었습니다.

아래와 같은 예제가 있습니다.

            var seq = DOTween.Sequence();
            
            transform.localScale = Vector3.one * 0.1f;

            seq.Append(transform.DOScale(1.1f, fadeTime).SetUpdate(true));
            seq.Append(transform.DOScale(1f, 0.1f).SetUpdate(true));

            seq.Play().OnComplete(() =>
            {
                transform.localScale = Vector3.one;
            });

어디가 잘못 되었는지 찾았나요?



수정한 코드입니다.

        var seq = DOTween.Sequence();
            
        transform.localScale = Vector3.one * 0.1f;

        seq.Append(transform.DOScale(1.1f, fadeTime));
        seq.Append(transform.DOScale(1f, 0.1f));

        seq.Play().SetUpdate(true).OnComplete(() =>
        {
            transform.localScale = Vector3.one;
        });

sequence 를 사용하고 있다면 Play() 에 SetUpdate(true)를 넣어야 합니다.