유니티 게임 프로젝트 4주차 작업 기록

박서영·2026년 2월 27일

2월 7일 작업로그

1. FieldItem.cs 스크립트 작업

  • 바닥에 떨어지는 아이템에 붙여줄 스크립트 작업
    • 조합 완료 후의 폭죽 ⇒ 스크립트 수정후 폭죽 역시 이 스크립트에 맞춰 생성 변경할 예정
    • 죽은 후 들고 있던 아이템
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;

public class FieldItem : MonoBehaviourPun, IInteractable
{
    [Header("Visual")]
    [SerializeField] private SpriteRenderer spriteRenderer;

    public int itemID;
    public ItemData itemData;

    [Header("UI 연결")]
    public TextMeshProUGUI interactText;

    void Start()
    {
        if (interactText == null) gameObject.GetComponent<TextMeshProUGUI>();
        interactText.gameObject.SetActive(false);
    }

    public void OnPhotonInstantiate (PhotonMessageInfo info)
    {
        object[] data = info.photonView.InstantiationData;

        if (data != null && data.Length > 0)
        {
            //넘어온 ID 받기
            this.itemID = (int)data[0];

            //ID 기반으로 이미지 채우기 -> ItemDatabase 사용해서 매핑
            ItemData dataObj = ItemManager.instance.GetItem(this.itemID);

            if (dataObj != null)
            {
                this.itemData = dataObj;
                this.spriteRenderer.sprite = dataObj.icon;
            }
        }
    }

    public void ShowUI(bool show)
    {
        interactText.gameObject.SetActive(show);
    }

    public void Interact(Player interactor)
    {
        photonView.RPC(nameof(RequestGetItem), RpcTarget.MasterClient, interactor);
    }

    

    [PunRPC]
    public void RequestGetItem(Player interactor)
    {   
        if (this.itemData == null) return;

        this.itemData = null;
        photonView.RPC(nameof(GetItem), interactor);
        PhotonNetwork.Destroy(gameObject);
    }
    
    [PunRPC]
    public void GetItem()
    {
        InventoryModel.instance.AddItem(this.itemData);
    }

    //사용X
    public void ShowPanel(bool show){}
}
  • 초반에 작성한 코드 ⇒ 포톤에서 메시지 받아서 온 정보(아이템 아이디로) Instantiate하는 법 몰라서 그 부분은 AI 사용해서 받고, 나머지는 작성했었는데 두 가지 문제가 있어서 아래처럼 수정
    • 문제1: void Interact()에서 당사자면 바로 아이템을 파괴하는 문제 ⇒ 이전에 작성한 폭죽 스크립트 보면서 작성해서 그때의 문제 파트 그대로 작성해버림
    • 문제2: GetItem() 부분에서 인자로 아이템 ID를 받아서 처리하는 것이 중간에 ReqeustGetItem()에서 null 처리하거나 중간에 다른 원인으로 인해 null처리되어 버려도 실행하는데에 문제가 없기에 그렇게 수정해야함
    • 문제3: 자동 생성을 위해 인터페이스 구현시켜야할게 있어서 그 부분 추가해야함 ⇒ IPunInstantiateMagicCallBack 사용해서 OnPhotonInstantiate()가 자동으로 호출됨
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;

public class FieldItem : MonoBehaviourPun, IInteractable, IPunInstantiateMagicCallback
{
    [Header("Visual")]
    [SerializeField] private SpriteRenderer spriteRenderer;

    //내부데이터
    private int itemID;
    private ItemData itemData;
    private bool isCollected;

    [Header("UI 연결")]
    public TextMeshProUGUI interactText;

    void Start()
    {
        if (interactText == null) gameObject.GetComponent<TextMeshProUGUI>();
        interactText.gameObject.SetActive(false);
    }

    //포톤 Instantiate로 생성될 때 데이터(ID)를 받는 함수
    public void OnPhotonInstantiate (PhotonMessageInfo info)
    {   
        //InstantiationData는 object[] 형태
        object[] data = info.photonView.InstantiationData;

        if (data != null && data.Length > 0)
        {
            //넘어온 ID 받기
            this.itemID = (int)data[0];

            //ID 기반으로 이미지 채우기 -> ItemDatabase 사용해서 매핑
            ItemData dataObj = ItemManager.instance.GetItem(this.itemID);

            if (dataObj != null)
            {
                this.itemData = dataObj;
                this.spriteRenderer.sprite = dataObj.icon;
            }
        }
    }

