본문 바로가기

- GameProgramming/- Unity 3D

★ 14. 포톤 클라우드 (Photon Cloud) Marco Polo 튜토리얼 [3/3]

반응형
Marco Polo 튜토리얼

지난번 [2/3]에 이어 [3/3]을 작성해보겠습니다.


# 애니메이션 추가하기

문제가 좀 있습니다. 쉽게 애니메이션 상태를 알 수가 없습니다. 여러 몬스터들이 달리고 혼합되는 것들에 대해 동기화 할 수 있는 다른 방법을 찾아봐야합니다.

MyThirdPersonController 클래스를 통해 문제를 해결할 수 있습니다. 이 클래스는 _characterState 변수를 모든 애니메이션 트리거를 위해 사용하고 몬스터의 CharacterController.velocity 정보에 기반하고 있습니다. 로컬에서 작동하지만 리모트 복사에는 velocity 를 가지고 있지 않습니다. 단순하게 위치를 재조정 하는 것입니다.

만약 몬스터가 자신의 상태를 전송한다면 복사본은 수신되는 값으로 적용되어야합니다. 사전에 그 컨트롤러를 사용할 수 있도록 했다면 잘 동작할 것입니다.

myThirdPersonController 르르 사용하려면 원격 인스턴스에도 활성화 되어야하고 "원격 몬스터들"의 입력을 무시할 다른 방법이 필요합니다. isControllable 변수가 적당합니다. 아직 설정된 적이 없으며 항상 설정되지 않아야 합니다. 한번 해보겠습니다.

유니티의 표준 작업으로 "monsterprefab" 에 myThirdPersonController 를 추가합니다. 애니메이션이 없어질 것이므로 각각의 필드에 애니메이션을 드롭하여 추가합니다. 프리팹의 Transform(프리팹의 가장 최상단에 위치)을 "Buik" 로 드래그 앤 드롭하세요.



더이상 CharacterControl과 CharacterCamera를 사용하지 않으므로 RandomMatchmaker.OnJoinedRoom 에서 enabling-code를 제거하세요. 두 컴포넌트들은 이제 비활성 상태(제거될 수도 있습니다)가 되었습니다. 대신 RandomMatchmaker에서 몬스터를 생성할 때 isControllable 를 설정합니다.

Code Example C#:
1
2
3
4
5
public override void OnJoinedRoom()
{
    GameObject monster = PhotonNetwork.Instantiate("monsterprefab", Vector3.zero, Quaternion.identity, 0);
    monster.GetComponent<myThirdPersonController>().isControllable = true;
}
cs

오류가 뜨는게 당연합니다 아직 코드 수정하지 않았기 때문입니다.
myThirdPersonController 코드를 열고 _characterState 를 찾아 public 으로 isControllable 도 마찬가지로 public 으로 하고 값을 false 로 설정해야합니다. 입력이 사용되는 모든 곳을 찾아 isControllable 를 체크하고 우리 몬스터가 아닌 입력 값을 무시해야합니다. 계속 읽어보세요.

가능한 것에 대해서만 입력된 축을 읽습니다 (myThirdPersonController 에서)

1
2
var v = Input.GetAxisRaw("Vertical");
var h = Input.GetAxisRaw("Horizontal");
cs
위의 코드를 찾아 아래와 같이 수정합니다.

Code Example C#:
1
2
3
4
5
6
7
8
var v = 0;
var h = 0;
 
if (isControllable)
{
    v = Input.GetAxisRaw("Vertical");
    h = Input.GetAxisRaw("Horizontal");
}
cs

isControllable이 아닌 경우에 "firing" 을 막습니다.

Code Example C#:
1
2
3
4
5
6
if (is Controllable && Input.GetButton("Fire1"))
{
    GetComponent.<Animation>()[attackPoseAnimation.name].AddMixingTransform(buik);
    GetComponent.<Animation>().CrossFade(attackPoseAnimation.name, 0.2);
    GetComponent.<Animation>().CrossFadeQueued(idleAnimation.name, 1.0);
}
cs

