티스토리 뷰
1월 20일
Player & ItemBox Interaction 구현
작업 내용 요약
Test_Scene_Nam 씬 만들고 작업.
플레이어와 아이템상자 간 상호작용 시스템 구현.
플레이어가 아이템상자에 접근하면 상호작용 UI (줍기(E)) 텍스트가 표시되며,
E키 입력 시 해당 아이템상자가 Photon 서버 기준으로 Destroy 되도록 처리.

1. Player 프리팹에 상호작용 추가
- Player 프리팹에 PlayerInteraction 스크립트 부착
- 이를 프리팹 자체에 적용하기 위해 overrides-apply all

- 아이템 탐지 위해 Interactable 전용 layer 만들고, 이를 Item에 부착
- 플레이어가 Interactable 오브젝트 근처에 접근 시:
- 아이템 상자 윗부분에 줍기(E) UI 표시
- E키 입력 시:
- 상호작용 대상 아이템을 PhotonNetwork.Destroy() 로 제거
- 방 안에 있는 클라이언트에게서 동일하게 삭제됨
- Inspector Field
- 상호작용 거리 : 1.2
- Interactable mask에만 반응하도록
- interact key : E키

- PlayerInteraction.cs 코드
using UnityEngine;
using Photon.Pun;
//플레이어가 앞의 상호작용 대상을 Raycast로 감지, E키로 상호작용
public class PlayerInteraction : MonoBehaviour
{
[Header("Raycast")]
//레이캐스트 쏘는 최대 거리. 이 거리 안에 있는 물체만 상호작용
public float interactDistance = 1.2f;
//Raycast가 맞출 레이어 필터(Interactable 레이어만 감지)
public LayerMask interactableMask;
[Header("Input")]
//상호작용 키를 E로 설정.
public KeyCode interactKey = KeyCode.E;
//내부 상태 변수
//현재 Raycast로 감지된 대상
private ItemBox currentTarget;
//플레이어가 바라보는 방향 벡터
private Vector2 lookDir = Vector2.right; //기본은 일단 오른쪽
//매 프레임마다 자동 호출됨
private void Update()
{
//현재 입력 기반으로 바라보는 방향(lookDir) 갱신
UpdateLookDirection();
//lookdir 방향으로 Raycast 쏴서 상호작용
DetectInteractable();
//대상이 있을 때 E키 입력 받으면 Interact() 실행
HandleInput();
}
//바라보는 방향 결정
void UpdateLookDirection()
{
//현재 입력 방향을 그대로 바라보는 방향으로 사용
//좌/우 입력 값 가져오기
float x = Input.GetAxisRaw("Horizontal");
//상/하 입력 값 가져오기
float y = Input.GetAxisRaw("Vertical");
//2d 방향벡터로 묶어서 input 변수 선언
Vector2 input = new Vector2(x, y);
//방향키를 하나라도 누르고 있으면
if (input.sqrMagnitude > 0.01f)
// 입력 방향을 길이 1로 정규화해서(크기는 상관없으니까) lookDir로 저장
lookDir = input.normalized;
}
//레이캐스트로 상호작용 가능한 물체 찾음
void DetectInteractable()
{
//플레이어 위치(transform.position)에서 lookDir 방향으로 interactDistance만큼 Raycast 쏜다.
//interactableMask로 interactable레이어만 맞도록 필터링
RaycastHit2D hit = Physics2D.Raycast((Vector2)transform.position, lookDir, interactDistance, interactableMask);
//이번 프레임에 새로 감지된 타겟 변수
ItemBox newTarget = null;
//레이캐스트가 어떤 콜라이더에 맞았으면
if(hit.collider != null)
//맞은 오브젝트의 ItemBox2D 스크립트 확인
newTarget = hit.collider.GetComponent<ItemBox>();
//타겟이 바뀌었으면(플레이어가 이동하면서 ui 옮겨가야 함)
if(newTarget != currentTarget)
{
//이전 타겟 ui 끔
if(currentTarget != null)
currentTarget.ShowUI(false);
//현재 타겟을 새 타겟으로 교체
currentTarget = newTarget;
//새 타겟 ui 켬
if(currentTarget!=null)
currentTarget.ShowUI(true);
}
//디버깅: Scene뷰에서 레이캐스트 어디로 쏴지는지 표시
//currentTarget 있으면 초록, 없으면 빨강 표시
Debug.DrawRay(transform.position, lookDir * interactDistance, currentTarget ? Color.green : Color.red);
}
//입력(E키) 처리
void HandleInput()
{
if(currentTarget == null) return;
//이번 프레임에 상호작용 /ㅋㅋㅋㅋㅌ키(E) 눌렀으면 true
if (Input.GetKeyDown(interactKey))
{
//E키 누르자마자 끔
currentTarget.ShowUI(false);
//타겟의 Interact() 호출, 누른 사람이 누구인지 photon에 전달
currentTarget.Interact(PhotonNetwork.LocalPlayer);
}
}
}
2. Item 상자 오브젝트 관련
- interactable layer 적용
- collider 설정 → isTrigger 체크
- Interact UI : 이 상자 아래 Canvas 끌어와서 붙임