    public void ShowUI(bool show)
    {
        interactText.gameObject.SetActive(show);
    }

    public void Interact(Player interactor)
    {
        photonView.RPC(nameof(RequestGetItem), RpcTarget.MasterClient, interactor);
    }

    

    [PunRPC]
    public void RequestGetItem(Player interactor)
    {   
        if (this.itemData == null || isCollected) return;

        isCollected = true;

        photonView.RPC(nameof(GetItem), interactor, this.itemID);
        PhotonNetwork.Destroy(gameObject);
    }
    
    [PunRPC]
    public void GetItem(int id)
    {
        ItemData item = ItemManager.instance.GetItem(id);
        InventoryModel.instance.AddItem(item);
    }

    //사용X
    public void ShowPanel(bool show){}
}

2. FieldItem을 Instantiate하는 방법

  • 위의 OnPhotonInstantiate()를 통해서 아이템을 생성하기 위해서 InstantiationData에 아이템의 아이디를 넣어주어야함
    • 이때 방장이 아이템을 생성하거나 InstantiateRoomObject를 사용해야 다른 사람들도 볼 수 있고 + 후자를 사용해야 방장이 나가도 아이템이 유지되기에 후자를 선택해서 사용할 예정
  1. 폭죽 생성
  • 우선 이전에 했던 폭죽 생성을 이 코드로 갈아끼워서 돌아가는지 테스트해봄
    [PunRPC]
        private void RPC_TryCraftItem()
        {
            if (!PhotonNetwork.IsMasterClient) return;
    
            //재료 확인
            foreach (bool state in slotStates) 
            {
                if (!state) return;
            }
    
            int fireworkID = 3;
            object[] data = new object[] {fireworkID};
    
            Vector3 dropPos = spawnPoint.position;
            dropPos += (Vector3)(Random.insideUnitCircle * 0.2f);
            PhotonNetwork.InstantiateRoomObject(craftResultPrefabName, dropPos, Quaternion.identity, 0, data);
    
            for (int i=0; i<slotStates.Length; i++)
            {
                photonView.RPC(nameof(RPC_UpdateSlot), RpcTarget.All, i, false);
            }
    
        }
    • 많이 달라지지는 않았고, InstantiateRoomObject 호출하면서 마지막에 인자로 object[] data를 전달해주면 되는 형식이었다 ⇒ 생각보다 간단하고 원래랑 별 차이없어서 삽질한 기분…
  1. 죽은 후 아이템 드랍하는 부분

    • 구현할 때 고민은 관련 코드 작성을 PlayerController 스크립트에 있는 OnDie() 메소드에서 그 안에 있는 스크립트를 호출할지, 아니면 아예 그 메소드 안에서 InventoryModel을 호출해서 그 안에서 처리하게 할 지 였는데… 아무래도 인벤토리 관련이니까 단일책임이나 관련성으로 미루어보아 후자로 처리하는게 나을 것 같다는 결론이 나왔다… ⇒ 결론적으로는 InventoryModel을 호출해서 처리하고 거기서 드랍할 아이템을 받아와서 PlayerController에서 드랍하도록하기로 결정.
    • InventoryModel에 우선 아이템 드랍 관련 메소드를 새로 작성해줌
      public ItemData DropItem() 
          {
              ItemData dropItem = this.item;
      
              this.item = null;
              OnInventoryChanged?.Invoke();
      
              return dropItem;
          }
    • PlayerController의 OnDie() 메소드에서 위의 메소드를 호출해 드랍할 아이템을 가져와 드랍하도록 메소드 수정 작성
      void Die()
          {
              Debug.Log($"{photonView.Owner.NickName} 사망!");
      
              if (photonView.IsMine)
              {
                  Vector3 randomPos = transform.position;
                  ItemData dropItem = InventoryModel.instance.DropItem();
                  if (dropItem != null) photonView.RPC(nameof(RPC_DropItems), RpcTarget.MasterClient, dropItem.itemID, randomPos);
              }
              
              
      
              photonView.RPC(nameof(RPC_OnDieVisual), RpcTarget.All);
          }
      
          [PunRPC]
          void RPC_DropItems(int itemID, Vector3 randomPos)
          {
              //FieldItem 생성 로직 -> object 사용해서 ID 전달
              object[] data = new object[] {itemID};
      
              //플레이어가 나가도 아이템이 남아야하기 때문에 RoomObject로 생성하기
              PhotonNetwork.InstantiateRoomObject("FieldItemPrefab", randomPos, Quaternion.identity, 0, data);
          }
      
          [PunRPC]
          void RPC_OnDieVisual()
          {
              if (spriteRenderer != null)
              {
                  playerNameText.color = Color.red;
                  Color color = spriteRenderer.color;
                  color.a = 0.5f; //반투명
                  spriteRenderer.color = color;
              }
      
              GetComponent<Collider2D>().enabled = false;
          }
      • 작성하다보니 수정된게 조금 늘어났다…
      • 우선 OnDie() 메소드 내에서 InventoryModel의 인스턴스를 사용해 그 안에 있는 드랍해야할 아이템을 (우선은 1개의 아이템만 가져오도록 해두었다) 받아온다
      • 처음에는 바로 RoomObject를 Instantiate했는데,
        • 모든 플레이어가 아이템을 드랍해버리는 문제가 생겼다 ⇒ 이건 조건문으로 체크하도록해서 금방 수정했는데 이렇게 바꾸니 드랍한 아이템이 모두에게 보이지 않고 당사자에게만 보이는 문제가 또 생겼다 ⇒ 그래서 다른 대부분의 게임 로직들을 처리했듯이 바로 드랍을 처리하는게 아니라, 당사자의 “좌표”와 “드랍할 아이템 아이디”를 매개변수로 전달해서 RPC를 통해 방장이 RoomObject를 생성하도록 코드를 수정하였다
      • 이 문제를 해결하면서 플레이어 사망 후의 이미지가 모든 플레이어에게 그렇게 보이지 않고 당사자에게만 보인다는 것 역시 깨닫게 되었다 ⇒ 당연하다.. RPC로 처리 안했으니까..
        • 따라서 이 부분도 RPC_OnDieVisual()이라는 함수를 만들어 모든 플레이어가 볼 수 있도록 처리하였다