isControllable 이 아니면 아무도 점프할 수 없습니다. Update 함수에서 수정해주세요.

Code Example C#:
1
2
3
4
if (isControllable && Input.GetButtonDown ("Jump"))
{
    lastJumpButtonTime = Time.time;
}
cs

노트 : 원시 스크립트는 Update 의 첫번째 부분에서 입력을 리셋하고 있었습니다. 그 코드를 제거해주시기 바랍니다. 여러 스크립트를 수행하였으며 어떤 것이 처음 실행되는 지에 대해서는 신경을 쓰지 않습니다. Input을 계속 리셋한다면 어디에선가 잃어버리게 됩니다.

이제 얼마나 애니메이션이 적용되었는지 살펴보겠습니다. "idle" 을 제외하고 현재 애니메이션은 상태에 의해 설정되고 페이드인 됩니다. 소리도 괜찮습니다.

원시 스크립트에서 아이들(idle) 애니메이션은 상태를 설정하지 않지만 동기화를 위해서 사용하려고합니다. 이것을 추가하기 위해서 Update() 에서 "controller.velocity.sqrMagnitude < 0.5" 를 찾아 상태를 설정합니다. 리모트 복사본들이 사용자 제어로부터 상태를 받은 것이므로 isControllable 의 값을 체크해야합니다.

캐릭터가 제어할 수 없는 것이라면 속도와 애니메이션을 적용할 것입니다. 아래 코드는 Update 메소드의 변경 내용입니다.

Code Example C#:
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
if(this.isControllable && controller.velocity.sqrMagnitude < 0.5)
{
    _animation.CrossFade(idleAnimation.name);
    this._characterState = CharacterState.Idle;
}
else 
{
    if (_characterState == CharacterState.Idle)
    {
        _animation.CrossFade(idleAnimation.name);
    }
    else if(_characterState == CharacterState.Running) {
        _animation[runAnimation.name].speed = runMaxAnimationSpeed;
        if (isControllable) _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, runMaxAnimationSpeed);
        _animation.CrossFade(runAnimation.name);    
    }
    else if(_characterState == CharacterState.Trotting) {
        _animation[walkAnimation.name].speed = trotMaxAnimationSpeed;
        if (isControllable) _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, trotMaxAnimationSpeed);
        _animation.CrossFade(walkAnimation.name);    
    }
    else if(_characterState == CharacterState.Walking) {
        _animation[walkAnimation.name].speed = walkMaxAnimationSpeed;
        if (isControllable) _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, walkMaxAnimationSpeed);
        _animation.CrossFade(walkAnimation.name);    
    }
}
cs

오리지널 스크립트가 가속도에 의해서 애니메이션 속도를 변경하는 반면에 우리는 다른 플레이어의 몬스터들을 그렇게 처리할 수 없습니다. 이 파트를 건너 뛰고 만족할만한 애니메이션 속도를 적용하세요. 더 잘 처리되도록 할 수 있지만 이 경우에서는 이것만으로도 충분합니다.

잊어버린 것은 없나요? characterState 를 전송하려고 계획하였으나 아직 처리하지 않았습니다.
NetworkCharacter 를 변경해보겠습니다.

Code Example C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    if (stream.isWriting)
    {
        // We own this player: send the others our data
        stream.SendNext(transform.position);
        stream.SendNext(transform.rotation);
 
        myThirdPersonController myC = GetComponent<myThirdPersonController>();
        stream.SendNext((int)myC._characterState);
    }
    else
    {
        // Network player, receive data
        this.correctPlayerPos = (Vector3)stream.ReceiveNext();
        this.correctPlayerRot = (Quaternion)stream.ReceiveNext();
 
        myThirdPersonController myC = GetComponent<myThirdPersonController>();
        myC._characterState = (CharacterState)stream.ReceiveNext();
    }
}
cs

이 버전에서 마침내 애니메이션이 동기화되고 움직임이 부드러워졌습니다.
에디터와 스탠드얼론에서 얼마나 동일한 애니메이션을 보여주는지 주의해서 확인하시기 바랍니다.


