Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

목록분류 전체보기 (36)

2026_05_27 강의 요약본 1. 네트워크 변수네트워크로 동기화되는 변수. 선언 방법은 다음과 같다.public class TempController : NetworkBehaviour{ // 네트워크에서 동기화되는 변수 NetworkVariable randNum = new NetworkVariable(1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); // 괄호 안 = Default 값, 읽을 수 있는 권한 범위, 쓸 수 있는 권한 범위} NetworkVariable으로 선언한다.권한을 직접 지정하지 않는 경우, 읽기는 Everyone, 쓰기는 Server(Host)가 기본으로 세팅된다. * 자료형은 기..... 더보기
2026_05_26 강의 요약본 NetworkManager세팅틱 레이트, 플레이어 프리팹 등록 Player세팅- NetworkObject- NetworkTransformAuthority Mode: 플레이어 오브젝트는 사용자가 좌표를 이동시키므로 Owner을 사용한다. - 이동 코드using UnityEngine;// 네트워크 라이브러리using Unity.Netcode;public class TempController : NetworkBehaviour // MonoBehaviour -> NetworkBehaviour{ [SerializeField] private float speed = 3; void Update() { if(!IsOwner) return; // 주인이 아니라..... 더보기
2026_03_06 강의 요약본 1. Vroid (+Blend Tree)2. 뒤끝 베이직 세팅3. 포톤 세팅 1. Vroid 3D 캐릭터를 쉽게 뽑을 수 있는 프로그램.완전 무료이고, 직접 꾸민 캐릭터는 만든 사람에게 저작권이 귀속된다. 다운로드https://vroid.com/en/studio 체형, 얼굴, 헤어 등 전부 커스텀이 가능하고, 기본적으로 텍스쳐를 수정하는 방식으로 꾸미기 때문에 허접해 보일 수 있다.다만 블렌더로 모델을 가져와 추가적으로 수정할 수 있기 때문에 충분히 보완할 수 있다. vrm으로 파일을 export하면 된다.이 파일은 유니티에서 정상적으로 읽히려면 https://github.com/vrm-c/UniVRM/releases/tag/v0.131.0 Release v0.131.0..... 더보기

김산나

2026_05_27 강의 요약본

 

1. 네트워크 변수

네트워크로 동기화되는 변수. 선언 방법은 다음과 같다.

public class TempController : NetworkBehaviour
{
	// 네트워크에서 동기화되는 변수
 	NetworkVariable<int> randNum = new NetworkVariable<int>(1,
 	NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
	// 괄호 안 = Default 값, 읽을 수 있는 권한 범위, 쓸 수 있는 권한 범위
}

 

NetworkVariable<자료형>으로 선언한다.

권한을 직접 지정하지 않는 경우, 읽기는 Everyone, 쓰기는 Server(Host)가 기본으로 세팅된다.

 

* 자료형은 기본 형식(숫자), 벡터, 색상, 레이, enum...값을 바로 넣는 타입의 변수들, 구조체, 길이가 고정된 string을 전달할 수 있다. - FixedString32Bytes ~ FixedString4096Bytes까지.

 

    void Update()
    {        
        Debug.Log($"owner id: "+ OwnerClientId + "\nRandom number: "+ randNum.Value);

        if(!IsOwner) return; // 주인이 아니라면 하단 코드 실행 금지


        // T키를 입력 시 랜덤 숫자 추첨
        if(Input.GetKeyDown(KeyCode.T))
        {
            randNum.Value = Random.Range(0, 100);
        }     
    }

 

테스트 코드를 작성한다.

T키를 누르면 랜덤 숫자를 추첨한다.

 

권한 설정이 기본인 읽기는 Everyone, 쓰기는 Server인 경우, 호스트를 제외한 유저는 번호 추첨을 할 수 없다.

하지만 권한을 수정하여 NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner로 변경한다면

모든 유저가 수정한 것을 확인할 수 있다.

 

네트워크 변수는 서버의 유일한 변수를 생성하는 것이 아닌, 유저마다 다른 값을 갖는다.

 

플레이어 1은 39를 추첨했고, 플레이어 2는 35를 추첨했다.

 

* 네트워크 구조체

public class TempController : NetworkBehaviour
{

    struct SomeData : INetworkSerializable
    {
        public int someInt;
        public bool someBool;

        public void NetworkSerialize<T> (BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref someInt);
            serializer.SerializeValue(ref someBool);
        }
    }
    NetworkVariable<SomeData> randNum = new NetworkVariable<SomeData>(new SomeData { someInt = 1, someBool = false },
    NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
    // 괄호 안 = Default 값, 읽을 수 있는 권한 범위, 쓸 수 있는 권한 범위

    void Update()
    {
        Debug.Log("owner id: "+ OwnerClientId + "\nRandom number: "+
        randNum.Value.someInt + "\nBool: " + randNum.Value.someBool);

        if(!IsOwner) return;
        if(Input.GetKeyDown(KeyCode.T))
        {
            randNum.Value = new SomeData { someInt = Random.Range(0, 100), someBool = true };
        }   
    }


}

 

 

 

 

플레이어 1, 2 둘 다 정상적으로 찍히는 걸 볼 수 있다.

 

 

* 네트워크 문자열 변수

    // 네트워크 변수 - String
    NetworkVariable<FixedString32Bytes> netString = new NetworkVariable<FixedString32Bytes>
    ("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);

 

...

void Update()
    {
        Debug.Log("String: " + netString.Value + "\nowner id: "+ OwnerClientId + "\nRandom number: "+
        randNum.Value.someInt + "\nBool: " + randNum.Value.someBool);
        
        ...

        if(Input.GetKeyDown(KeyCode.R))
        {
            netString.Value = "가나다라마바사";
        }
        
    }

 

 

 

2. RPC(Remote Procedure Call)

Host, Client가 다른 유저의 요소를 호출하기 위한 기술. 사용법은 다음과 같다.

 

    [Rpc(SendTo.Server)]
    private void TestServerRPC(int number, string msg, RpcParams rpcParams = default)
    {
        Debug.Log("called Owner ID" + OwnerClientId + "Received number " + number + "and message: " + msg
        + "sender client ID: " + rpcParams.Receive.SenderClientId);
    }

[Rpc(SendTo.대상)] ... 메서드 작성 ...

 

void Update()
{
        if(Input.GetKeyDown(KeyCode.R))
        {
            TestServerRPC(Random.Range(0, 100), "Hello Server!", new RpcParams());
        }
}

 

테스트 코드 작성.

 

위 예시 코드는 Server(Host)로만 전송하는 RPC 예제이다. 특정 유저에게 보내려면 어떻게 해야 할까?

 

    [Rpc(SendTo.SpecifiedInParams)] // 파라미터로 받을 대상 직접 지정
    void TestClientRpc(RpcParams rpcParams = default)
    {
        Debug.Log("called Owner ID" + OwnerClientId + "sender client ID: " + rpcParams.Receive.SenderClientId);
    }

 

SendTo.(대상) 대상에 특정 파라미터를 받겠다고 선언하고, 매개변수로 파라미터 값을 받으면 된다.

 

        ...
        
        if(Input.GetKeyDown(KeyCode.Alpha1))
        {
            TestClientRpc(RpcTarget.Single(0, RpcTargetUse.Temp));
            // 0번을 타겟으로 하겠다.
        }
        if(Input.GetKeyDown(KeyCode.Alpha2))
        {
            TestClientRpc(RpcTarget.Single(1, RpcTargetUse.Temp));
            // 1번을 타겟으로 하겠다.
        }
        if(Input.GetKeyDown(KeyCode.Alpha3))
        {
            TestClientRpc(RpcTarget.Single(2, RpcTargetUse.Temp));
            // 2번을 타겟으로 하겠다.
        }
        
        ...

0번 - 호스트
2번 - 두 번째로 접속한 유저

 

3번 - 세 번째로 접속한 유저

 

<응용>

특정 키를 눌러 플레이어 자신의 색상을 랜덤하게 변경한다.

 

[Rpc(SendTo.Everyone)]
void TestChangeColorRPC(Color color, RpcParams rpcParams = default)
{
    transform.GetChild(0).GetComponent<Renderer>().material.color = color;
}

void Update()
    {
    
    if(!IsOwner) return;
    
    if(Input.GetKeyDown(KeyCode.Alpha1))
        {
            if(OwnerClientId == 0)
            {
                Color randomColor = Random.ColorHSV();
                TestChangeColorRPC(randomColor);
            }
        }
        if(Input.GetKeyDown(KeyCode.Alpha2))
        {
            if(OwnerClientId == 1)
            {
                Color randomColor = Random.ColorHSV();
                TestChangeColorRPC(randomColor);
            }
        }
        if(Input.GetKeyDown(KeyCode.Alpha3))
        {
            if(OwnerClientId == 2)
            {
                Color randomColor = Random.ColorHSV();
                TestChangeColorRPC(randomColor);
            }
        }
    
    }

 

* IsOwner 판정을 통해 플레이어 자신의 오브젝트만 제어하도록 한다.

* Alpha0 vs KeyPad0: 전자는 키보드 상단 키패드이고, 후자는 텐키이다.

* Random.ColorHSV(): 랜덤 컬러 함수

* transform.GetChild(0).GetComponent<Renderer>().material.color = color

자식의 색상을 변경하는 코드. (3D 오브젝트 기준) 플레이어 오브젝트 구조가 다음과 같아 이렇게 짠다.

 

결과

플레이어가 특정 버튼을 누를 때, 자신의 캐릭터 색상만 변경할 수 있다.

 

 

===========================================================

 

 

2026_05_26 강의 요약본

 

NetworkManager세팅

틱 레이트, 플레이어 프리팹 등록

 

Player세팅

- NetworkObject

- NetworkTransform

Authority Mode: 플레이어 오브젝트는 사용자가 좌표를 이동시키므로 Owner을 사용한다.

 

- 이동 코드

using UnityEngine;

// 네트워크 라이브러리
using Unity.Netcode;


public class TempController : NetworkBehaviour // MonoBehaviour -> NetworkBehaviour
{
    [SerializeField] private float speed = 3;

    void Update()
    {
        if(!IsOwner) return; // 주인이 아니라면 하단 코드 실행 금지

        float horInput = Input.GetAxis("Horizontal");
        float verInput = Input.GetAxis("Vertical");

        Vector3 movement = new Vector3(horInput, 0f, verInput) * speed * Time.deltaTime;
        transform.Translate(movement);
        
    }
}

 

MonoBehaviour을 NetworkBehaviour로 변경한다.

그리고 반복문 상단에 IsOwner로 플레이어를 구분한다.

 

* 네트워크 Host, Client 접속 코드

using Unity.Netcode;
using UnityEngine;

public class NetworkManagerUI : MonoBehaviour
{
    public void StartHost()
    {
        NetworkManager.Singleton.StartHost();
    }

    public void StartClient()
    {
        NetworkManager.Singleton.StartClient();
    }
}

 

테스트용 버튼 연결을 위한 함수.

네트워크 매니저의 싱글톤에 관련 코드가 있다.

 

NetworkManager.Singleton.StartHost();

NetworkManager.Singleton.StartClient();

 

 

 

===========================================================

 

멀티게임 복습 시작!

 

2026_03_06 강의 요약본

 

1. Vroid (+Blend Tree)

2. 뒤끝 베이직 세팅

3. 포톤 세팅

 

1. Vroid

 

3D 캐릭터를 쉽게 뽑을 수 있는 프로그램.

완전 무료이고, 직접 꾸민 캐릭터는 만든 사람에게 저작권이 귀속된다.

 

다운로드

https://vroid.com/en/studio

 

체형, 얼굴, 헤어 등 전부 커스텀이 가능하고, 기본적으로 텍스쳐를 수정하는 방식으로 꾸미기 때문에 허접해 보일 수 있다.

다만 블렌더로 모델을 가져와 추가적으로 수정할 수 있기 때문에 충분히 보완할 수 있다.

 

 

vrm으로 파일을 export하면 된다.

이 파일은 유니티에서 정상적으로 읽히려면

 

https://github.com/vrm-c/UniVRM/releases/tag/v0.131.0

 

Release v0.131.0 · vrm-c/UniVRM

Installation You can install UniVRM using the UnityPackage or the UPM Package. The UniVRM supports Unity 2022.3 LTS or later. UnityPackage Download the unitypackage, and drag and drop it to import ...

github.com

 

해당 패키지를 다운받으면 된다.

 

자체적인 툰 셰이더가 있는 것 같다.

 

 

이 캐릭터에 애니메이션을 주고 싶다.

이동의 경우에는 "속도"값을 기준으로 애니메이션을 적용하면 자연스러울 것 같다.

그때 사용되는 기능이 "Blend Tree"이다.

 

Speed 파라미터 생성. (float)

애니메이터 > 블렌드 트리 생성

더블클릭하면

이런 게 뜬다.

이걸 선택하면

이런 창이 뜨는데, Motion List의 +버튼을 눌러 조정할 수 있다.

속도값이 얼마일 때까지 저 모션을 출력하는지 설정한다.

 


2. 뒤끝 베이스 세팅

https://docs.backnd.com/sdk-docs/backend/base/start-up/

 

시작하기 | 뒤끝 개발자

SDK 연동 과정을 영상으로 확인하세요!

docs.backnd.com

 

해당 문서를 읽으면 된다.

SDK를 다운받고, 프로젝트를 생성하여 Client ID, Signature Key를 세팅값에 넣어주면 된다.

 

<코드>

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

// 뒤끝 SDK namespace 추가
using BackEnd;

public class BackendLogin
{
    private static BackendLogin _instance = null;

    public static BackendLogin Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BackendLogin();
            }

            return _instance;
        }
    }

    public void CustomSignUp(string id, string pw)
    {
        Debug.Log("회원가입을 요청합니다.");

        var bro = Backend.BMember.CustomSignUp(id, pw);

        if (bro.IsSuccess())
        {
            Debug.Log("회원가입에 성공했습니다. : " + bro);
        }
        else
        {
            Debug.LogError("회원가입에 실패했습니다. : " + bro);
        }
    }

    public void CustomLogin(string id, string pw)
    {
        Debug.Log("로그인을 요청합니다.");

        var bro = Backend.BMember.CustomLogin(id, pw);

        if (bro.IsSuccess())
        {
            Debug.Log("로그인이 성공했습니다. : " + bro);
        }
        else
        {
            Debug.LogError("로그인이 실패했습니다. : " + bro);
        }
    }

    public void UpdateNickname(string nickname)
    {
          Debug.Log("닉네임 변경을 요청합니다.");

        var bro = Backend.BMember.UpdateNickname(nickname);

        if (bro.IsSuccess())
        {
            Debug.Log("닉네임 변경에 성공했습니다 : " + bro);
        }
        else
        {
            Debug.LogError("닉네임 변경에 실패했습니다 : " + bro);
        }
    }
}

 

