💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을
3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 


1. 멀티플레이 환경
2. 포톤 기본 세팅 
3. 포톤 연결 / 로비와 방 생성
4. 플레이어 생성 
5. Agora를 사용한 실시간 카메라 연결 

해당 포스터는 3번:포톤 연결 / 로비와 방 생성부터 작성되었습니다. 전 내용이 궁금하신 분들은 이전 블로그를 확인해 주세요.

이전 글 보러 가기 >

2025.02.19 - [Unity] - [Unity][멀티플레이어] #1. Photon을 유니티에 연동해 보자

 

[Unity][멀티플레이어] #1. Photon을 유니티에 연동해보자

💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다.  1.

youcheachae.tistory.com


🎨 3. 포톤 연결 / 로비와 방 생성 

- 전체적인 흐름

 

- PhotonManger 스크립트 생성

using Photon.Pun;

public class PhotonManager : MonoBehaviourPunCallbacks
{
    // 싱글톤 
    private static PhotonManager instance = null;
    public static PhotonManager Instnace { get => instance; }

    [Header("===Info===")]
    [SerializeField] string _roomVersion = "1.0.0";
    [SerializeField] string _roomName = "BlackVillage";

    [Header("===동기화 Player===")]
    [SerializeField] private GameObject _player;                    // 생성할 플레이어 오브젝트
    
    public GameObject photonPlayer => _player; 

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);
    }
}

□ MonoBehaviourPunCallbacks를 상속받아야 함.

□ 포톤은 Lobby - Room으로 이루어져 있음

           포톤 연결 후 Lobby에 연결

           룸생성 후 접속

 

- 포톤 서버 연결 함수

private void F_MasterClient() 
{

    PhotonNetwork.AutomaticallySyncScene = true;

    // 게임버전 할당 
    PhotonNetwork.GameVersion = _roomVersion;
    Debug.Log("포톤 서버 초당 데이터 통신 수 : " + PhotonNetwork.SendRate);

    // ** 포톤 사용자들을 포톤 서버에 연결
    if (!PhotonNetwork.IsConnected)
        PhotonNetwork.ConnectUsingSettings();           // ** 포톤 서버 세팅 정보로 

}

  PhotonNetwork.ConnectUsingSettings()

            포톤의 서버 세팅 정보로 포톤에 연결

   PhotonNetwork.AutomaticallySyncScene = true;

             마스터 클라이어언트가 LoadLevel 메서드 실행 시 , 즉 다른 씬으로 이동 시 
             다른 멤버들도 마스터클라이언트가 있는 씬으로 자동 이동

             false로 세팅하면 이동하지 않는다

 

- 서버의 마스터(나)가 서버에 접속 성공 시 콜백함수

public override void OnConnectedToMaster() 
{
    // MonoBehaviourPunCallbacks 의 OnConnected~함수를 오버라이딩
    // 서버 마스터가 접속하면 메서드 실행 
    Debug.Log("포톤에 접속 성공 유무 : " + PhotonNetwork.IsConnected);

    // 로비 접속 추가 
    PhotonNetwork.JoinLobby();
}

  서버 연결에 성공하면 OnConnectedToMater() 함수 실행 

   PhotonNetwork.JoinLobby();          

           로비 입장 함수 실행 

 

- 로비 접속 시 콜백함수

 public override void OnJoinedLobby()
 {
     // 로비에 접속하면 실행

     // Debug.Log("로비접속 "  + PhotonNetwork.InLobby);
     JointRoom();
 }

  로비에 입장하면 OnJoinedLobby() 함수 실행

  방(Room)에 입장 

 

- 방(Room) 생성 후 입장 

 private void JointRoom() 
 {
     // 방 정보 생성
     RoomOptions roomOptions = new RoomOptions();
     roomOptions.MaxPlayers = 3;
     roomOptions.IsOpen = true;
     roomOptions.IsVisible = true;

     // 생성 후 바로 입장
     PhotonNetwork.JoinOrCreateRoom(_roomName , roomOptions , TypedLobby.Default);
     // -> 방 이름 기준으로 작동함
     // 즉 마스터클라이언트가 방을 만들면, 다음 접속자는 이름에 해당하는 방이 이미 있으니 거기로 접속 
 }

  RoomOptions

           방 (room)을 만들 때 필요한 설정값들을 가지고 있음

  PhotonNetwork.JoinOrCreateRoom()

          ■ 해당 이름 (_roomName)의 방이 없으면 → 새 방을 생성하고 입장

           해당 이름의 방이 있으면  → 기존 방에 입장 

 public override void OnCreatedRoom()
 {
     // 방 생성시 호출 (오버라이딩)
     Debug.Log("방 생성 완료 : " + PhotonNetwork.CurrentRoom.Name );
 }

 public override void OnJoinedRoom()
 {
     // 방 입장 시 호출 (오버라이딩)
     Debug.Log($"{PhotonNetwork.CurrentRoom.Name} 에 입장 / 방에 있는지 여부 : {PhotonNetwork.InRoom}" +
         $" 인원 수 : {PhotonNetwork.CurrentRoom.PlayerCount}" );
 }
 public override void OnJoinRandomFailed(short returnCode, string message)
 {
     // 방 접속 실패 시 호출 (오버라이딩)
     Debug.Log($"JoinRoom Failed {returnCode} {message} / 방 없음 -> 생성시도중....");
     JointRoom();
 }

  OnCreateedRoom()

           마스터클라이언트가 최초 접속했을 때 실행

  OnJoinedRoom()

           방에 입장하면 호출

  OnJoinRamdonFailed

           방에 입장 실패 시 호출

           JointRoom() 다시 방 만들기 시도 

 