# 외치고 응답하기

몬스터들이 대화를 할 수 있다면 정말 멋질 것입니다. 직접 녹음하거나 글을 음성으로 변경해주는 신디사이저를 웹에서 검색할 수도 있습니다. 아무튼 두 개의 오디오 클립을 가져오겠습니다.
http://www.fromtexttospeech.com/ 링크에서 원하는 문장을 입력해 다운로드 받으실 수 있습니다.

PhotonView 에는 "Remote Procedure Call" 의 약어인 "RPC" 라고 하는 메소드가 있습니다. 이 의미는 또 다른 메소드를 호출한다는 것입니다.

RPC의 장점은 : 동일한 룸 내의 같은 게임 오브젝트의 다른 클라이언트 메소드를 호출하는 것입니다. 편의상 "this" 클라이언트의 메소드라고 부르기도 합니다.

PhotonView.RPC() 메소드로 호출할 수 있는 것은 PunRPC 속성을 가진 메소드입니다. 코드에서는 [PunRPC] (and@PunRPC in UnityScript) 입니다.

Unity 5.1 에서는 RPC 특성을 구식으로 취급하고 있기 때문에 나중에 삭제될 것임이 분명합니다. 따라서 PUN v1.56과 새로운 버전에서는 'PunRPC' 라는 다른 속성을 사용하고 있습니다. RPC와 "Remote Procedure Call" 은 다른 모든 사용에는 영향을 주지 않습니다.

좋습니다. RPC 메소드를 사용하여 쉽게 몬스터들이 고함치도록 만들 수 있습니다. 로컬과 다른 플레이어의 클라이언트에서 "Marco Polo" 폴더 안에 "AudioRpc" 라는 스크립트를 생성합니다. PunRPC 속성을 가진 "Marco" 와 "Polo" 메소드를 추가합니다. 그리고 public AudioClip 필드를 추가하여 사운드 파일을 참조할 수 있도록 합니다.

Code Example C#:
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
using UnityEngine;
 
public class AudioRpc : MonoBehaviour
{
    public AudioClip marco;
    public AudioClip polo;
 
    [PunRPC]
    void Marco()
    {
        Debug.Log("Marco");
 
        this.GetComponent<AudioSource>().clip = marco;
        this.GetComponent<AudioSource>().Play();
    }
 
    [PunRPC]
    void Polo()
    {
        Debug.Log("Polo");
 
        this.GetComponent<AudioSource>().clip = polo;
        this.GetComponent<AudioSource>().Play();
    }
}
cs

레퍼런스에 "AudioRpc" 를 "monsterprefab" 에 추가하고 아까 만들어둔 사운드 파일 2개를 적용시켜주세요. 또한 프리팹에 오디오 소스 컴포넌트를 추가해야 합니다 "AudioSource" 컴포넌트를 추가해주세요.

편의상 RandomMatchmaker GUI 에 버튼들을 추가할 수 있으며 이것을 통해 RPC를 호출할 수 있습니다. 아직 몬스터의 PhotonView 에는 접근하지 않았습니다. 몬스터가 생성되었을 때 잡아서 저장할 수 있습니다. 수정된 RandomMatchmaker 은 다음과 같습니다.

Code Example C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public override void OnJoinedRoom()
{
    GameObject monster = PhotonNetwork.Instantiate("monsterprefab", Vector3.zero, Quaternion.identity, 0);
    monster.GetComponent<myThirdPersonController>().isControllable = true;
    myPhotonView = monster.GetComponent<PhotonView>();
}
 
void OnGUI()
{
    GUILayout.Label(PhotonNetwork.connectionStateDetailed.ToString());
 
    if (PhotonNetwork.connectionStateDetailed == PeerState.Joined)
    {
        if (GUILayout.Button("Marco!"))
        {
            this.myPhotonView.RPC("marco", PhotonTargets.All);
        }
        if (GUILayout.Button("Polo!"))
        {
            this.myPhotonView.RPC("Polo", PhotonTargets.All);
        }
    }
}
cs