- ItemBox 스크립트 부착
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
//아이템에 붙이는 스크립트
//플레이어가 E로 상호작용하면 Interact()가 호출됨
public class ItemBox : MonoBehaviourPun
{
//디버그용 콘솔에 띄우는 아이템 이름
public string debugItemName = "Test Item";
[Header("UI")]
[SerializeField]
//ItemBOX 아래 Canvas 넣기
private GameObject interactUI;
//중복 상호작용 방지 플래그(이미 먹힌 상자인지)
private bool used;
private void Awake()
{
//interactUI null 아닌 거 확인하고
if(interactUI != null)
//기본으로 줍기(E) 안보이도록
interactUI.SetActive(false);
}
//PlayerInteraction이 보면 켜고, 안 보면 끔
public void ShowUI(bool show)
{
Debug.Log($"[ItemBox] ShowUI({show}) on {name}");
//interactUI 존재 && 아직 사용 안한 상자일 때
if(interactUI != null && !used)
interactUI.SetActive(show);
}
//플레이어가 E키 상호작용 성공했을 때 호출될 함수
public void Interact(Player interactor)
{
//이미 누가 먹었으면(used=true) 추가 실행 금지
if (used) return;
//이제부터 사용됨 상태
used = true;
//먹는 순간 "줍기(E)" UI 끔
ShowUI(false);
//콘솔에 누가, 뭘 눌렀는지 확인
Debug.Log($"[ItemBox] Opened by {interactor?.NickName} : {debugItemName}");
//TODO(Phase2): 나중에 실제 아이템 데이터(ItemData)와 인벤토리 연결 코드를 여기에 추가할 예정
//예: playerInventory.Add(itemData); 같은 코드가 들어갈 자리
//보통 방장만 Destroy 하는 게 안전하다네유..
if (PhotonNetwork.IsMasterClient)
{
PhotonNetwork.Destroy(gameObject);
}
else
{
//이 클라이언트가 방장이 아니면 방장에게 요청 (RPC)
photonView.RPC(nameof(RequestDestroy), RpcTarget.MasterClient);
}
}
[PunRPC]
private void RequestDestroy()
{
//방장에서 실행됨
if (PhotonNetwork.IsMasterClient)
PhotonNetwork.Destroy(gameObject);
}
}
⚠️ 참고 사항 — Input System 설정
- 본 작업은 New Input System을 사용하지 않고 Old Input 방식(Input.GetKey, Input.GetKeyDown)을 사용함
- 따라서 프로젝트 설정에서 Input System을 BOTH로 설정해야 정상 동작함
필수 설정 경로
Edit →ProjectSettings →Player →OtherSettings
→ActiveInputHandling:BOTH
- 해당 설정이 New Input System Only로 되어 있을 경우:
- E 키 입력이 인식되지 않음
- 상호작용(UI 표시 / 아이템 줍기) 기능이 정상 동작하지 않음
1월 21일
작업 요약
1. 오브젝트 동작 변경
- ItemBox는 가구이므로 사라지지 않도록 변경.
- 시체는 신고되면 방 전체에서 사라지도록 만듦.
- 모두 플레이어와 상호작용하는 오브젝트들이므로 IInteractable 인터페이스 코드 새롭게 짰음.
using UnityEngine;
using Photon.Realtime;
//상호작용 가능한 대상들이 갖는 기능,약속 적어둔 인터페이스
public interface IInteractable
{
//가까이 있을 때 UI(줍기/신고) 켜거나 끄는 함수
void ShowUI(bool show);
//플레이어가 E를 눌렀을 때 실행되는 상호작용 함수
void Interact(Player interactor);
}
- 이에 따라 PlayerInteraction.cs 코드에서 IInteractable 사용하도록 변경
//맞은 오브젝트의 IInteractable 스크립트 확인
newTarget = hit.collider.GetComponent<IInteractable>();
- 변경된 ItemBox 코드
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
//아이템에 붙이는 스크립트
//플레이어가 E로 상호작용하면 Interact()가 호출됨
//인터페이스 implements
public class ItemBox : MonoBehaviourPun, IInteractable
{
//디버그용 콘솔에 띄우는 아이템 이름
public string debugItemName = "Test Item";
[Header("UI")]
[SerializeField]
//ItemBOX 아래 Canvas 넣기
private GameObject interactUI;
//중복 상호작용 방지 플래그(이미 먹힌 상자인지)
private bool used;
private void Awake()
{
//interactUI null 아닌 거 확인하고
if(interactUI != null)
//기본으로 줍기(E) 안보이도록
interactUI.SetActive(false);
}
//PlayerInteraction이 보면 켜고, 안 보면 끔
public void ShowUI(bool show)
{
Debug.Log($"[ItemBox] ShowUI({show}) on {name}");
//interactUI 존재 && 아직 사용 안한 상자일 때
if(interactUI != null && !used)
interactUI.SetActive(show);
}
//플레이어가 E키 상호작용 성공했을 때 호출될 함수
public void Interact(Player interactor)
{
//이미 누가 먹었으면(used=true) 추가 실행 금지
if (used) return;
//방장이 열림 확정->모두에게 전파->사용됨 상태로 만들어야 함
//오픈한사람의 넘버 초기화
int openerActorNumber = -1;
if(interactor != null)
{
//연 사람의 고유 넘버 저장
openerActorNumber = interactor.ActorNumber;
}
//내가 방장일 경우, 전체에게 '열림' 적용
if (PhotonNetwork.IsMasterClient)
{
//이 박스에 붙은 PhotonView 이용,
//방 안 모두에게 RpcOpen 함수 원격 호출(누가 열었는지도 전달)
photonView.RPC("RpcOpen", RpcTarget.All, openerActorNumber);
}
else
{
//방장 아닐 경우, 방장에게 open request
photonView.RPC("RequestOpen", RpcTarget.MasterClient, openerActorNumber);
}
}
[PunRPC]
//방장에게 열기 요청
private void RequestOpen(int openerActorNumber)
{
if(!PhotonNetwork.IsMasterClient) return;
//방장이 전체에게 open 적용
photonView.RPC(nameof(RpcOpen), RpcTarget.All, openerActorNumber);
}
//모두에게 열림 적용하는 함수
[PunRPC]
private void RpcOpen(int openerActorNumber)
{
if(used) return;
//열린 상태 확정
used = true;
//안내 ui 끄기
ShowUI(false);
//누가 열었는지 ActorNumber로 찾아서 로그 찍기
Player openerPlayer = null;
if(PhotonNetwork.CurrentRoom != null)
{
openerPlayer = PhotonNetwork.CurrentRoom.GetPlayer(openerActorNumber);
}
//"누가 뭘 열었습니다." 공지 필요할 경우
string openerName = "Unknown";
if(openerPlayer != null)
{
openerName = openerPlayer.NickName;
}
Debug.Log("[ItemBox] Opened by " + openerName + " : " + debugItemName);
//!!!!여기부터가 '연 사람만 받는 개인 처리' 자리
//예) 팝업 띄우기, 인벤토리에 넣기, 개인 효과 등
if(PhotonNetwork.LocalPlayer != null)
{
if(PhotonNetwork.LocalPlayer.ActorNumber == openerActorNumber)
{
// 여기 코드는 "연 사람"에게만 실행됨
Debug.Log("[ItemBox] This client opened the box -> show personal popup / give item");
// TODO: UIManager.ShowItemPopup(...)
// TODO: Inventory.AddRandomItem(...)
}
}
//가구 열림 스프라이트 관련해서 추가..?
}
}
2. 시체 오브젝트 및 신고 UI 구현
- 캡슐 모양 오브젝트로 시체(DeadBody) 프리팹 생성
- 플레이어가 시체 근처로 다가가면 “신고(E)” 텍스트 UI 표시
- E키 상호작용 시 시체 제거 동기화, 중복 신고 방지도 포함.
- 캡슐 오브젝트에 Capsule Collider 2D, Photon View, Dead Body 스크립트 컴포넌트로 붙임.
- 캔버스 render mode : world space