회원가입, 로그인, 닉변 세 가지 기능을 구현하는 코드.

나중에는 인폿값을 받아 직접 구현하면 된다.

 

using UnityEngine;

// 뒤끝 SDK namespace 추가
using BackEnd;

public class BackendManager : MonoBehaviour
{
    void Start()
    {
        var bro = Backend.Initialize(); // 뒤끝 초기화

        // 뒤끝 초기화에 대한 응답값
        if (bro.IsSuccess())
        {
            Debug.Log("초기화 성공 : " + bro); // 성공일 경우 statusCode 204 Success
        }
        else
        {
            Debug.LogError("초기화 실패 : " + bro); // 실패일 경우 statusCode 400대 에러 발생
        }

        Test();
    }

  void Test()
    {
        BackendLogin.Instance.CustomLogin("user1", "1234"); // [추가] 뒤끝 회원가입 함수
        BackendLogin.Instance.UpdateNickname("김산나"); // [추가] 뒤끝 닉네임 변경 함수
        Debug.Log("테스트를 종료합니다.");
    }
}

 

실제 서버와 연결하는 코드.

 


3. 포톤 세팅

포톤은 20명 동접까지 무료이다.

기본적으로 클라이언트끼리 연동하는 방식을 갖고 있다.

 

https://assetstore.unity.com/packages/tools/network/pun-2-free-119922?locale=ko-KR&srsltid=AfmBOopCuQBVL5uSi7cq6Rvw7w-a6JSGBbQAKOU6St9hyKvL932Ezxld

 