다른 PhotonTargets 옵션이 있다는 것에 주의하세요. 로컬에서도 사운드를 재생하기 위해 현재는 "All" 을 사용하고 있습니다. 두 개의 클라이언트를 실행시키고 있다면 둘 중 아무 버튼이나 눌러보세요. 하지만 음성이 너무 희미하여 거의 들리지 않을 수 있습니다. 카메라를 이동시키거나 볼륨을 키워보시면 소리가 들릴 것입니다. 두 개의 클라이언트 모두 소리를 끝냅니다. "remote" 플레이어는 약간 지연되어 재생됩니다. (네트워크 지연과 연관되어 있습니다.)

테스트할 동안에만 차이점이 있지만 백그라운드에서 RPC 오디오를 disable 해봅니다. AudioRPC 의 OnApplicationFocus 를 구현하여 앱이 포커스를 잃었을 때 disable 시킵니다. 그리고 테스트를 다시 해봅시다. 에코는 아직 발생합니다.

RPC 가 스크립트에서 disable 되어 있어도 역시 호출됩니다. 참 이해가 가지 않는 부분이지만 대부분은 좋은 기능입니다. 스크립트에서 RPC 를 스킵하려면 enabled 를 체크하세요. "Polo" 파트에 대해서는 다음과 같습니다.

Code Example C#:
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
31
32
[PunRPC]
void Marco()
{
    if (!this.enabled)
    {
        return;
    }
 
    Debug.Log("Marco");
 
    this.GetComponent<AudioSource>().clip = marco;
    this.GetComponent<AudioSource>().Play();
}
 
void OnApplicationFocus(bool focus)
{
    this.enabled = focus;
}
 
[PunRPC]
void Polo()
{
    if (!this.enabled)
    {
        return;
    }
 
    Debug.Log("Polo");
 
    this.GetComponent<AudioSource>().clip = polo;
    this.GetComponent<AudioSource>().Play();
}
cs


# "It" 전환

지금까지 호출하고 응답할 수 있었습니다. 기본적인 게임 로직에서 누락된 것은 누가 "it" 이고 다른 플레이어를 태그하는 것입니다. 게임 로직의 첫 부분에 대해서는 "Marco Polo" 폴더에 "GameLogic" C# 스크립트를 생성하고 "Scripts" 객체를 추가하였습니다.

룸에 들어간 첫 번째 플레이어는 "it" 입니다. OnJoinedRoom 이 호출되므로 룸에 입장 하였는지 알 수 있으며
PhotonNetwork.playerList 를 통하여 우리가 첫 번째 플레이어인지 쉽게 파악할 수 있습니다.


# 플레이어 ID

Photon은 플레이어 "ID" 또는 "playerNumber" 를 사용하여 룸 안의 각 플레이어를 표기합니다. 정수로서 상대적으로 데이터 크기가 작습니다. 다시 재할당 될 수 없으므로 룸 안에서 플레이어 ID를 통해 대화할 수 있습니다.

로컬 클라이언트의 ID 는 PhotonNetwork.player.ID 에 저장됩니다. 만약 혼자 있다면 이 값을 정적(static) 필드인 "playerWhoIsIt" 에 저장할 것이며 다른 스크립트에서도 쉽게 접근할 수 있습니다.

Code Example C#:
1
2
3
4
5
6
7
8
9
10
11
12
public static int playerWhoIsIt;
 
void OnJoinedRoom()
{
    // game logic: if this is the only player, we're "it"
    if (PhotonNetwork.playerList.Length == 1)
    {
        playerWhoIsIt = PhotonNetwork.player.ID;
    }
 
    Debug.Log("playerWhoIsIt: " + playerWhoIsIt);
}
cs

더 많은 플레이어들이 참가할 때 플레이어들에게 누가 "it"인지 알려주어야합니다. PUN은 플레이어가 방에 참가했을 때 OnPhotonPlayerConnected 를 호출하기 때문에 이에 반응하는 것은 쉽습니다.