- DeadBody.cs 코드
using UnityEngine;
using Photon.Realtime;
using Photon.Pun;
//시체 오브젝트
//가까이 가면 신고(E) UI 표시
//E 눌러서 한번만 신고
//신고되면 방 전체에서 시체 사라짐
public class DeadBody : MonoBehaviourPun, IInteractable
{
[Header("UI")]
[SerializeField] private GameObject reportUI; //신고(E)(report) UI
// 이미 신고됐는지(중복 신고 방지)
private bool reported;
private void Awake()
{
// 시작할 때 UI는 꺼두기
if (reportUI != null)
{
reportUI.SetActive(false);
}
}
//가까이 있을 때 UI 키기
public void ShowUI(bool show)
{
if(reported) return;
if(reportUI!=null)
reportUI.SetActive(show);
}
//E키 눌렀을 때 신고 처리(PlayerInteraction에서 호출)
public void Interact(Player interactor)
{
//이미 신고됐으면 무시
if(reported) return;
//신고한 사람 ActorNumber 저장
int reporterActorNumber = -1;
if(interactor != null)
{
reporterActorNumber = interactor.ActorNumber;
}
//방장이면 바로 전체에 신고 처리 적용
if (PhotonNetwork.IsMasterClient)
{
photonView.RPC(nameof(RPCReport), RpcTarget.All, reporterActorNumber);
}
else
{
//방장이 아니면 방장에게 신고 요청
photonView.RPC(nameof(RequestReport), RpcTarget.MasterClient, reporterActorNumber);
}
}
//방장 실행: 신고 요청 받고 -> 확정 -> 전체 적용 + destroy
[PunRPC]
private void RequestReport(int reporterActorNumber)
{
if(!PhotonNetwork.IsMasterClient) return;
if (reported) return;
//전체에게 신고 처리 적용
photonView.RPC(nameof(RPCReport), RpcTarget.All, reporterActorNumber);
}
//신고 요청
[PunRPC]
private void RPCReport(int reporterActorNumber)
{
//중복 호출 방지
if(reported) return;
//신고 확정
reported = true;
//ui 끄기
ShowUI(false);
//디버그 로그 : 누가 신고했는지
Debug.Log("[DeadBody] Reported! reporterActorNumber = " + reporterActorNumber);
//Destroy는 여기서 방장만
if (PhotonNetwork.IsMasterClient)
PhotonNetwork.Destroy(gameObject);
//TODO: 여기서 "회의/투표 시작"을 연결하면 됩니다..!
//투표는 보통 방장만 시작시키고, 다른 클라는 UI만 열게 만드는 구조가 안정적이라네유..
//예) VoteManager.Instance.StartVote();
}
}
테스트 해봄.
3. 늦게 들어온 플레이어 프리팹이 생성되지 않는 문제 해결
- 테스트 씬에 PlayerSpawner 붙여 멀티플레이어 입장 테스트 진행
- 문제: 늦게 들어온 플레이어의 캐릭터 프리팹이 생성되지 않음
- 해결: NetworkManager의 씬 이동을 LoadScene → PhotonNetwork.LoadLevel로 변경
- 이유: Photon의 방 관련 콜백들을 처리하고 있는 도중에 SceneManager의 LoadScene이 강제로 씬 전환해버리니 문제가 생겼던 것. 반면 PhotonNetwork.LoadLevel은 Photon이 씬 로딩과 룸 상태를 네트워크 흐름에 맞춰 처리해줘서 이런 타이밍 꼬임이 줄어듦.
4. 늦게 들어온 플레이어 닉네임이 안 뜨는 문제 해결
- 문제: 늦게 들어온 유저/새로 로드된 씬에서 닉네임 UI가 비어있음

