본문 바로가기
공허의 유산/표현의 자유

[백문이 불여일타] Unity용 Firebase 실시간 데이터베이스 예제

by 바른생활머시마 2023. 6. 9.
728x90
반응형

Firebase를 Unity에서 활용하는 기본적인 내용을 살펴 보았습니다.

https://learn-and-give.tistory.com/118

 

[백문이 불여일타] Unity용 Firebase 실시간 데이터베이스 시작하기

NodeJS 기반으로 이것 저것 해보려고 알아보던 중, Unity 기반으로 멀티플레이로 뭔가 검토 해 볼 것이 있어 Firebase 기반의 Unity 연동에 대해 알아보고자 합니다. 일단은 Firebase에서 제공되는 튜토리

learn-and-give.tistory.com

 

간단한 샘플을 하나 만들어 보겠습니다.

여러 명의 플레이어가 동시에 보여지는 간단한  application입니다.

 

데이터 구조화

 

플레이에 참가하는 사람을 Participants라고 하고, 이름과 좌표라는 속성을 가지도록 하겠습니다.

대략 요런 속성?? 이것을 Firebase의 Realtime Database에 저장하도록 합니다.

 

키 입력 확인

 

Unity 예제들을 보면 3인칭 샘플들이 많이 있는데, 지금은 그것이 중요한 것이 아니니 일단 키보드 방향키로 XZ 방향으로 움직이도록 하겠습니다. 

 

키보드 입력은 아래와 같이 3가지 상태를 체크 할 수 있습니다. 이 스크립트를 카메라에 임시로 할당해서 키보드 이벤트를 잘 잡는지 확인 해 보겠습니다.

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("스페이스 키 누름");
        }
        if (Input.GetKey(KeyCode.Space))
        {
            Debug.Log("스페이스 키 누르는 중");
        }
        if (Input.GetKeyUp(KeyCode.Space))
        {
            Debug.Log("스페이스 키 손뗌");
        }
    }

키보드 이벤트가 잘 잡히는 것을 볼 수 있네요.

 데이터 저장 테스트

 

키보드 입력에 따라 내 플레이어의 위치가 갱신 되게 할 것인데, 이 위치를 Firebase에 기록 할 것입니다. 

일단 스페이스 키를 누르면 하드 코딩 된 데이터를 저장하도록 해보겠습니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using Firebase;
using Firebase.Database;


public class FirebaseInit : MonoBehaviour
{
    DatabaseReference dbref = null;
    
    // Start is called before the first frame update
    void Start()
    {
        // Get the root reference location of the database.
        dbref = FirebaseDatabase.DefaultInstance.RootReference;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("스페이스 키 누름");
            updateParticipantsTransform();
        }
    }

    void updateParticipantsTransform()
    {
        string json = "{ 'participants':{ 'user_a':{ 'px':10,'py':10},'user_b':{ 'px':-10,'py':10},'user_c':{ 'px':-10,'py':-10} }";
        dbref.Child("participants").SetValueAsync(json);
    }

}

이렇게 했더니~

흠....

 

구성 파일을 다시 받아봅니다.

 

흠...

흠... 어디서부터 문제인지 확인 해 보기 위해 dbref의 null 체크를 해보니 DB가 null이군요..

Start에서 할당 받아두면 될 줄 알았는데 그렇지 않은가 봅니다. 사용 할 곳에서 다시 받도록 하니,

일단 null은 아니게 되었습니다.

 

다른 에러는, 로그를 보니 설정 파일 위치가 여기가 아닌가 봅니다.

StreamingAssets이라는 폴더를 만들고 그 하위로 설정 파일을 옮겨둡니다. (Tutorial의 내용은 Unity 버젼에 따라 다르게 적용해야 하나 봅니다.)

실행하고, 스페이스 키 누르면 특별한 오류 로그는 없네요.

Firebase를 확인 해 보니 뭔가 등록되기는 했네요.  내용은 없으니 완벽하게 된 것은 아니고..