- 실행

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);

        // 게임시작 시 포톤 세팅 
        // 1. 포톤서버에 연결, 로비 생성 후 방 생성
        F_MasterClient();

    }

: PhotonManager의 Awake에서 실행한다.

🎨 4. 플레이어 생성

- 플레이어 생성

IEnumerator CreatePlayer() 
{
    // 3초 기다리기
    yield return new WaitForSeconds(3);

    // 입력받은 닉네임 가져오기 
    PhotonNetwork.NickName = UiManager.Instnace.F_InputName();

    // 플레이어 생성 
    // PhotonNetwork의 Instantiate 사용 
    _player = PhotonNetwork.Instantiate("Player", new Vector3(0,0,0), Quaternion.identity);

    if (_player == null)
    {
        Debug.Log("생성한 플레이어가 null");
        yield break;
    }
}

□ yield return new WaitforSeconde()

            서버 연결 + 로비생성 + 방 생성 후 바로 생성하는 것보다 혹시 모를 문제를 방지하기 위해서 3초 대기 

PhotonNetwork.NickName

            InputField에서 입력받은 닉네임으로 네트워크에 설정

  PhotonNetwork.Instantiate

            네트워크상 동기화되는 오브젝트 (player) 생성

            생성 시 모든 네트워크 참가자에게 자동으로 생성됨

            각 오브젝트에 고유한 네트워크 ID가 부여됨 

 

🔖 생성할 프리팹은 Resources폴더 하위에 있어야 한다

🔖 문자열로 검사하기 때문에 프리팹의 이름을 일치시켜 줘야 한다

🔖 프리팹에는 포톤뷰 컴포넌트, 트랜스폼 컴포넌트, 애니메이션 컴포넌트를 추가한다

 

- 로컬과 리모트

: 멀티플레이 게임에서는 내가 제어하는 플레이어 오브젝트만 제어해야 함!

게임을 플레이하는 주체에 따라 '로컬'과 '리모트'로 나뉨

🔖로컬(Local)

: 내가 직접 조작하는 캐릭터나 오브젝트

🔖리모트(Remote)

: 다른 사람이 조작하는 캐릭터

: 멀티플레이어에서, 나를 제외한 다른 사람이 조작하는 캐릭터는 모두 리모트 플레이어

 

PlayerMovement.cs

[Header("===Photon View")]
private PhotonView playerView;

private IEnumerator IE_PlayerMovenet() 
{
    while (true)
    {
        // 로컬체크 - 로컬이 아니면 움직임 x
        if (!playerView.IsMine)
            continue;

        // 플레이어 움직임 코드
        playerMove();

        // 매프레임마다 
        yield return null;
    }
}

  플레이어의 PhotonView의 IsMine 사용 

            로컬이 아니면 플레이어 움직임 코드 실행 x 

 

- 실행

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);

        // 게임시작 시 포톤 세팅 
        // 1. 포톤서버에 연결, 로비 생성 후 방 생성
        F_MasterClient();

        // 2. 플레이어 생성 
        StartCoroutine(CreatePlayer());
    }

: PhotonManager의 Awake에서 실행한다.

 


🎨 5. Agora를 사용한 실시간 카메라 연결  