- 해결: 닉네임을 단순히 로컬에만 들고 있지 않고,→ 닉네임이 정해지는 순간 플레이어 프로퍼티(포톤이 관리하는 플레이어 상태 테이블)로 전파되며, 다른 클라이언트도 해당 값을 읽어 UI에 표시 가능
- PhotonNetwork.NickName + CustomProperties("nick")에 저장해서 룸 참가자들에게 공유되도록 처리
- ConnectSceneManager.cs 에 ApplyNicknameToPhoton(string nick) 함수 추가 → 닉네임 입력 끝났을 때(OnEndEdit), RoomItemUI.cs에서 참여하기 버튼 눌렀을 때(OnJoinButtonClicked) 에서 이 함수 호출하도록 변경
//닉네임 photon에 저장, 갱신 함수
private void ApplyNicknameToPhoton(string nick)
{
if(string.IsNullOrWhiteSpace(nick)) return;
PhotonNetwork.NickName = nick;
//Photon 서버 메모리 상의 플레이어 상태 테이블 담을 해시테이블 생성
var props = new ExitGames.Client.Photon.Hashtable();
//상태 테이블에 닉네임 저장
props["nick"] = nick;
//저장된 로컬 플레이어의 테이블을 photon 서버에 업로드/동기화
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
}
- 결과