어찌되었든, 일단 app 연결은 된 것이니, 입력 할 데이터나 사용 할 API 등을 확인 해 보면 되겠군요.

 

설정 할 데이터를 가이드 문서처럼 class를 json으로 변경 해 주는 방식으로 한번 해보겠습니다.

참가자 class를 먼저 정의하고,

using System.Collections;
using System.Collections.Generic;

public class Participant
{
    public string   name;
    public float px;
    public float py;

    public Participant()
    {
        name = "Unnamed";
        px = py = 0.0f;
    }

    public Participant(string name, float px, float py)
    {
        this.name = name;
        this.px = px;
        this.py = py;
    }
}

 

데이터 기록 시점에 요렇게 Participant instance를 만들어 이것으로 JSON을 만들어 등록합니다.

    void updateParticipantsTransform()
    {
        // Get the root reference location of the database.
        DatabaseReference dbref = FirebaseDatabase.DefaultInstance.RootReference;

        //string source = "{ 'participants':{ 'user_a':{ 'px':10,'py':10},'user_b':{ 'px':-10,'py':10},'user_c':{ 'px':-10,'py':-10} }";

        Participant part = new Participant("user_a", 10, 10);

        
        string json = JsonUtility.ToJson(part);
        if(dbref == null)
        {
            Debug.Log("DB가 null");
            return;
        }
        dbref.Child("participants").SetValueAsync(json);
    }

 

결과는~ 이렇게 해도 일반 JSON 문자열 저장하는 것과 같은 형식으로 저장되네요. tree로 보이길 기대했는데...

(이 내용은 나중에 뒤에서 나옵니다.)

다른 사용자 정보를 등록하기 위해서 이름과 좌표를 변경하여 등록하면.

        Participant part = new Participant("user_b", -10, -10);

사용자 이름이 다르니, 데이터가 추가 될 줄 알았는데 기대와 다르게 값이 교체 되었습니다.

곰곰히 생각 해 보면, participants라는 Child의 내용을 이것으로 설정해라고 했으니, 그 내용으로 대체 되는 것이 맞네요..

각 사용자별로 이름과 위치를 기록하기 위해서는 특정 사용자를 child로 선택을 해야 하니,

user가 participants의 하위 child이기 때문에 이렇게 해야 맞겠네요.

        Participant part = new Participant("user_b", -10, -10);

        string json = JsonUtility.ToJson(part);
        if(dbref == null)
        {
            Debug.Log("DB가 null");
            return;
        }
        dbref.Child("participants").Child(part.name).SetValueAsync(json);

 

결과도 확인 해 보겠습니다. 아까와 다르게 user_b라는 것이 문자열의 Key로 등록 된 것을 볼 수 있습니다.

 

이 상태에서 데이터 이름과 좌표를 변경하여 동일한 코드를 다시 실행하면, 애초의 의도와 같이 이름을 키로 하여 서로 다른 데이터로 잘 등록 됩니다.

 

Unity 객체와 연동하기

Firebase와 연동하는 객체 정보가 있고, 이것이 Unity로 표현되는 정보가 또 있습니다. 이것은 서로 관련은 있지만 동일한 데이터가 아니므로 서로 구별을 해야합니다.

 

일단, Firebase와 연동하여 Unity에 그려질 대상들을 먼저 정리 해 보겠습니다.

아래와 같이 바닥과 참여자 역할을 할 두 개의 객체를 생성하고, 두 객체의 기본 위치는 원점 주변으로 합니다.

 

user_a의 Transform을 얻어서 자동으로 JSON 추출을 해보려고 했는데, 안되네요. 지원되는 class가 따로 있나봅니다.

    void updateParticipantsTransform()
    {
        // Get the root reference location of the database.
        DatabaseReference dbref = FirebaseDatabase.DefaultInstance.RootReference;

        tr = user_a.GetComponent<Transform>();

        Participant part = new Participant("user_a", 10, 10);
        string json = JsonUtility.ToJson(tr);
        if(dbref == null)
        {
            Debug.Log("DB가 null");
            return;
        }
        dbref.Child("participants").Child(part.name).SetValueAsync(json);
    }

 

