프로젝트 일지/Unity

[Unity/TIL] 3D 퍼즐 플랫폼 게임 - 팀 프로젝트 일지 (3) | World Space UI 클릭 이벤트 발생 안함 해결, 제네릭 데이터 로더 구현, 데이터 AES 암호화해서 json으로 입출력

톰마토 2025. 3. 17. 23:47
728x90

World Space의 UI 클릭 이벤트 발생하지 않는 문제 해결

두가지 퍼즐 구현 마무리를 하고 다른 파트와 합쳐보다가 문제가 발생했다.

현상 : Player를 추가하고나면 World Space에 있는 UI에 클릭이 안된다. 

World Space에 있는 UI 캔버스의 EventCamera는 스크립트에서 동적으로 할당하기 때문에 이 문제는 아닌 것 같았다. 

찾은 원인 : 커서 Lock모드 

PlayerController에서 커서의 lockState를 Locked로 세팅해주고 있는데, 

EventSystem은 보통 마우스가 움직일 때 UI 감지를 업데이트 하는데 Locked 상태에서는 마우스 이동이 없다고 판단될 수 있다고 한다. 

원인 코드 : 커서 Locked

해결 :  클릭 감지 시 UI 이벤트 수동으로 실행 

이벤트 감지를 못하는 것이 원인이기 때문에 마우스 클릭 시 현재 클릭에 걸리는 UI를 체크해서 이벤트를 실행시켜주기로

했다. 

UI 요소에 Ray를 쏘고 클릭 이벤트 직접 실행

  • 클릭 이벤트 실행 시 넣어줘야 하는 PointerEventData의 생성자에 현재 이벤트 시스템을 넣어주고 position은 마우스 포지션(대부분의 상황에서 화면의 중심임)으로 세팅해준다.
  • EventSystem의 RaycastAll은 UI 요소에 별도의 Raycast를 수행한다. 
  • Ray에 걸린 애들의 클릭 이벤트를 모두 실행시킨다.

마우스 좌클릭 시 실행하던 로직이 있어서, 그곳에서 WorldUIClick 메서드를 추가로 호출해줘서 해결하였다.

+ 이곳의 Raycast에서 최상위 UI만 검출되는 것이 아니라 UI가 걸리기 때문에, 쓸데없는 검출을 막고자 배경이미지같은 UI들은 Raycast Target 옵션을 비활성화 해줬다. 


제네릭, return T, default

두 번째 역할 분담으로 게임 데이터 저장 기능을 맡게 되었다. 지난번 팀플에서도 데이터 로더를 만들었었는데, 튜터님 피드백 시간에 로더는 다 같은 코드를 갖고 있으니 제네릭으로 만들면 보완이 될 것이라고 말씀해주셨었다. 그래서 이번에는 제네릭으로 DataManager를 구현했다.

그런데 제네릭 함수에서 return null을 하면 오류가 뜬다. T가 nullable이 아닐 수 있기 때문이다. 값 타입이라든가.. 

제네릭에서 return null;

그래서 에러 메세지에 뜬대로 dafault를 사용했다. dafault는 각 타입의 기본값을 반환해준다. int라면 0, 클래스라면 null 등.

공식문서

 

기본 제공 형식의 기본값 - C# reference

bool, char, int, float, double 등과 같은 C# 형식의 기본값을 알아봅니다.

learn.microsoft.com

dafault(T)로 써줘도 되고, default만 써줘도 된다. 

default를 사용하는 것으로 수정

 

암호화, 복호화 적용된 SaveData(), LoadData() 코드

(Encrypt, Decrypt 함수 내용은 글 아래에서)

public static T LoadData<T>(string filePath)
{
    if(File.Exists(Application.persistentDataPath + filePath))
    {
        string loaded = File.ReadAllText(Application.persistentDataPath + filePath);
        if (loaded == null)
        {
            throw new System.NullReferenceException();
        }

        string context = Decrypt(loaded);
        T Data = JsonConvert.DeserializeObject<T>(context);

        return Data;
    }
    else
    {
        return default;
    }
}

public static void SaveData<T>(T data, string filePath)
{
    string context = JsonConvert.SerializeObject(data);
    context = Encrypt(context);
    File.WriteAllText(Application.persistentDataPath + filePath, context);
}

 


데이터 암호화 / 복호화 

데이터를 저장할 때 json 파일 그대로 저장되는 것을 방지하기 위해 데이터의 간단한 암호화 / 복호화를 적용시켜 구현해보았다.

