본문 바로가기

Unity 개발 공부

[내배캠] 본캠 57 pun2 공부4

구조물 랜덤생성 및 페인팅, 점수 계산 관련 로직.

왜 중립 오브젝트(Neutral Object)를 쓰는가?
Ownership 고정(Fixed)
플레이어가 아닌 “씬(Scene)” 소유로 만들어 두면,
그 오브젝트에 대해 TransferOwnership·RequestOwnership 같은 호출이 절대 일어나지 않습니다.
MasterClient 전환 안전
Photon 룸의 MasterClient가 바뀌면(방장이 나갈 때 등),
만약 Surface가 단순 PhotonNetwork.Instantiate 로 MasterClient 소유로 생성되었다면 →
새 방장이 되기 전까지 그 오브젝트에 대한 제어·RPC가 불가능해집니다.
반면, 신(Scene) 소유로 만든 씬 객체는 방장이 바뀌어도 그대로 남아 있고,
누구나 Paint RPC를 ReliableOrdered로 호출해도 잘 동작합니다.

중립오브젝트 스폰 로직

// SurfaceManager.cs
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class SurfaceManager : MonoBehaviourPunCallbacks
{
    [Tooltip("Scene에서 배열로 지정해 둔 10개의 스폰 가능한 위치")]
    public Transform[] spawnPoints;

    [Tooltip("PaintSurface 프리팹 (PhotonView 포함)")]
    public GameObject surfacePrefab;

    private const int NUM_SURFACES = 10;

    public override void OnJoinedRoom()
    {
        // 마스터만 스폰
        if (!PhotonNetwork.IsMasterClient) return;

        for (int i = 0; i < NUM_SURFACES; i++)
        {
            // 랜덤 위치 선택
            Transform sp = spawnPoints[Random.Range(0, spawnPoints.Length)];
            // Scene Object로 모두에게 동기화해서 생성
            PhotonNetwork.InstantiateSceneObject(
                surfacePrefab.name,
                sp.position,
                sp.rotation,
                0,      // 그룹
                null    // 커스텀 프로퍼티
            );
        }
    }
}

Prefab 세팅:
surfacePrefab에 PhotonView 컴포넌트 추가
Ownership Transfer는 Fixed (Scene 소유)
Observed Components에는 없음 (자체 RPC로 Paint만 처리)

Paint 로직
// Surface.cs
using UnityEngine;
using Photon.Pun;
using System.Collections.Generic;

public class Surface : MonoBehaviourPun
{
    // 0…100 범위
    public float paintA = 0f;
    public float paintB = 0f;

    // UI 프로그레스바 참조 (A, B, Empty)
    public UnityEngine.UI.Slider barA, barB, barEmpty;

    // 한번 충돌 시 채울 퍼센트
    public float paintPerHit = 10f;

    // 충돌 시 호출 (총알, 그래플 등)
    public void OnPaintHit(int teamId)
    {
        // ReliableOrdered RPC 로만 쏘세요
        photonView.RPC(
          nameof(RPC_AddPaint),
          RpcTarget.AllBuffered,
          teamId,
          paintPerHit
        );
    }

    [PunRPC]
    void RPC_AddPaint(int teamId, float amount)
    {
        // 현재 빈 영역
        float empty = 100f - (paintA + paintB);

        if (teamId == 0)
        {
            // A팀
            float fill   = Mathf.Min(empty, amount);
            float steal  = Mathf.Min(Mathf.Max(0f, amount - fill), paintB);
            paintA += amount;
            paintB -= steal;
        }
        else
        {
            // B팀
            float fill   = Mathf.Min(empty, amount);
            float steal  = Mathf.Min(Mathf.Max(0f, amount - fill), paintA);
            paintB += amount;
            paintA -= steal;
        }

        // Clamp 0…100
        paintA = Mathf.Clamp(paintA, 0f, 100f);
        paintB = Mathf.Clamp(paintB, 0f, 100f);

        // UI 갱신
        float total = paintA + paintB;
        barA.value      = (total > 0f ? paintA / total : 0f);
        barB.value      = (total > 0f ? paintB / total : 0f);
        barEmpty.value  = (100f - total) / 100f;
    }
}
RpcTarget.AllBuffered:
패치된 델타를 나중에 들어온 플레이어에게도 자동으로 실행
teamId: 0 = A팀, 1 = B팀
UI: 각 Surface 별로 3개의 Slider를 두고 비율(0…1)을 설정