2025.02.21 - [Unity] - [Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

 

[Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

→💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야 복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 1. 멀티

youcheachae.tistory.com

 

 


깃허브

https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

Contribute to kimYouChae/Sparta_Metaverse development by creating an account on GitHub.

github.com

 

 

1. 구현 동작
2. 문제점
3. 해결과정
    □ 어디서 문제가 발생하는가?
    □ GetHashCode()로 해결가능
4. 해결 방법
    □ PhonView의 IsMine
    □ 최종 코드
5. 결론

 

📝 1. 구현 중인 동작

  zep처럼 특정 구역에 들어가면, 오른쪽에 카메라가 켜고 꺼지는 동작을 구현중.

  내가 플레이하는 캐릭터 기준으로, 특정구역 들어갔을 때 사람이 있으면 사람 수 대로 오른쪽에 카메라가 생성되고, 

특정구역에 나갔을 때 카메라가 전부 삭제되어야 한다.

 

📝 2. 문제

  특정 오브젝트에서 모든 플레이어의 충돌을 감지하여 , 내가 조종하는 캐릭터가 아닌 다른 사람의 캐릭터가 오브젝트에 충돌할 시, 내 화면에도 UI가 켜지고 꺼지는 것을 확인했다. 

 

📝 3. 해결과정 

1. 해당문제가 발생하는 코드

  현재 "특정구역"에 Conference라는 스크립트를 넣어 충돌을 감지하고 있다.

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    private void OnTriggerEnter2D(Collider2D collision)
    {
        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum)
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

 

  여기서 하나 생각 못한 부분이 있다.

포톤에서 플레이어를 인스턴스화할 때 같은 프리팹으로 한다. 즉 생성되는 모든 플레이어는 같은 Player 레이어를 가지고 있다.

 

 

  같은 프리팹을 사용해서 인스턴스화하는 중.......

 

  즉 다른 사람(LayerMask가 Player)이 '특정구역'의 충돌 조건에 만족하는 것이다. 

 

🔖 내가 원했던 동작

  충돌을 할 때, 내가 플레이하는 캐릭터가 충돌 했을 때를 검사하고싶다.

내가 플레이 하는 캐릭터가 '특정구역'에 들어갔을 때, 나갔을 때에 조건을 검사해야 한다.

 

2. GetInstance()로 검사하면 되지 않을까?

  PhotonInstance로 생성한 오브젝트의 GetHashCode와, 충돌 시 매개변수로 들어온 충돌체의 GetHashCode를 검사해서 같으면, 내가 플레이하는 캐릭터를 알 수 있지 않을까?

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

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    private void OnTriggerEnter2D(Collider2D collision)
    {
        var hash = PhotonManager.Instnace.photonPlayer.GetHashCode();

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && hash == collision.gameObject.GetHashCode() ) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        var hash = PhotonManager.Instnace.photonPlayer.GetHashCode();

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && hash == collision.gameObject.GetHashCode() )
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

  원하는 대로 잘 동작한다.

PotonManger의 player는 항상 로컬 플레이어 오브젝트를 가지고 있기 때문에, 충돌체가 내가 플레이하고 있는 오브젝트인지 
(로컬 인지) , 다른 사람의 오브젝트인지 (리모트 인지) 검사할 수 있다. 

 

📝 4. 해결방법

  GetHashCode()를 사용하지 않고도 해결할 수 있다.

  내가 원하는 동작은 즉 "충돌체가 로컬 오브젝트"인지 검사해야 한다.

→ 로컬 오브젝트 인지 알 수 있는 방법은 PhotonView의 IsMine 사용하면 된다.

  플레이어 오브젝트는 동기화를 위해서 
PhotonView, Photon Trasform View, Photon Animator View의 컴포넌트를 추가해 놓은 상태이다 

  PhotonNetwork.Instantiate() 로 오브젝트를 생성하면, 로컬인지 리모트인지에 관한 정보가 Photon View에 저장된다.

(포톤이 알아서 정보를 저장해 준다 고마워 따봉포톤아!)

           로컬이면 IsMine이 true로 설정

           리모트이면 IsMine이 false로 설정 

 

PhotonView의 IsMine이 true일 때만 충돌한다! 

 

- 최종 코드

using Photon.Pun;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    // 플레이어의 포톤 정보 가져오기 
    [SerializeField]
    private PhotonView _playerView;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        _playerView = collision.GetComponent<PhotonView>();

        // 플레이어가 들어오면 + 로컬 플레이어면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && _playerView.IsMine ) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        _playerView = collision.GetComponent<PhotonView>();

        // 플레이어가 들어오면 + 로컬플레이어면
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && _playerView.IsMine)
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

 

 

- 로컬오브젝트의 충돌만 검사해서 Ui를 끄고켬!

 

 

 

📝 5. 결론

멀티플레이 게임을 만들 때 충돌검사를 할 시 로컬인지 리모트인지 검사하는 것이 중요하다는 것을 깨달았다.

또한 충돌처리가 있는 부분마다 검사해줘야 한다 생각하니 멀티게임이 왜 만들기 까다로운지 알게 되었다

 


스파르타-유니티 개인과제 깃허브