[죽기전: 화약 아이템 들고 있음]

[죽은 후: 바닥에 화약 아이템 떨어져있음 + 다른 사람 눈에도 반투명하게 보이도록 우선] ⇒ 이후에 생존자에게는 죽은 사람 안보이도록 처리해야함…

3. 새로운 가구 배치 및 충돌, 정렬 처리

  • 이전에 작업하듯이 충돌 및 정렬 처리를 위해 Collider와 피벗 설정을 새롭게 추가된 가구들에게 붙여주었다
  • 다만, 아직 기존의 가구들처럼 아이템 박스/슬롯 작업은 하지 않아 추후에 처리해야함

4. 벽난로 눌렀을 때 투표 시작시키기

  • 기존 IInteractable 인터페이스를 활용해서 Fireplace.cs 스크립트를 아래와 같이 작성
    using UnityEngine;
    using Photon.Pun;
    using Photon.Realtime;
    using TMPro;
    
    public class Fireplace : MonoBehaviourPun, IInteractable
    {
        [Header("UI 연결")]
        public TextMeshProUGUI interactText;
    
        void Start()
        {
            if (interactText != null) interactText.gameObject.SetActive(false);
        }
        public void ShowUI(bool show)
        {
            interactText.gameObject.SetActive(show);
        }
    
        public void Interact(Player interacter)
        {   
            Debug.Log("interact");
            GameStateManager.instance.MeetingButtonPressed(interacter);
        }
    
        //사용X
        public void ShowPanel(bool show) {}
    }
    
  • 상호작용 키인 [E] 키를 눌렀을 때, 투표를 시작하도록 GameStateManager.cs에 이전에 작성해두었던, 투표 시작 관련 로직을 연결해주기만하면 완성되었음.
  • 다른 IInteractable 구현한 스크립트/클래스처럼 관련 UI를 연결해주고 나머지는 이미 전에 작성된 코드들이 알아서 잘 돌아가서 간단하게 완성함 ⇒ 단, CustomProperties를 추가해 플레이어당 신고는 한 번 하도록 하는 부분은 코드 추가적으로 작성해야함. 현재로써는 interacter를 인자로 전달해서 GameStateManager 쪽에서 처리하는 방법을 생각중.
  • GameStateManager.cs에 CustomProperties를 확인하고 업데이트하는 등 관련 변수를 관리하는 코드를 추가로 작성함 ⇒ 이쪽에 작성하는게 단일 책임 원칙에 더 부합한… (벽난로는 정말 버튼 같은 느낌이고, 처리/로직은 다 GSM 쪽으로 넘겨야? 만약 다른 방법으로 투표 시작하게 될 때, 더 간편함)
    #region [투표 관련 로직]
    		...
    		public int maxMeetingCount = 1; //플레이어당 최대 회의 소집 횟수 
        private const string MEETING = "MeetingCount"; //상수 키값 (CustomProperties)
        ...
        
        public void MeetingButtonPressed(Player interacter)
        {   
            if (currentState != GameState.Playing_OnLight && currentState != GameState.Playing_OffLight) return;
            
            int currentCount = GetPlayerMeetingCount(interacter);
            if (currentCount >= maxMeetingCount) {
                Debug.Log("회의 소집 횟수를 모두 소진했습니다");
                return;
            }
            
           photonView.RPC("RPC_RequestMeeting", RpcTarget.MasterClient, interacter);
        }
    
        [PunRPC]
        public void RPC_RequestMeeting(Player interacter)
        {
            if (!PhotonNetwork.IsMasterClient) return;
    
            if (currentState == GameState.Voting) return;
    
            //2차 검사
            int currentCount = GetPlayerMeetingCount(interacter);
            if (currentCount >= maxMeetingCount) return;
    
            //회의 소집 가능 횟수 관련 CustomProperties 업데이트
            Hashtable props = new Hashtable {{MEETING, currentCount + 1}};
            interacter.SetCustomProperties(props);
    
            StartMeeting();
        }
    
        //플레이어의 현재 투표 소집 사용횟수 가져오는 함수
        private int GetPlayerMeetingCount (Player player)
        {
            if (player.CustomProperties.ContainsKey(MEETING))
                return (int) player.CustomProperties[MEETING];
            return 0; //키가 없으면 0번 사용한 것.
        }
        
        //투표 횟수 초기화용 함수
        public void ResetMeetingCounts()
        {
            foreach (Player p in PhotonNetwork.PlayerList)
            {
                Hashtable props = new HashTable {{MEETING, 0}};
                p.SetCustomProperties(props);
            }
        }
        #endregion
  • 테스트 해봤을 때 공지문이 떠서 사라지지 않는 것을 깨달음 ⇒ 애초에 지우는 코드 안 적어서 이건 당연하긴한데, 잠깐 띄우고 사라지는 것을 어떻게 처리할지 몰라서 AI에게 물어봤을때 “코루틴(Coroutine)”을 사용하는 것을 추천받음 ⇒ 다만 코루틴 쓰게되면 안그래도 GSM(GameStateManager)가 코드 양도 많고 지금 거대해서 “공지/메시지”를 적어주는 UI 클래스(NotifyUI)를 하나 만들어주고, GSM에서는 이 메시지 클래스를 호출해서 업데이트 하는 식으로 수정함. 코루틴은 UI 클래스 안에 작성해줘, GSM은 호출만하면 알아서 시간 지났을 때, 사라지게해주니 간편함 + 다른 공지/메시지 작성도 훨씬 간편함.

회고
4주차에는 여행 일정이 있어서 회의 전까지 작업한 날이 하루(굳이굳이 따지면 1.5일?인데 2월 7일에 작업로그를 그냥 한 번에 작성했다.)이다. 당시에 여행 일정이 있어서 맡은 분량도 확연히 적어서 하루에 집중해서 끝낼 수 있던 간단간단한 부분이었다..

profile
이불 밖은 위험해.

0개의 댓글