"TaggedPlayer" 라는 메소드를 생성하여 "playerWhoIsIt"을 설정하고 RPC로 만들어 이미 게임에 있는 플레이어들이 호출할 수 있도록 합니다.

Code Example C#:
1
2
3
4
5
6
[PunRPC]
void TaggedPlayer(int playerID)
{
    playerWhoIsIt = playerID;
    Debug.log("TaggedPlayer: " + playerID);
}
cs


# 씬의 PhotonView

이미 RPC 를 준비 해놓았으나 PhotonView 의 게임 오브젝트에 추가되지 않으면 호출할 수 없습니다. 논리적으로는 누가 "it" 인지 몬스터가 기억할 필요가 없으니 다른 PhotonView 를 추가해봅시다.

이번에는 씬에 있는 "scripts" 객체에 PhotonView 를 추가합니다. 씬 계층의 일부분으로 소유자는 "Scene" 일 것입니다. 쉽게 찾을 수 있으며 모든 플레이어에 의해 항사아 사용될 수 있습니다. GameLogic 과 PhotonView 는 동일한 게임 오브젝트에 있으므로 씬의 PhotonView 에서 GetComponent 를 이용하여 RPC 를 호출할 수 있습니다.

한 플레이어가 새로운 플레이어를 업데이트 하는 것으로 충분합니다. Unity Networking 에서는 호스트가 없으나 PUN 에서는 호스트를 대체할 수 있는 것이 있습니다. "마스터 클라이언트"는 룸 안에서 가장 낮은 번호를 가진 플레이어입니다. 모든 클라이언트는 PhotonNetwork.isMasterClient 로 현재 시점에서 마스터 클라이언트인지 체크할 수 있습니다.

OnPhotonPlayerConnected() 를 추가하고 정적 메소드 "TagPlayer" 를 GameLogic 스크립트에 추가합니다.
"TagPlayer" 는 어떤 곳에서도 쉽게 호출할 수 있습니다. 이 메소드는 나중에 필요합니다. 이 부분의 코드는 다음과 같습니다.

Code Example C#:
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
private static PhotonView ScenePhotonView;
 
void Start()
{
    ScenePhotonView = this.GetComponent<PhotonView>();
}
 
void OnPhotonPlayerConnected(PhotonPlayer player)
{
    Debug.Log("OnPhotonPlayerConnected: " + player);
 
    // when new players join, we send "who's it" to let them know
    // only one player will do this: the "master"
 
    if (PhotonNetwork.isMasterClient)
    {
        TagPlayer(playerWhoIsIt);
    }
}
 
public static void TagPlayer(int playerID)
{
    Debug.Log("TagPlayer: " + playerID);
    ScenePhotonView.RPC("TaggedPlayer", PhotonTargets.All, playerID);
}
cs


# 떠난 그 이후

플레이어들은 언제라도 방을 떠날 수 있으며 이 방식으로 "찾고 있는" 플레이어를 잃어 버릴 수도 있습니다.

PUN은 플레이어가 방을 나가면 OnPhotonPlayerDisconnected 를 호출해줍니다. 방을 나간 플레이어가 "it" 인지의 여부를 체크하고 새로운 플레이어로 대체해주어야 합니다. 이 작업은 한 명의 플레이어인 마스터에 의해서만 수행됩니다.

Code Example C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
void OnPhotonPlayerDisconnected(PhotonPlayer player)
{
    Debug.Log("OnPhotonPlayerDisconnected: " + player);
 
    if (PhotonNetwork.isMasterClient)
    {
        if (player.ID == playerWhoIsIt)
        {
            // if the player who left was "it", the "master" is the new "it"
            TagPlayer(PhotonNetwork.player.ID);
        }
    }
}
cs


# 때리기

다른 몬스터들을 태그할 방법이 필요합니다. 콜라이더(Collider)와는 별도로 플레이어가 다른 몬스터를 클릭했는지 체크할 것입니다. 클릭된 몬스터가 "it"이 될 예정입니다.