https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

Contribute to kimYouChae/Sparta_Metaverse development by creating an account on GitHub.

github.com

 

 

💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을
3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 

 


1. 멀티플레이 환경
2. 포톤 기본 세팅 
3. 포톤 연결 / 로비와 방 생성
4. 플레이어 생성 
5. Agora를 사용한 실시간 카메라 연결

🎨 1. 멀티플레이 환경

멀티플레이 환경을 구성할 수 있는 방법에는 몇 가지가 있다.

 

1. Unity의 NetCode For GameObjects (NGO)

: 유니티에서 제공하는 멀티플레이어 네트워크 솔루션

2. Photon (PUN2)

: 멀티플레이어 게임을 위한 클라우드 기반 서버 서비스

: 직관적이고 사용하기 쉬운 Api를 제공 / 클라우드 서버 제공하므로 서버 관리를 하지 않아도 됨

: 무료버전은 10 ~ 20명 정도 사용가능

: 100 트래픽 이하의 경우 무료로 사용가능

: 대규모 게임에는 추가적인 비용이 발생할 수 있음

: Fusion, Pun2, Quantum 등의 패키지가 있음

 

📝 P2P (Peer-to-Peer)

: 서버에 100프로 의존하지 않고 클라이언트들이 서로 통신하며 게임을 진행하는 방식

📝 Pun2

: 처음 방을 만든 플레이어가 자동으로 그 방의 "마스터 클라이언트"가 됨.

: 마스터 클라이언트가 게임에서 나가면 방에 남아 있는 다른 플레이어 중 한 명이 자동으로 새로운 마스터 클라이언트가 됨

: 누군가 마스터 역할을 항상 맡고, 이 과정이 자동으로 이루어지기 때문에 게임을 개발하거나 방을 관리하는 사람이 따로 신경 쓸 필요가 없다.

 

🔖서버-클라이언트 구조 (Client-Server-Architecture)

: p2p와 비교할 때 가져오는 네트워크 구조

: 서버가 모든 중요한 역할을 맡아서 처리하고, 클라이언트 (플레이어)는 서버에 요청을 보내고 응답을 받는 방식으로 동작함.

1. 서버 : 모든 게임의 중요한 데이터를 관리, 게임의 상태 동기화

2. 클라이언트 : 서버에게 요청을 보내어 게임에 필요한 정보를 받음

: 대표적인 예시로 MMO RPG 같은 대규모 온라인 게임에서 사용함.

 

🎨 2. 포톤 세팅

https://dashboard.photonengine.com/ko-kr

 

로그인 | Photon Engine

Sign In 아직 계정이 없으신가요? 회원 등록은 여기를 클릭

id.photonengine.com

1. 포톤 홈페이지에 로그인한 후 "관리화면으로 이동" 텍스트 클릭 

2. 새 애플리케이션 만들기 버튼 클릭

3. Photon 종류 - Pun으로 지정, 본인이 사용할 이름 작성 

4. 본인이 만든 Photon 애플리케이션이 보인다.

 5. 에셋스토어에 접속 후 PUN2를 검색 후 추가한다.

 

6. 유니티 프로젝트를 열어 PackageManger - My Asset의 Pun2를 다운로드 한 뒤 Import 한다.

7. PUN Wizard가 뜨면 포톤 홈페이지에서 만들었던 애플리케이션의 App Id를 복사 붙여 넣기 한다.

App Id 복사
Pun SetUp에 붙여넣기

7. Setup Project 버튼을 클릭한다

포톤과 연동을 완료하였다.

 


🎨 플레이어 생성

2025.02.20 - [Unity] - [Unity][멀티플레이] #2. 플레이어 생성, 동기화

 

[Unity][멀티플레이] #2. 플레이어 생성, 동기화

💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 1. 멀

youcheachae.tistory.com

 

🎨 Agora를 통한 실시간 카메라 연결