PUN 2 - FREE | 네트워크 | Unity Asset Store

Get the PUN 2 - FREE package from Photon Engine and speed up your game development process. Find this & other 네트워크 options on the Unity Asset Store.

assetstore.unity.com

먼저 포톤 전용 패키지를 다운받는다.

 

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

 

그 다음 포톤에 가입하여 프로젝트를 생성한다. 포톤 종류는 PUN, 멀티게임 선택

 

 

포톤 위자드 열기 (이미 열려있으면 거기서 진행)

 

Setup Project 선택

프로젝트의 ID부분을 클릭하면 복붙할 수 있게 되는데, 복사해서 AppID에 넣어주면 된다.

 

<서버 코드 (방 생성 및 플레이어 배치)>

더보기
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class PhotonManger : MonoBehaviourPunCallbacks
{
    [Header("Room Settings")]
    [SerializeField] private string roomName = "KatanaRoom";
    [SerializeField] private byte maxPlayers = 4;

    [Header("Player")]
    [SerializeField] private string playerPrefabName = "Player"; // Resources 폴더 내 프리팹 이름
    [SerializeField] private Transform spawnPoint;               // 없으면 원점 스폰

    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();
    }

    // 마스터 서버 연결 완료 → 로비 입장
    public override void OnConnectedToMaster()
    {
        Debug.Log("마스터 서버 연결됨. 로비 입장 중...");
        PhotonNetwork.JoinLobby();
    }

    // 로비 입장 완료 → 방 참가 또는 생성
    public override void OnJoinedLobby()
    {
        Debug.Log("로비 입장 완료. 방 참가 시도...");
        RoomOptions options = new()
        {
            MaxPlayers = maxPlayers,
            IsOpen = true,
            IsVisible = true
        };
        PhotonNetwork.JoinOrCreateRoom(roomName, options, TypedLobby.Default);
    }

    // 방 입장 완료 → 캐릭터 스폰
    public override void OnJoinedRoom()
    {
        Debug.Log($"방 입장 완료. 플레이어 수: {PhotonNetwork.CurrentRoom.PlayerCount}");
        SpawnPlayer();
    }

    // 방 생성 실패 시 재시도
    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Debug.LogWarning($"방 생성 실패: {message}. 재참가 시도...");
        PhotonNetwork.JoinOrCreateRoom(roomName, new RoomOptions { MaxPlayers = maxPlayers }, TypedLobby.Default);
    }

    // 방 참가 실패 시 새 방 생성
    public override void OnJoinRoomFailed(short returnCode, string message)
    {
        Debug.LogWarning($"방 참가 실패: {message}. 새 방 생성 중...");
        PhotonNetwork.CreateRoom(roomName, new RoomOptions { MaxPlayers = maxPlayers });
    }

    // 다른 플레이어 입장 로그
    public override void OnPlayerEnteredRoom(Photon.Realtime.Player newPlayer)
    {
        Debug.Log($"{newPlayer.NickName} 입장. 현재 인원: {PhotonNetwork.CurrentRoom.PlayerCount}");
    }

    // 다른 플레이어 퇴장 로그
    public override void OnPlayerLeftRoom(Photon.Realtime.Player otherPlayer)
    {
        Debug.Log($"{otherPlayer.NickName} 퇴장. 현재 인원: {PhotonNetwork.CurrentRoom.PlayerCount}");
    }

    // 연결 끊김 처리
    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.LogWarning($"연결 끊김: {cause}. 재연결 시도...");
        PhotonNetwork.ConnectUsingSettings();
    }

    private void SpawnPlayer()
    {
        Vector3 pos = spawnPoint != null ? spawnPoint.position : Vector3.zero;
        Quaternion rot = spawnPoint != null ? spawnPoint.rotation : Quaternion.identity;
        PhotonNetwork.Instantiate(playerPrefabName, pos, rot);
    }
}