Transform에서 얻은 좌표를 넣어서 participant instance에 할당을 한 후, 앞에서 사용한 방법으로 다시 기록합니다.

    void updateParticipantsTransform()
    {
        // Get the root reference location of the database.
        DatabaseReference dbref = FirebaseDatabase.DefaultInstance.RootReference;

        tr = user_a.GetComponent<Transform>();

        Participant part = new Participant("user_a", tr.position.x, tr.position.z);
        string json = JsonUtility.ToJson(part);
        if(dbref == null)
        {
            Debug.Log("DB가 null");
            return;
        }
        dbref.Child("participants").Child(part.name).SetValueAsync(json);
    }

잘 되네요.

실시간 좌표 업데이트

 

키보드로 움직이면서 그 좌표를 기록해보도록 하겠습니다. 먼저 키보드로 오브젝트 이동부터 구현합니다.

방향키를 입력하면 위치를 갱신하도록 합니다..

  // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("스페이스 키 누름");
            updateParticipantsTransform();
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            Debug.Log("왼쪽 화살표 누름");
            // updateParticipantsTransform();
            tr = user_a.GetComponent<Transform>();
            tr.position -= new Vector3(Time.deltaTime * speed, 0, 0);
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            Debug.Log("오른쪽 화살표 누름");
            // updateParticipantsTransform();
            tr = user_a.GetComponent<Transform>();
            tr.position += new Vector3(Time.deltaTime * speed, 0, 0);
        }
        if (Input.GetKey(KeyCode.UpArrow))
        {
            Debug.Log("위 화살표 누름");
            // updateParticipantsTransform();
            tr = user_a.GetComponent<Transform>();
            tr.position += new Vector3(0,0,Time.deltaTime * speed);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            Debug.Log("아래 화살표 누름");
            // updateParticipantsTransform();
            tr = user_a.GetComponent<Transform>();
            tr.position -= new Vector3(0,0,Time.deltaTime * speed);
        }
    }

뭐 별로 어려운 것이 아니니 간단하게 되네요.

 

이제 이것을 계속 DB에 기록 해 보겠습니다.

위치 수정 후 기존에 DB에 기록하던 updateParticipantsTransform을 호출하기만 하면 됩니다.

 

결과는,

꽤 빠르게 업데이트 되는 것을 볼 수 있습니다.

 

이제 멀티플레이를 한다고 가정하고 한번 검토 해 보겠습니다.

멀티 플레이어를 할 때 내 Client 위치는 키보드 입력으로 갱신했는데 그것이 상대방과 동기화 되지 않으면 이상하게 되니, 내 위치 갱신은 Firebase에 기록하고, 그 기록 된 내용을 가져와서 위치를 업데이트 해주는 방식으로 해보도록 하겠습니다 Firebase 기반으로 구현했던 채팅도 그런 방식이었는데, 주기적으로 위치를 가져오는 방식을 쓰거나, 업데이트 되면 callback 을 호출하도록 이벤트를 등록하는 방식이 있습니다. 이벤트 관련 내용 있었으니 그것을 한번 살펴보겠습니다.

 

이벤트 관련 내용은 이전에 Skip한 내용 중에 있습니다.

이벤트 수신 대기

https://firebase.google.com/docs/database/unity/retrieve-data?hl=ko#listen_for_events 

 

데이터 검색  |  Firebase 실시간 데이터베이스

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 데이터 검색 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 문서에서는

firebase.google.com

 

Start 함수에서 participants 하위의 데이터 변경에 대해 이벤트를 통지 받도록 합니다.

    // Start is called before the first frame update
    void Start()
    {
        //Firebase event 등록
        FirebaseDatabase.DefaultInstance
                    .GetReference("participants")
                    .ValueChanged += HandleValueChanged;
    }

    void HandleValueChanged(object sender, ValueChangedEventArgs args)
    {
        if (args.DatabaseError != null)
        {
            Debug.LogError(args.DatabaseError.Message);
            return;
        }
        // Do something with the data in args.Snapshot
        Debug.Log("participants 수정 됨.");
    }

 