2025.02.21 - [Unity] - [Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

 

[Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

→💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야 복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 1. 멀티

youcheachae.tistory.com

 

 


깃허브

https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

Contribute to kimYouChae/Sparta_Metaverse development by creating an account on GitHub.

github.com

 

1. ScipratbleObject
2. 생성 방법
3. 사용 방법
4. 유용한 이유, 하나의 메모리에 참조

 

📝 1. ScriptableObject 

: 스크립터블 오브젝트(ScriptableObject )는 대량의 데이터를 저장하는 데 사용할 수 있는 데이터 컨테이너다.

: 값의 사본이 생성되는 것을 방지하여 프로젝트의 메모리 사용을 줄이는데 유용하다.

: MonoBehaviour 스크립트를 프리팹에 넣으면, 해당 프리팹을 인스턴스 화 할 때마다 사본이 생성된다.

: ScriptableObject를 사용하면, 메모리에 데이터 사본을 하나만 저장하여 모든 프리팹이 하나의 메모리에 참조한다.

 

📝 2. 생성 방법

[CreateAssetMenu(fileName = "MonsterData", menuName = "ScriptableObjects/MonsterData")]
public class Scriptable_Monster : ScriptableObject
{
    [SerializeField] private int health;
    [SerializeField] private int damage;
    [SerializeField] private string monsterName;

    // 프로퍼티
    public int Health { get => health; set => health = value; }
    public int Damage { get => damage; set => damage = value; }
    public string MonsterName { get => monsterName; set => monsterName = value; }

}

ScriptableObject를 상속받아서 구현한다.

CreateAssetMenu

             에디터의 메뉴를 통해서 쉽게 에셋을 생성할 수 있게 한다.

           ■  fileName : 저장할 파일의 이름

             menuName : create시 눈에 보이는 목록 이름

 

- 생성방법

상단의 Asset 버튼을 클릭하거나 Asset 폴더 내에서 우클릭 → Create   ScriptableObject MonsterData 선택

 

: Asset폴더에 잘 생성되었다.

 

: 인스펙터창에서 스크립트의 프로퍼티들을 확인할 수 있다.

 

 

📝 3. 사용방법

: 각각 다른 필드를 가진 Monster 에셋을 여러 개 만들었다: 예를 들어서, Health는 10, Damage는 10, Name은 Duck으로 입력해 준다.

 

MonsterTest라는 Monobehavior을 상속받는 클래스를 하나 만들어주고,
Slime 프리팹에 추가한다.

public class MonsterTest : MonoBehaviour
{
    [SerializeField]
    private Scriptable_Monster _monsterScriptable;

    private void Start()
    {
        Debug.Log("몬스터 체력 :: " + _monsterScriptable.Health);
        Debug.Log("몬스터 이름 :: " + _monsterScriptable.Damage);
        Debug.Log("몬스터 공격력 :: " + _monsterScriptable.MonsterName);
    }

}

 

그리고 Slime이라는 이름의 ScriptableObject 타입의 데이터를 드래그해서 넣어준다.

 

실행하면,해당 Slime에 작성한 필드들이 출력되는 것을 확인할 수 있다.

 

 

📝 4. 유용한 이유, 하나의 메모리에 참조

위쪽에서 ScriptableObject를 설명할 때
" ScriptableObject를 사용하면, 메모리에 데이터 사본을 하나만 저장하여 모든 프리팹이 하나의 메모리에 참조한다."라는 말을 사용했다. GetHashCode()를 사용하여 테스트해 보았다.

 

Slime프리팹에 추가된 클래스에 HasCode()를 출력하는 코드를 추가하였다.

private void Start()
{
    Debug.Log("몬스터 체력 :: " + _monsterScriptable.Health);
    Debug.Log("몬스터 이름 :: " + _monsterScriptable.Damage);
    Debug.Log("몬스터 공격력 :: " + _monsterScriptable.MonsterName);

    Debug.Log("Scriptable Object를 사용한 인스턴스화 : " + _monsterScriptable.GetHashCode());

}

 

 

이와 비교하기 위해서, 클래스를 인스턴스화하는 클래스도 작성해 보았다.

public class MonsterClass
{
    private int health;
    private int damage;
    private string monsterName;
}


public class MonsterClassInstanceTest : MonoBehaviour
{
    [SerializeField]
    MonsterClass monster;

    void Start()
    {
        monster = new MonsterClass();

        Debug.Log("클래스 인스턴스화 : " + monster.GetHashCode());
    }
}

 

각각 10번씩 반복해서 Instantiate (인스턴스화) 해보았다.

public class ScriptableSpawn : MonoBehaviour
{
    [SerializeField]
    private GameObject _objectableSlime;    // ScriptableObject를 사용하는 프리팹
    [SerializeField]
    private GameObject _classSlime;         // 클래스를 사용하는 프리팹

    private void Start()
    {
        for (int i = 0; i < 10; i++) 
        {
            Instantiate(_objectableSlime);

            Instantiate(_classSlime);
        }
    }
}

 

- 출력 

  ScriptableObject를 사용하는 오브젝트들은 다 같은 Hashcode를 가진다.

             모든 오브젝트가 Asset으로 생성된 동일한 ScriptableObject를 참조하기 때문이다.

  클래스를 인스턴스화하는 오브젝트들은 다 다른 Hashcode를 가진다. 

              new 키워드를 사용했기 때문에 각각 독립적인 메모리에 할당되어 있다.

             각 인스턴스는 자신만의 데이터 복사본을 가진다.

 

(+)

🔖문제점

상황에 따라 동일한 ScriptableObject를 참조하는 것이 문제가 될 수 있다.

예를 들어, 던전에 슬라임 10마리가 생성되어서 슬라임 한 마리를 처치했는데, 즉 Health가 0 이하로 떨어졌는데

다 같은 메모리를 참조하기 때문에 던전에 생성된 모든 슬라임의 Health가 0 이하가 되는 대참사가 발생할 수도 있다.

 

1. 해결방법

: 변경될 수 있는 데이터는 각 인스턴스에서 별도로 관리해야 한다.

public class Monster : MonoBehaviour 
{
    public MonsterData monster; 	// ScriptableObject 
    private int currentHealth; 	// 변하는 데이터
    
    void Start()
    {
        // 초기 체력은 ScriptableObject에서 복사
        currentHealth = monster.Health;
    }
}

 

2. 불변의 데이터를 저장하는 데 사용한다.

□ 스프라이트, 사운드 등 에셋참조

□ 몬스터 이름, 설명 등 고정 텍스트 


유니티 - ScriptableObject

https://docs.unity3d.com/kr/2022.3/Manual/class-ScriptableObject.html

 

ScriptableObject - Unity 매뉴얼

ScriptableObject는 클래스 인스턴스와는 별도로 대량의 데이터를 저장하는 데 사용할 수 있는 데이터 컨테이너입니다. ScriptableObject의 주요 사용 사례 중 하나는 값의 사본이 생성되는 것을 방지하

docs.unity3d.com

 


깃허브

https://github.com/kimYouChae

 

kimYouChae - Overview

클라이언트 개발자 지망생입니다! . kimYouChae has 9 repositories available. Follow their code on GitHub.

github.com

 

1. 과제
2. 아이디어
3. Mathf.Clamp()
4. 구현코드, 시연영상

 

📝 과제

: 카메라가 플레이어를 따라다니되 맵 범위를 벗어나지 않는 카메라를 구현했다.

예시영상

📝 아이디어

: 맵이 직사각형일 때 , 왼쪽 아래의 x, y값(최솟값)과 오른쪽 위의 x, y값 (최댓값)을 구하 카메라가 그 위치를 넘지않으면 되지 않을까?

 

📝 x 범위

: 최소 "왼쪽 아래 x 값 + 8.8" ~ 최대 "오른쪽 위 x 값 - 8.8" 

 

📝 y 범위

: 최소 "왼쪽 아래 y 값 + 5" ~ 최대 "오른쪽 위 y 값 - 5" 

 

📝 Mathf.Clamp(값, 최소값, 최대값)

: Mathf.Clamp()를 사용하면 쉽게 최소값과 최대값 사이의 결과를 받을 수 있다.

🔖 주어진 값이 최솟값보다 작으면 최솟값 return

🔖 주어진 값이 최댓값보다 크면 최댓값 return

 

Mathf.Clamp(float value, float min, float max);

□ 파라미터

           value : 최소값과 최대값으로 정의된 범위 내에서 제한할 부동 소수점 값 

 

           min : 비교할 최소 부동 소수점 값

           max : 비교할 최대 부동 소수점 값

 

📝 실제 구현

- 필드

 [Header("===Camera===")]
 [SerializeField] private Transform _camera;         // 카메라 
 [SerializeField] private Transform _playerTrs;

 [Header("===Map Edge===")]
 [SerializeField] private Transform _leftDownEdge;   // 왼쪽 아래
 [SerializeField] private Transform _rightUpEdge;    // 오른쪽 위

 

- Start/Update

 private void Start()
 {
     _playerTrs = PlayerManager.Instnace.playerTrs;

 }

 private void LateUpdate()
 {
     F_CheckLimitAndFollow();
 }

 

- F_CheckLimitAndFollow()

 private void F_CheckLimitAndFollow() 
 {
     // x와 y 위치를 각각 제한
     float clampedX = Mathf.Clamp(_playerTrs.position.x,
         _leftDownEdge.position.x + halfWidth,
         _rightUpEdge.position.x - halfWidth);

     float clampedY = Mathf.Clamp(_playerTrs.position.y,
         _leftDownEdge.position.y + halfHeight,
         _rightUpEdge.position.y - halfHeight);

     // 최종 카메라 위치 설정
     _camera.position = new Vector3(clampedX, clampedY, 0);

🔖Mathf.Clamp()로 x와 y의 위치를 제한했다.

 

- 구현이 잘 된 모습을 볼 수 있다!

 


-  유니티 Mathf.Clamp() 공식문서

https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Mathf.Clamp.html

 

Unity - Scripting API: Mathf.Clamp

Returns the minimum value if the given float value is less than the minimum. Returns the maximum value if the given value is greater than the maximum value. Use Clamp to restrict a value to a range that is defined by the minimum and maximum values. Returns

docs.unity3d.com

 


https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

Contribute to kimYouChae/Sparta_Metaverse development by creating an account on GitHub.

github.com

🔥 DontDestroyOnLoad를 할 때 오브젝트가 missing이 되어서

🔥 어떤 경우에 missing 되는지 알아보는 글


- Manager스크립트

public class Manager : MonoBehaviour
{
    // 싱글톤
    private static Manager instance;

    [Header("===Asset 폴더에서 드래그한 오브젝트===")]
    [SerializeField]
    private Sprite _assetSprite;
    [SerializeField]
    private GameObject _assetPrefab;

    [Header("===Canvas상의 ui 컴포넌트===")]
    [SerializeField]
    private Button _button;
    [SerializeField]
    private TextMeshProUGUI _Text;

    [Header("===씬 상의 오브젝트===")]
    [SerializeField]
    private GameObject _sceneObject;

    private void Awake()
    {
        // 싱글톤
        // 중복방지
        if (instance != null && instance != this)
        {
            Destroy(gameObject);  // 객체 전체를 파괴
            return;
        }
        instance = this;

        // 삭제 x
        DontDestroyOnLoad(this.gameObject);

        // 버튼에 이벤트 추가
        _button.onClick.AddListener(test);
    }

    private void test()
    {
        SceneManager.LoadScene("TestScene2");
    }
}

어떤 오브젝트가 missing 되는지 알기 위해서 

 

1. Asset폴더에서 드래그한 오브젝트
2. Canvas상의 UI 컴포넌트
3. 씬 상의 오브젝트

 

3 분류로 나누어봤다. 

 

- 초기 인스펙터창

: 각각에 맞게 오브젝트를 할당한 모습이다.

- 하이어러키 창에서  DontDestroyOnLoad가 잘 되는 것도 볼 수 있다.

 

- 씬 전환 시!!!!!

- 씬 상에 있던 모든 오브젝트가 missing이 되는 상황이다

1. Asset폴더에서 드래그한 오브젝트
2. Canvas상의 UI 컴포넌트
3. 씬 상의 오브젝트

- Asset폴더에서 드래그한 오브젝트만 할당되어 있는 것을 볼 수 있다.

 


그럼 씬의 스크립트에 할당된 오브젝트들은 어떻게 되지?

- 씬 전환 전

- 씬 전환 후 

! 당연히 영향 안 받는다

 


이 글을 쓰게 된 계기

: 싱글톤을 검색하면 항상 연관되어 있는 것이 DontDestroyOnLoad이다.
나는 이때까지 UI 오브젝트나 맵 상에 있는 오브젝트를 관리하는 Manager 스크립트에 Singleton을 사용하였기 때문에

DontDestroyOnLoad 하면 missing오류가 날 수밖에 없었다.

: 스크립트의 역할에 맞춰서 DontDestroyOnLoad 해야 한다

 

예를 들어서)

DontDestroyOnLoad로 관리해야 할 데이터
1. 재화 데이터
2. 플레이어 해금 데이터
3. 사운드 관리 
4. 선택한 플레이어 데이터

정도가 있을 것 같다.

 

+) 만약 오브젝트를 DontDestroyOnLoad 해야 할 때

Resource.Load로 동적로딩을 해도 된다. 


 

개인프로젝트의 DontDestroyOnLoad문제를 해결하게 되면 싱글톤 관련해서 글을 작성하겠다.....

그리고 확실하지 않은 게 있으면 꼭 테스트프로젝트에서 테스트해 보고 적용하자...!!!!

 

1. 스크립트 순서를 정하지 않고 실행했을 때

 

 

2. Edit -> Project Setting -> ScriptExcutionOrder에서 스크립트 순서를 지정했을 때

 

⬅️ 예상 Csv awake 와 Start출력 후 PlayerSelectUi의 Awake와 Start출력

 

⬅️ 출력 

지정한 순서대로 Awake 먼저 실행 후

순서대로 Start를 실행한다.

 

https://docs.unity3d.com/kr/2019.4/Manual/ExecutionOrder.html

 

이벤트 함수의 실행 순서 - Unity 매뉴얼

Unity 스크립트를 실행하면 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행됩니다. 이 페이지에서는 이러한 이벤트 함수를 소개하고 실행 시퀀스에 어떻게 포함되는지 설명합니다.

docs.unity3d.com

⬅️ 유니티의 라이프사이클을 알고있었는데도 왜 Awake 전에 Start가 실행된다고 생각했을까 ? ㅎㅅㅎ

생각해보면 당연한 실행순서인데.....

 

<코드>

- CsvParent.cs

public abstract class CsvParent : MonoBehaviour
{
    protected int _test;

    private void Awake()
    {
        Debug.Log("Csv Parent 스크립트의 Awake");
    }

    private void Start()
    {
        Debug.Log("Csv Parent 스크립트의 Start");

        F_setting();
    }

    public abstract void F_setting();
}

 

- AnimalCsv.cs

public class AnimalCsv : CsvParent
{
    public override void F_setting()
    {
        Debug.Log("이 디버깅은 animalCsv입니다 ");
    }
}

 

- PlayerSelectUi.cs

public class PlayerSelectUI : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("PlayerSelectUI 스크립트의 Awake");
    }

    private void Start()
    {
        Debug.Log("PlayerSelectUI 스크립트의 Start");
    }

}

 