<플레이어 움직임>

더보기
using Photon.Pun;
using UnityEngine;

public class Player : MonoBehaviourPun, IPunObservable
{
    [Header("Movement")]
    [SerializeField] private float walkSpeed = 3f;
    [SerializeField] private float runSpeed = 6f;
    [SerializeField] private float rotationSmoothTime = 0.1f;

    [Header("Animation")]
    [SerializeField] private float animationDampTime = 0.1f;

    [Header("Network Sync")]
    [SerializeField] private float positionLerpSpeed = 10f;
    [SerializeField] private float rotationLerpSpeed = 10f;

    private Animator animator;
    private Rigidbody rb;
    private Transform cameraTransform;

    private float vertical;
    private float horizontal;
    private bool isRunning;
    private float rotationVelocity;

    // 원격 플레이어 동기화용
    private Vector3 networkPosition;
    private Quaternion networkRotation;
    private float networkAnimSpeed;

    private static readonly int SpeedHash = Animator.StringToHash("Speed");

    void Awake()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true;
        rb.interpolation = RigidbodyInterpolation.Interpolate;

        networkPosition = transform.position;
        networkRotation = transform.rotation;

        // 내 캐릭터만 카메라 사용, 원격 플레이어는 물리 비활성화
        if (photonView.IsMine)
        {
            cameraTransform = Camera.main.transform;
        }
        else
        {
            rb.isKinematic = true;
        }
    }

    void Update()
    {
        if (photonView.IsMine)
        {
            HandleInput();
        }
        else
        {
            // 원격 플레이어: 수신한 위치/회전으로 보간
            transform.SetPositionAndRotation(
                Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * positionLerpSpeed),
                Quaternion.Lerp(transform.rotation, networkRotation, Time.deltaTime * rotationLerpSpeed)
            );
            animator.SetFloat(SpeedHash, networkAnimSpeed, animationDampTime, Time.deltaTime);
        }
    }

    void FixedUpdate()
    {
        if (!photonView.IsMine) return;

        Vector2 inputDir = new(horizontal, vertical);

        if (inputDir.sqrMagnitude < 0.01f)
        {
            rb.linearVelocity = new Vector3(0f, rb.linearVelocity.y, 0f);
            return;
        }

        // 카메라 기준 이동 방향 계산
        Vector3 camForward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up).normalized;
        Vector3 camRight = Vector3.ProjectOnPlane(cameraTransform.right, Vector3.up).normalized;
        Vector3 moveDir = (camForward * vertical + camRight * horizontal).normalized;

        // 이동 방향으로 캐릭터 부드럽게 회전
        float targetAngle = Mathf.Atan2(moveDir.x, moveDir.z) * Mathf.Rad2Deg;
        float smoothAngle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref rotationVelocity, rotationSmoothTime);
        rb.MoveRotation(Quaternion.Euler(0f, smoothAngle, 0f));

        // 이동
        float currentSpeed = isRunning ? runSpeed : walkSpeed;
        Vector3 velocity = moveDir * currentSpeed;
        rb.linearVelocity = new Vector3(velocity.x, rb.linearVelocity.y, velocity.z);
    }

    // Photon 직렬화: 위치/회전/애니메이션 동기화
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            // 내 데이터를 전송
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
            stream.SendNext(animator.GetFloat(SpeedHash));
        }
        else
        {
            // 원격 데이터 수신
            networkPosition = (Vector3)stream.ReceiveNext();
            networkRotation = (Quaternion)stream.ReceiveNext();
            networkAnimSpeed = (float)stream.ReceiveNext();
        }
    }

    private void HandleInput()
    {
        vertical = Input.GetAxisRaw("Vertical");
        horizontal = Input.GetAxisRaw("Horizontal");
        isRunning = Input.GetKey(KeyCode.LeftShift);

        float animSpeed = 0f;
        if (vertical != 0f || horizontal != 0f)
            animSpeed = isRunning ? runSpeed : walkSpeed;
        animator.SetFloat(SpeedHash, animSpeed, animationDampTime, Time.deltaTime);
    }
}

 