이동 시 잘 통지되는 것을 볼 수 있습니다.

이제 키 입력이 되면 좌표를 DB에 보내기만 하고, 위치 갱신은 이벤트 발생 시 처리하도록 하겠습니다.

데이터가 수정되어 이벤트가 호출되면 관련 데이터의 snap shot이 인자로 넘어옵니다. 이 snap shot은 Dictionary로 구성되어 있습니다. 로그로 출력 해 보면....

participants 수정 됨.DataSnapshot { key = participants, value = System.Collections.Generic.Dictionary`2[System.String,System.Object] }

 

전달 받은 Snapshot에서 데이터를 꺼내는 방법을 찾기가 매우 어려웠는데, Dictionary로 Casting하고 여러가지 시도를 했는데, 아래 사이트에서 작동하는 코드를 겨우 찾았습니다.

https://answers.unity.com/questions/1780756/firebase-list-users.html#:~:text=FirebaseDatabase.DefaultInstance.GetReference%28%22Users%22%29.OrderByChild%28%22Score%22%29.ValueChanged%20%2B%3D,AuthManager_ValueChanged%3B%20%7D

 

Firebase List Users - Unity Answers

 

answers.unity.com

 

아래의 코드를 통해서 전달 받은 Snapshot의 Key와 Value들을 다 출력 할 수 있습니다.

        // Do something with the data in args.Snapshot
        if (args.Snapshot != null && args.Snapshot.ChildrenCount > 0)
        {
            //Show Results in a list
            foreach (var snapshot in args.Snapshot.Children)
            {
                var uname = snapshot.Key.ToString() + ": " + snapshot.GetValue(true).ToString();
                Debug.Log(uname.ToString());
            }
        }

 

기왕 정리하는 김에, 로컬에서는 '내' 데이터만 관리하고, '다른 참가자'는 정보 업데이트 시, 동적으로 추가 해 주도록 합니다. 여러 사람이 함께 사용 할 수 있도록 "내 정보"를 Editor에서, Participants의 Inspector를 통해 입력하고 그 정보로 나를 초기화 하도록 합니다. 이때 이름으로 나와 다른 사람을 구분하도록 합니다.(이름이 같은 경우의 예외 처리는 필요할 때)

 

일단, 여차저차 해서 코드를 정리하면 아래와 같이 됩니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using Firebase;
using Firebase.Database;


public class FirebaseInit : MonoBehaviour
{
    public GameObject unity_me;
    public string user_me_name;
    public float user_me_px;
    public float user_me_pz;

    private Participant data_me;
    private Participant data_dummy;

    private Transform unity_me_transform;
    private const float speed = 10.0f;

    // Start is called before the first frame update
    void Start()
    {

        unity_me_transform = unity_me.GetComponent<Transform>();
        unity_me_transform.position = new Vector3(user_me_px, 0, user_me_pz);

        data_me = new Participant(user_me_name, unity_me_transform.position);
        data_dummy = new Participant(user_me_name, unity_me_transform.position);

        //Firebase event 등록
        FirebaseDatabase.DefaultInstance
                    .GetReference("participants")
                    .ValueChanged += HandleValueChanged;
    }

    void HandleValueChanged(object sender, ValueChangedEventArgs args)
    {
        if (args.DatabaseError != null)
        {
            Debug.LogError(args.DatabaseError.Message);
            return;
        }
        // Do something with the data in args.Snapshot
        if (args.Snapshot != null && args.Snapshot.ChildrenCount > 0)
        {

            if (args.Snapshot != null && args.Snapshot.ChildrenCount > 0)
            {
                //Show Results in a list
                foreach (var a_user in args.Snapshot.Children)
                {
                    string user_val = a_user.GetValue(true).ToString();
                    data_dummy.updateWithJSON(user_val);

                    // Me
                    if(data_dummy.name == user_me_name)
                    {
                        data_me.updateWithJSON(user_val);
                        unity_me_transform.position = data_me.pos;
                    }
                    // Me가 아니면 다른 Agent 표시
                }
            }
        }
    }


    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("스페이스 키 누름");
            updateParticipantsTransform();
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            //Debug.Log("왼쪽 화살표 누름");
            unity_me_transform.position -= new Vector3(Time.deltaTime * speed, 0, 0);
            updateParticipantsTransform();
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            //Debug.Log("오른쪽 화살표 누름");
            unity_me_transform.position += new Vector3(Time.deltaTime * speed, 0, 0);
            updateParticipantsTransform();
        }
        if (Input.GetKey(KeyCode.UpArrow))
        {
            //Debug.Log("위 화살표 누름");
            unity_me_transform.position += new Vector3(0,0,Time.deltaTime * speed);
            updateParticipantsTransform();
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            //Debug.Log("아래 화살표 누름");
            unity_me_transform.position -= new Vector3(0,0,Time.deltaTime * speed);
            updateParticipantsTransform();
        }
    }

    void updateParticipantsTransform()
    {
        // Get the root reference location of the database.
        DatabaseReference dbref = FirebaseDatabase.DefaultInstance.RootReference;
        data_me.pos = unity_me_transform.position;
        string json = JsonUtility.ToJson(data_me);
        if(dbref == null)
        {
            Debug.Log("DB가 null");
            return;
        }
        dbref.Child("participants").Child(data_me.name).SetValueAsync(json);
    }

}

 

Prefab 등록

나와 남을 나타내는 데이터는 이후에도 계속 여러 개 사용 될 수 있으므로, Prefab으로 설정 해 두도록 합니다. 이렇게 해둬야 나중에 Asset Bundle로 내 모습을 변경하는 것도 용이 할 것 같습니다.

 

Other는 동적으로 생성하면서 이미 있으면 업데이트 해주고 없으면 생성하여 추가 해 주도록 합니다.

데이터를 받지 못하면 사라지는 처리도 하기는 해야 되겠네요. 고려 할 것이 많으니 차차 해보기로...

 

Other 데이터를 좀 수정하고 나서 잘 되던 코드가 잘 안되서 원인을 찾아보니,

 

DB에 저장 되는 형식을 JSON에서 Object로 변경했기 때문입니다. 동일한 데이터를 다른 형태로 보여주기만 하는 것인가 싶었는데 저장 형태가 달라서 처리도 달라져야 하는 것 같네요.

일단 JSON 문자열로 처리하는 중이니, 다시 String으로 관리하도록 하겠습니다. Object에서 String으로는 변환이 되는데 그 역변환은 방법을 못찾겠네요.

 

라벨의 표시

여러 개의 Agent가 있을 때 이를 구별하기 위해 아래와 같이 라벨을 구현하여 사용합니다. 간단한 코드인데 인터넷 검색해서 찾기가 쉽지 않았습니다.

                        //Unity2D
                        Text txt = (Text)unity_me.GetComponentInChildren<Text>();
                        txt.text = data_me.name;

                        RectTransform tr = txt.GetComponent<RectTransform>();
                        Vector2 screenPoint = Camera.main.WorldToScreenPoint(data_me.pos);
                        tr.transform.position = screenPoint;

내 아바타를 키보드로 움직이거나 다른 사람 아바타를 Firebase console에서 위치를 수정하여 이동하면 요렇게 됩니다.

같은 코드를 여러 PC에서 실행하면 각자의 화면에는 내 것이 노란색으로 보이고 다른 사람이 초록색으로 보일 것입니다.

Firebase+Unity조합에서 Realtime Database를 사용하는 자료는 많지 않네요. 전용 게임엔진도 있으니..

 

728x90
반응형

댓글