3. MonoBehaviour를 상속받은 클래스를 상속받은 클래스의 이벤트 함수

: 위 스크립트에서 CsvParent는 MonoBehaviour를 상속받고있고, AnimalCsv는 CsvParent를 상속받고 있다.

여기서 AnimalCsv스크립트에 Awake()와 Start()를 추가하고 실행하게되면 ?

public class AnimalCsv : CsvParent
{
    private void Awake()
    {
        Debug.Log("AnimalCsv 스크립트의 Awake");
    }

    private void Start()
    {
        Debug.Log("AnimalCsv 스크립트의 Start");
    }

    public override void F_setting()
    {
        Debug.Log("이 디버깅은 animalCsv입니다 ");
    }
}

 

! 하위의 Awake()와 Start()만 실행된다! 

상위의 Awake()와 Start()는 실행되지 않는다. 

 MonoBehaviour를 상속받은 클래스를 상속받은 클래스의 이벤트 함수 를 사용할 때는 이러한 점에 주의해야한다. 

 


https://github.com/kimYouChae

 

kimYouChae - Overview

클라이언트 개발자 지망생입니다! . kimYouChae has 6 repositories available. Follow their code on GitHub.

github.com

 

[ IPointer Interface ]

유니티에 내장되어있는 클릭,드래그,호버(마우스포인터 올릴때) 등의 이벤트를 처리해주는 아주 편리한 기능!  

 

  [IPointer Interface가 동작이 안돼요!]  