점수 집계& 게임매니저
// GameManager.cs
using UnityEngine;
using Photon.Pun;
using System.Collections;

public class GameManager : MonoBehaviourPunCallbacks
{
    public float matchDuration = 180f; // 3분
    private float timer;

    private int scoreA = 0, scoreB = 0;

    void Start()
    {
        timer = matchDuration;
        StartCoroutine(MatchTimer());
    }

    IEnumerator MatchTimer()
    {
        while (timer > 0f)
        {
            timer -= Time.deltaTime;
            yield return null;
        }
        // 타임 업 → 점수 집계
        if (PhotonNetwork.IsMasterClient)
            CalculateScores();
        // 종료 UI 등
    }

    void CalculateScores()
    {
        // Scene에 있는 모든 Surface 스크립트 탐색
        foreach (var s in FindObjectsOfType<Surface>())
        {
            if (s.paintA > s.paintB) scoreA++;
            else if (s.paintB > s.paintA) scoreB++;
            // 무승부는 점수 없음
        }

        // 결과를 모두에게 알리기
        photonView.RPC(
          nameof(RPC_ShowResult),
          RpcTarget.All,
          scoreA, scoreB
        );
    }

    [PunRPC]
    void RPC_ShowResult(int a, int b)
    {
        Debug.Log($"최종 스코어 A:{a} vs B:{b}");
        // 여기서 승패 UI 띄우기
    }
}

----------------------






----------------------


오브젝트 풀링을 네트워크 동기화 하는방법
먼저 게임오브젝트를 활성화/비활성화하는 [PunRPC] 함수

public class PoolScript : MonoBehaviourPun
{
 [PunRPC]
 void SetActiveRPC(bool b)
 {
  gameObject.SetActive(b);
 }
}

Resources의 PhotonView가 있는 생성할 오브젝트에 넣음, 거리 3이상시 순간이동하도록 만듬.

그다음 
ObjectProoler라고 실제로 풀링에 필요한 함수 로직들을 짜놓은 클래스도 필요하다.
public class ObjectPooler : MonoBehaviourPun
{
 [System.Serializable] // 클래스안에 해당 직렬화된 클래스는 우리가 풀링할 오브젝트의 맴버들을 가진다.
 public class Pool
 {
  public string tag; //프리팹이름태그
  public GameObject prefab; //실제 프리팹
  public int size; // 생성할 사이즈(몇개?)
 }

 public static ObjetPooler OP;
 void Awake() => OP = this; // 전역으로 접근가능하도록 처리