<카메라 세팅용 코드>

using Photon.Pun;
using Unity.Cinemachine;
using UnityEngine;

public class CameraConnector : MonoBehaviour
{
    [Tooltip("플레이어 프리팹 안의 CameraTarget 자식 이름")]
    [SerializeField] private string cameraTargetName = "CameraTarget";

    private CinemachineCamera cinemachineCamera;

    void Awake()
    {
        cinemachineCamera = GetComponent<CinemachineCamera>();
    }

    void Update()
    {
        if (cinemachineCamera.Follow != null) return;

        // 로컬 플레이어의 CameraTarget을 탐색 (연결되면 더 이상 실행 안 함)
        GameObject[] players = GameObject.FindGameObjectsWithTag("Player");
        foreach (GameObject player in players)
        {
            PhotonView pv = player.GetComponent<PhotonView>();
            if (pv == null || !pv.IsMine) continue;

            Transform target = player.transform.Find(cameraTargetName);
            if (target == null)
            {
                Debug.LogWarning($"[CameraConnector] '{cameraTargetName}' 자식을 찾을 수 없습니다.");
                return;
            }

            cinemachineCamera.Follow = target;
            cinemachineCamera.LookAt = target;
            Debug.Log("[CameraConnector] 카메라 타겟 연결 완료.");
            return;
        }
    }
}

 

 

 

플레이어 오브젝트 (vrm파일 오브젝트)에 플레이어 컨트롤러, 포톤 뷰, 트렌스폼 뷰, 애니메이터 뷰를 세팅한다.

참고로 애니메이터 뷰는 Disabled로 되어있을 시 작동을 하지 않는다.

 

카메라에 카메라 커넥터 코드를 연결하고, 플레이를 하면 정상 작동한다.

 


===========================================================

 

멀티께임을 만들 수 있게 됐다 !