암호화 코드 

Key와 초기화벡터 IV를 이용해서 암호화하는 코드이다. 암호화한 내용물 앞에 IV값을 덧붙여서 내보내는 것을 볼 수 있다.

  • IV는 대칭키 암호화에서 사용되는 매개변수인데, 키가 같아도 IV는 항상 달라야 한다. IV가 같으면 같은 데이터에 대해 항상 같은 결과물이 나오게 되고, 그로인해 어떤 문자가 어떻게 암호화되는지 패턴을 찾을 수 있어지기 때문에 암호화의 의미가 없어진다. 그리고 사용한 IV를 키와 함께 알고 있어야 복호화가 가능하기 때문에 보통 암호문과 함께 저장한다고 한다. 
  • TransformFinalBlock() 으로 암호화가 이루어지고 있는데, 이는 암호화할 데이터가 블록 크기의 배수에 딱 떨어지지 않아도 마지막 블록에 패딩을 적용해서 올바른 암호화가 이루어지도록 한다. 
public static string Encrypt(string plainText)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;
        aes.GenerateIV();

        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
        byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(plainText);

        // 암호화 
        byte[] encryptedData = encryptor.TransformFinalBlock(bytesToEncrypt, 0, bytesToEncrypt.Length);

        // IV를 데이터 앞에 덧붙임
        byte[] result = new byte[aes.IV.Length + encryptedData.Length];
        Array.Copy(aes.IV, 0, result, 0, aes.IV.Length);
        Array.Copy(encryptedData, 0, result, aes.IV.Length, encryptedData.Length);

        return Convert.ToBase64String(result);
    }
}

 

그런데!! using문 저렇게 사용하는 거 처음 봤다. 이 문법은 IDisposable 인터페이스를 구현한 객체를 사용할 때, using 블록을 벗어나면 자동으로 Dispose()가 호출되고 리소스 정리가 자동으로 이루어지게 하는 문법이라고 한다.댕신기.

Aes의 부모 클래스에 IDisposable이 구현되어 있다. 

Aes는 IDisposable을 구현한 클래스임

저장할 때 암호화 전과 후를 비교한 모습

json 형태의 데이터
암호화된 데이터

 

복호화 코드

암호화할 때 앞에 붙여뒀던 IV를 복구하고, 그 IV와 키를 사용해서  복호화를 진행한다. 앞에 붙여둔 IV를 구할 때 aes.IV.Length 길이만큼 구하는데, 이는 항상 AES의 기본 블록 크기이기 때문에 항상 동일하다. (AES의 블록 크기는 16바이트 고정임.)

public static string Decrypt(string encryptedString)
{
    byte[] encryptedData = Convert.FromBase64String(encryptedString);

    using(Aes aes = Aes.Create())
    {
        aes.Key = key;

        //IV 복구
        byte[] iv = new byte[aes.IV.Length];
        byte[] realEncrytedContext = new byte[encryptedData.Length - iv.Length];
        Array.Copy(encryptedData, 0, iv, 0, iv.Length);
        Array.Copy(encryptedData, iv.Length, realEncrytedContext, 0, realEncrytedContext.Length);

        // 복호화 
        aes.IV = iv;
        ICryptoTransform decrytor = aes.CreateDecryptor(aes.Key, aes.IV);
        byte[] decryptedData = decrytor.TransformFinalBlock(realEncrytedContext, 0, realEncrytedContext.Length);

        return Encoding.UTF8.GetString(decryptedData);
    }
}

 

다 만들고서 봤는데 공식 문서에도 예시와 메서드 등이 잘 나와있따.

https://learn.microsoft.com/ko-kr/dotnet/api/system.security.cryptography.aes?view=net-8.0

 

Aes 클래스 (System.Security.Cryptography)

모든 AES(Advanced Encryption Standard) 구현에서 상속해야 하는 추상 기본 클래스를 나타냅니다.

learn.microsoft.com


 

많이 도움 받은 블로그 : https://ljhyunstory.tistory.com/349

 

[Unity] 유니티 데이터 저장하기[1] - (PlayerPrefs + 암호화)

유니티를 이용해서 게임/ 어플리케이션을 제작할 경우 생성되는 데이터들...저 역시 개발을 진행 할 수록 어플리케이션이 종료되어도 유지되는 데이터들을어딘가에 저장해야하는 상황이 발생

ljhyunstory.tistory.com

 

 

 

 

 

 

 

728x90