"Marco Polo" 폴더에 "ClickDetector" 라는 C# 스크립트를 생성하고 "monsterprefab" 으로 드래그합니다. 그 안에서 Update 메소드를 사용하여 플레이어의 턴이면 마우스 클릭을 체크 합니다. 클릭된 객체가 있으면 그 객체를 얻고 플레인 또는 내 몬스터가 아닌지 확인합니다.

이제 어떤 것이 클릭되었는지 알기 때문에 PhotonView 에서 클릭된 것을 얻습니다. 그 몬스터를 어떤 플레이어가 소유하고 있는지 알기 때문에 태그하기 위하여 플레이어 ID 를 사용할 수 있습니다.

Code Example C#:
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
31
32
33
34
35
36
using System;
using UnityEngine;
 
public class ClickDetector : MonoBehaviour
{
    void Update()
    {
        // if this player is not "it", the player can't tag anyone, so don't do anything on collision
        if (PhotonNetwork.player.ID != GameLogic.playerWhoIsIt)
        {
            return;
        }
 
        if (Input.GetButton("Fire1"))
        {
            GameObject goPointedAt = RaycastObject(Input.mousePosition);
 
            if (goPointedAt != null && goPointedAt != this.gameObject && !goPointedAt.name.Equals("Plane", StringComparison.OrdinalIgnoreCase))
            {
                PhotonView rootView = goPointedAt.transform.root.GetComponent<PhotonView>();
                GameLogic.TagPlayer(rootView.owner.ID);
            }
        }
    }
 
    private GameObject RaycastObject(Vector2 screenPos)
    {
        RaycastHit info;
        if (Physics.Raycast(Camera.mainCamera.ScreenPointToRay(screenPos), out info, 200))
        {
            return info.collider.gameObject;
        }
 
        return null;
    }
}
cs

다른 플레이어를 태그하기 위해 정적(Static) TagPlayer 메소드를 사용하는 것은 매우 쉽습니다.

"Marco" 와 "Polo" GUI 버튼 둘다 모든 플레이어가 볼 수 있습니다. "playerWhoIsIt" 을 체크하여 쉽게 변경할 수 있습니다. 우리가 "it" 이기 때문에 "Marco" 만 보입니다. RandomMatchmaker 는 다음과 같이 변경되어야 합니다.

Code Example C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void OnGUI()
{
    GUILayout.Label(PhotonNetwork.connectionStateDetailed.ToString());
 
    if (PhotonNetwork.connectionStateDetailed == PeerState.Joined)
    {
        bool shoutMarco = GameLogic.playerWhoIsIt == PhotonNetwork.player.ID;
 
        if (shoutMarco && GUILayout.Button("Marco!"))
        {
            this.myPhotonView.RPC("Marco", PhotonTargets.All);
        }
        if (!shoutMarco && GUILayout.Button("Polo!"))
        {
            this.myPhotonView.RPC("Polo", PhotonTargets.All);
        }
    // and so on...
cs


# 결론

이제 간단한 게임에 대한 작은 프로토 타입을 가지게 되었습니다. 물론 많은 부분이 없기는 하지만 기본적인 것은 모두 다루었습니다. 우리 게임은 무작위 룸에서 플레이어를 찾고 위치를 동기화하며 이동을 부드럽게 하는 것은 물론 게임 로직까지 있습니다. Player ID, "마스터 클라이언트", PUN 네트워킹 메시지 등에 대해서 학습했습니다.

이제 "it" 이 아닌 플레이어들을 숨김처리, RPC를 통한 "attack" 동기화 또는 누군가 맞았다면 다음 태그를 지연하기 등을 구현하는 것이 더이상 두려운 것이 아닐 것입니다. 또한 캐릭터, 카메라를 어떻게 제어하는지 알게되었으며 더이상 어려운 사항이 아닐 것입니다.

당신의 몬스터로 공격해보세요!



Marco Polo 튜토리얼을 마치겠습니다.


참고한 사이트 : https://doc.photonengine.com/ko-kr/pun/current/tutorials/tutorial-marco-polo



반응형