깃에 올릴 땐 PlayerSpawner 제외하기.
1월 22일
Git 작업 로그 - 커밋 상세

### 1. feat: 플레이어 상호작용 시스템 코드 구현
1) 시체 상호작용 스크립트
- `Assets/Scripts/DeadBody.cs`
- `Assets/Scripts/DeadBody.cs.meta`
2) 상호작용 인터페이스
- `Assets/Scripts/IInteractable.cs`
- `Assets/Scripts/IInteractable.cs.meta`
3) 아이템 박스 상호작용 스크립트
- `Assets/Scripts/ItemBox.cs`
- `Assets/Scripts/ItemBox.cs.meta`
4) 플레이어 상호작용 입력/감지 스크립트
- `Assets/Scripts/PlayerInteraction.cs`
- `Assets/Scripts/PlayerInteraction.cs.meta`
### 2. feat: 시체 및 아이템 박스 상호작용 프리팹 추가
1) 시체 상호작용 프리팹
- `Assets/Prefabs/DeadBody.prefab`
- `Assets/Prefabs/DeadBody.prefab.meta`
2) 아이템 박스 상호작용 프리팹
- `Assets/Prefabs/ItemBox.prefab`
- `Assets/Prefabs/ItemBox.prefab.meta`
### 3. feat: 플레이어 프리팹에 상호작용 컴포넌트 추가
- `Assets/Prefabs/Player.prefab`
- `Assets/Prefabs/Player.prefab.meta`
### 4. fix: 상호작용 시스템 연동을 위한 네트워크 로직 수정
- `Assets/Scripts/NetworkManager.cs`
- `Assets/Scripts/NetworkManager.cs.meta`
- `Assets/Scripts/ConnectSceneManager.cs`
- `Assets/Scripts/ConnectSceneManager.cs.meta`
'개인 프로젝트 > BlackOut' 카테고리의 다른 글
| [Unity] 게임 프로젝트 작업 기록 : 접속씬에서 비공개방 만들기 (0) | 2026.02.26 |
|---|---|
| [Unity] 3주차 작업 로그 : 시야 연출 (Sight System) (0) | 2026.02.25 |
| [Unity] 1주차 작업 로그 : 타이틀 & 접속 씬 구현 (0) | 2026.02.25 |
| [Unity] 게임 프로젝트 작업 기록 : Photon PUN2 기반 접속씬 구현 (1) | 2026.02.24 |