1. Scene에 EventSystem이 있어야함

2. Ui 오브젝트에 Raycast Target 을 체크 해야함

3. 게임오브젝트를 raycast한다면, Collider가 있어야함

4. 게임오브젝트를 raycast한다면, 카메라에 Physics Raycaster 가 있어야함

 

<참고링크> 

https://discussions.unity.com/t/implementing-ipointerclickhandler-interface-does-not-seem-to-work/150103/6

 

[사용방법]

< 지원되는 이벤트 >

https://docs.unity3d.com/kr/2022.1/Manual/SupportedEvents.html

 

지원되는 이벤트 - Unity 매뉴얼

이벤트 시스템은 다수의 이벤트를 지원하며 사용자가 작성한 입력 모듈을 통해 한층 더 효율적으로 커스터마이징할 수 있습니다.

docs.unity3d.com

 

using UnityEngine.EventSystems;

 

1. IPointerClickHandler - OnPointerClick() 

: 포인터를 누르고 뗄 때 호출됩니다.

public class EventSystemTest : MonoBehaviour , IPointerClickHandler 
{
    public void OnPointerClick(PointerEventData eventData)
    {
        Debug.Log("OnPointerClick");
    }
}

-> Ipointer Interface를 사용할 때에는 항상 그에 맞는 메서드를 오버라이딩 해야함! 

 

2. IPointerEnterHandler - OnPointerEnter() / IPointerExitHandler - OnPointerExit()

: 오브젝트에 포인터가 들어갈 때 / 나올 때 호출됩니다. 

 

3. IPointerDownHandler - OnPointerDown() / IPointerUpHandler - OnPointerUp()

: 포인터가 오브젝트 위에서 눌렸을 때 / 포인터를 뗄 때 호출됩니다.

 

 

4. IBeginDragHandler - OnBeginDrag()

/ IDragHandler - OnDrag()

/ IEndDragHandler - OnEndDrag()

: 드래그가 시작되는 시점에 / 드래그 하는 동안 / 드래그를 끝낸 시점에 대상오브젝트에서 호출됩니다.

 


https://github.com/kimYouChae

 

kimYouChae - Overview

클라이언트 개발자 지망생입니다! . kimYouChae has 6 repositories available. Follow their code on GitHub.

github.com

 

+ Recent posts