 public List<Pool> pools; // 앞선 직렬화클래스 인스턴스를 담은 리스트
 public Dictionary<string, Queue<GameObject>> poolDictionary; // 이름태그와, gameObject를 담을 큐를 키와 값으로 가지는 딕셔너리

//방 참가시 미리만들어 놓아 비활성화 시켜놓는 메서드. 플레이어마다 한번 호출하며, 인스펙터에 넣은
ObjectPooler대로 생성된다.
public void PrePoolInstantiate()
{
 poolDictionary = new Dictionary<string, Queue<GameObject>>();
 foreach (Pool pool in pools)
{
  Queue<GameObject> objectPool = new Queue<GameObject>();
  for( int i = 0; i< pool.size; i++)
  {
   GameObject obj = PhotonNetwork.Instantiate(pool.tag, Vector3.zero, Quaternion.identity); // 해당 string Pool.tag는 Resources폴더에 있는 해당 프리팹의 이름과 같아야함.
   obj.GetComponent<PhotonView>().RPC("SetActiveRPC", RpcTarget.All, false);
   objectPool.Enqueue(obj);
  }
}
}
//poolDictionary를 string 키와 Queue<GameObject> 값으로 구성되게
하나 새로 인스턴스를 만들고 기존에 만들었던 Pool들을 담음 Pools 리스트를  
직렬화된 Pool 클래스로 인스펙터창에서 만들었을것이다.
그럼 foreach문으로 해당 Pools리스트를 순회하여서 pool마다
objectPool 큐를 새로 만들어놓은다음에,
pool사이즈만큼 이중for문을 돌아서
pool.tag를 네트워크 동기화로 생성해서 obj 에 넣어준다.
해당 obj에 접근해서 RPC로 SetActiveRPC를 false로 모두를 대상으로 브로드캐스트해준다.
그다음에 만든 objectPool 큐에 해당 obj를 pool.size만큼 넣어주는 행동을 반복하고
objectPool 큐의 경우는 pool의 갯수만큼 생성된다.
생성된 objectPool 큐의 경우 해당 pool.tag와 대응되도록 
poolDictionary에 키와 값으로 추가해준다. 

그다음에 실제 생성로직은 비활성화된 것들을 꺼내주는 함수를 만들어놓는걸로 한다.
딕셔너리에서 이름에 맞는 오브젝트르 꺼내고 활성화후 위치와 회전을 대입하고 다음 큐에 다시 넣는동작을한다.

public GameObject PoolInstantiate(string tag, Vector3 position, Quaternion rotation)
{
  if(!poolDictionary.ContainsKey(tag))
  {
    Debug.LogWarning($"Poolwith tag {tag} doesn't excist.");
    return  null;
  }
  GameObject obj = poolDictionary[tag].Dequeue(); //선입으로 Enqueue햇던 큐에서 바로 다시 선출로 꺼내준 녀석
  obj.GetComponent<PhotonView>().RPC("SetActiveRPC", RpcTarget.All, true); // 활성화를 전체인원에게 브로드케스트
  obj.transform.position = position; //위치 받아서 잡고
  obj.transform.rotation = rotation; //로테이션 받아서 잡고
  poolDictioanry[tag].Enqueue(obj); // 딕셔너리의 해당 태그에 있는 큐에다가 다시 obj를 집어넣어줌.
 
  reutrn obj; //해당 위치와 로테이션이 보정된 obj를 반환해서 쓴다.
}
//파괴 로직은 특정 obj를 넣어주면 비활성화
public void PoolDestroy(GameObject obj)
{
 obj.GetComponent<PhotonView>().RPC("SetActiveRPC", RpcTarget.All, false);
}

실제 사용은

using static OBjectPooler;

public class NetworkManager : MonoBehaviourPunCallbacks
{
 public Gameobject Curobj;
 
 void Update()
{
 if(!PhotonNetowrk.InRoom) return;
 if(Input.GetKeyDown(KeyCode.Alpha1))
 OP.PrePoolInstantiate();
 else if (Input.GetKeyDown(KeyCode.Alpha2))
 CurObj = OP.PoolInstantitate("Portion", Vector3.zero, Quaternion.identity);
 else if(Input.GetKeyDown(KeyCode.Alpha3))
 OP.PoolDestroy(CurObj));
}
...

전체흐름을 보자면 Enqueue로  Pool(이 Pool은 하나의 아이템의 종류를 지칭한다, 총알, 포션 등등... 마음대로자유롭게 프리팹을만들어서씀)
을 Pools 리스트에서 꺼내어 size만큼 복제해서 담아주고.
필요할때는 Dequeue해서 활성화하고 위치,로테이션 보정해주고 다시 담는다. 
그렇다면 예를들면 열개라면 가장 앞에있던 비활성화된 녀석이 빠져나와서 활성화되고 맨끝으로 다시 담긴샘이다.
다쓰면 PoolDestory로 해당 꺼냈던 녀석을 특정해서 비활성화해줄테니까
계속해서 리볼버의 카트리지가 돌아가듯이 선입선출구조가 이루어지는것이다. 앞에것부터
하나씩 빠지면서 활성화된상태로 들어가고, 다시 꺼주는 일련의 행위를 3개의 메서드로 풀링을 만들어서 해내는것이다.
여기서 켜주고 꺼줄때마다 RPC로 브로드캐스트한다.