오늘은 마지막! 어제 만들어서 공개했던 "파리 플라이트"를 Photon Cloud에 맞춰서 개조하는 작업을 할겁니다.
(어제의 작업은 사실상 불필요했던 작업이지만, 제 머리속이 복잡해서 정리차원에서 만든 프로토타입으로 이해를...)
(한편 마음 속깊이에서는 "고급코딩 기술 알려줬음 됐어." 하는 의미부여도... 쿨럭...)

중요한 메소드별 정리는 4회 마지막회차에서 정리드리도록 하고 어제 포스팅한 http://www.wolfpack.pe.kr/800 과 비교바랍니다.

먼저 어제 보셨던 MainGame이 어떻게 변화되었는지 주목해주세요.

using UnityEngine;
using System.Collections;

public class MainGame : MonoBehaviour
{

    #region 이하 Photon 때문에 삭제!
    //public GameObject Player1;
    //public GameObject enemy;
    #endregion

    private Vector3[] GenPoint = new Vector3[]{
        new Vector3(-4.5f, 10f, 0f), 
        new Vector3(-3f, 10f, 0f), 
        new Vector3(-1.5f, 10f, 0f),
        new Vector3(0f, 10f, 0f),
        new Vector3(1.5f, 10f, 0f),
        new Vector3(3f, 10f, 0f),
        new Vector3(4.5f, 10f, 0f)};

    private float GenStart = 0f;
    private float GenTerm = 3f;
    private float DropSpeed = 5f;



	// Use this for initialization
	void Start () {
        GenStart = Time.time;
        //이하 Photon으로 인한 삭제 
        //Instantiate(Player1, new Vector3(0f, 0f, 0f), Quaternion.Euler(new Vector3(0f,0f,180f)));
	}
	
	// Update is called once per frame
	void Update () {
        //이하 Photon으로 인한 변화
        if (PhotonNetwork.isMasterClient == true ) EnemyGen();
	}

    void EnemyGen()
    {
        if (GenStart + GenTerm > Time.time) return;
        
        //GenTerm -= 0.01f;
        DropSpeed += 0.02f;
        StartCoroutine("EnemyDrop");
        GenStart = Time.time;

    }

    IEnumerator EnemyDrop()
    {
        foreach (Vector3 genposition in GenPoint)
        {
            //이하 Photon으로 인한 변화
           GameObject gobj = PhotonNetwork.InstantiateSceneObject("enemy", genposition, Quaternion.identity, 9, null) as GameObject;
           if (gobj.GetComponent<Enemy>() != null) gobj.GetComponent<Enemy>().Set_Speed(DropSpeed);
        }
        return null;
    }
}

중요한 몇가지 변화가 있었습니다. 기존에는 Prefab을 생성할때 GameObject에 할당하고 호출했지만,

Prefab이라는 폴더명을 "Resources"라는 이름으로 변경함으로써 공통 Resource로 할당한 뒤
PhotonNetwork.InstantiateSceneObject("enemy", genposition, Quaternion.identity, 9, null)

와 같이 string(문자열)로 호출하였습니다.
(위의 Method는 Scene에 종속된 Object를 생성하는 것 입니다. =) 상세설명은 4회차에서 차근차근 설명하죠.)

나머지 소스도 확인해보겠습니다.
Enemy의 경우는 다음과 같이 " IEnumerator Die()"이 아주 소소하게 변화되었습니다.

using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour {


    private float Dropspeed = 0f;


	// Update is called once per frame
	void Update () {
        Dropit();
        CheckAutoDie();
	}

    void Dropit()
    {
        transform.Translate(new Vector3(0f, -1f, 0f) * Dropspeed * Time.deltaTime);
    }

    void CheckAutoDie()
    {
        if (transform.position.y > -5f) return;
        StartCoroutine("Die");
    }

    public void Set_Speed(float x)
    {
        Dropspeed = x;
    }

    void OnTriggerEnter(Collider col)
    {
        if (col.tag != "beam") return;
        StartCoroutine("Die"); 
    }

    IEnumerator Die()
    {
       //바로 여기!! 여기가 바뀌었습니다. 
        if (PhotonNetwork.isMasterClient) PhotonNetwork.Destroy(gameObject);
        //if (!PhotonNetwork.isMasterClient) Destroy(gameObject);
        return null;
    }
}

역시 4회차에서 상세히 설명하겠습니다. 단지 하나 중요한 개념은 MasterClient라는 개념인데, 이녀석은 요 강좌 말미에 다시 말씀드리죠.

Beam(총알)의 경우는 변화가 없습니다.


using UnityEngine;
using System.Collections;

public class Beam : MonoBehaviour {

    private float speed = 10f;

	// Update is called once per frame
	void Update () {
        Moving();
        AutoDie();
	}

    void Moving()
    {
        transform.Translate(Vector3.up * speed * Time.deltaTime);
    }

    void AutoDie()
    {
        if (transform.position.y < 6f) return;
        StartCoroutine("Die");
    }

    IEnumerator Die()
    {
        Destroy(gameObject);
        return null;
    }
}


우리의 친구인 Player의 경우 약간 변화가 많습니다.

using UnityEngine;
using System.Collections;

public class Player : Photon.MonoBehaviour {

    public GameObject beam;
    
    private float fire_last = 0f;
    private float fire_term = 0.3f;
    private float Speed = 10f;
    
    //이하 네트워크로 인한 추가로 반드시 내 클라이언트는 내가 조작할 수 있도록 해주는 Augment값입니다.
    public bool isControl = false;

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
        Fire();
        if (isControl != true) return; //상당히 중요합니다. 내가 통제권이 없다면 조작을 못하도록 막는 역할을 합니다.
        if (Input.GetKey(KeyCode.RightArrow)) Move_Right();
        if (Input.GetKey(KeyCode.LeftArrow)) Move_Left();
	}

    void Move_Right()
    {
        transform.Translate(Vector3.left * Speed * Time.deltaTime);
        if (transform.position.x > 5f) transform.position = new Vector3(5f, 0f, 0f);
    }

    void Move_Left()
    {
        transform.Translate(Vector3.right * Speed * Time.deltaTime);
        if (transform.position.x < -5f) transform.position = new Vector3(-5f, 0f, 0f);
    }

    void Fire()
    {
        if (fire_last + fire_term > Time.time) return;
        StartCoroutine("Fire_Beam");
        fire_last = Time.time;
    }

    IEnumerator Fire_Beam()
    {
        Instantiate(beam, transform.position, Quaternion.identity);
        return null;
    }

    //기존에 OnCollisionEnter에서 TriggerEnter로 약간 변화를 주었습니다.
    void OnTriggerEnter(Collider col)
    {
        if (col.transform.tag != "enemy") return;
        PhotonNetwork.Destroy(gameObject); //없앨때 그냥 Destory가 아니라 PhotonNetwork.Destroy입니다.
    }

    //통제권을 할당하는 부분입니다. 호출되어야 하므로 당근 public으로 선언합니다.
    public void Set_isControl(bool x)
    {
        isControl = x;
    }
}



지금부터는 새로 추가된 녀석을 보도록 하겠습니다.
새로 추가한 녀석이 2넘 있는데, 이 녀석들이야 말로 Photon Network를 제대로 작동시키는 녀석들이라 중요합니다.

추가된 녀석중에 1회에서 보았던 서버에 로그인하고 로비에서 다른 방으로 들어가는 녀석인 Master_Join 보겠습니다.
조금 많이 추가됐지만, 눈으로 그냥 한번 흩어 본다는 생각으로 봐주세요.
역시 상세한 설명은 마지막회차에서 설명드리겠습니다.


using UnityEngine;
using System.Collections;

public class Master_Join : Photon.MonoBehaviour
{
    public static int playerWhoIsIt = 0;
    private static PhotonView ScenePhotonView;

	// Use this for initialization
	void Start () {
        PhotonNetwork.ConnectUsingSettings("0.1"); //아시죠? 서버 로그인 부분
        ScenePhotonView = this.GetComponent<PhotonView>(); //이녀석은 씬을 관리하는 PhotonView입니다.
	}

    //로비에 접속됐을 때 호출되는 메소드로 아무런 방이나 기어들어갑니다.
    void OnJoinedLobby()
    {
        PhotonNetwork.JoinRandomRoom();
    }
   //들어갈 방이 없으면 4인 Room으로 방을 만듭니다.
    void OnPhotonRandomJoinFailed()
    {
        PhotonNetwork.CreateRoom(null, true, true, 4); 
    }

    //방에 들어가면 MasterClient인걸 확인하고 Player_1 또는 Player_2를 생성하니다.
    void OnJoinedRoom()
    {
        if (PhotonNetwork.playerList.Length == 1) playerWhoIsIt = PhotonNetwork.player.ID;
        string player = (PhotonNetwork.isMasterClient == true) ? "Player_1" : "Player_2";
        if (Gen(player) != null) StartCoroutine(Gen(player));
    }

     //플레이어가 들어오면 플레이어에게 ID를 Tagging합니다. 보너스 메소드
    void OnPhotonPlayerConnected(PhotonPlayer player)
    {
        if (PhotonNetwork.isMasterClient) TagPlayer(playerWhoIsIt);
    }

    public static void TagPlayer(int playerID)
    {
        ScenePhotonView.RPC("TaggedPlayer", PhotonTargets.All, playerID);
    }

    //PhotonNetwork에 player를 생성합니다.
    IEnumerator Gen(string player)
    {
        PhotonNetwork.Instantiate(player, new Vector3(0f, 0f, 0f), Quaternion.Euler(new Vector3(0f, 0f, 180f)), 1);
        return null;
    }

    [RPC]
    void TaggedPlayer(int playerID)
    {
        playerWhoIsIt = playerID;
    }

    //플레이어가 나가게되면 MasterClient인지 확인후 다른 플레이어를 MasterClient로 설정합니다.
    void OnPhotonPlayerDisconnected(PhotonPlayer player)
    {
        Debug.Log("Disconnected: " + player);

        if (PhotonNetwork.isMasterClient)
        {
            if (player.ID == playerWhoIsIt)
            {
                TagPlayer(PhotonNetwork.player.ID);
            }
        }
    }

   //MasterClient가 바뀌게되면 호출되는 녀석인데 거의 불필요합니다.
    void OnMasterClientSwitched()
    {
        Debug.Log("OnMasterClientSwitched");
    }
}


여기까지는 누차 말씀드리지만 1회에서 보았던 바대로 서버에 로그인하고 로비에서 방까지 이동하는 로직입니다.
실재 Object들의 움직임이나 상태를 Sync하는 로직은 다음의 NetworkObj입니다.

using UnityEngine;
using System.Collections;

public class NetworkObj : Photon.MonoBehaviour {
    //싱크될 Position과 Rotation을 저장할 초기값입니다.
    private Vector3 correctPlayerPos = Vector3.zero;
    private Quaternion correctPlayerRot = Quaternion.identity;

    //AWAKE 네트워크로 인한 추가
    void Awake()
    {
        //만약 Player라면 PhotonView가 내 클라이언트인지 확인하고 제어권을 True또는 False로 만듭니다.
        //Player에서 만든 Set_isControl이 여기서 사용됩니다.
        if (GetComponent<Player>() != null) GetComponent<Player>().Set_isControl(photonView.isMine);
        //만약 Enemy라면 Player들과 중첩되어 생성되지 않도록 보다 윗쪽으로 초기호되게 합니다.
        if (GetComponent<Enemy>() != null) correctPlayerPos = new Vector3(0f, 10f, 0f);
    }

    void Update()
    {
        //여기서 중요한건 내 Object가 아니라 Network상의 다른 Object의 Position과 rotation을 업데이트 시키는 겁니다.
        if (photonView.isMine == true) return;
        //원래 위치와 업데이트된 위치가 2f이상 차이가 나면 즉시 반영하고 아니라면 부드럽게 움직이도록 설정합니다.
        float distance = Vector3.Distance(transform.position, this.correctPlayerPos);
        if (distance < 2f)
        {
            transform.position = Vector3.Lerp(transform.position, this.correctPlayerPos, Time.deltaTime * 5);
            transform.rotation = Quaternion.Lerp(transform.rotation, this.correctPlayerRot, Time.deltaTime * 5);
        }
        else
        {
            transform.position = this.correctPlayerPos;
            transform.rotation = this.correctPlayerRot;
        }
    }

    //데이터를 Sync할 때 호출되는 Method입니다.
    void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.isWriting)
        {
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            this.correctPlayerPos = (Vector3)stream.ReceiveNext();
            this.correctPlayerRot = (Quaternion)stream.ReceiveNext();
        }
    }
}


위 코드의 결과는 다음과 같습니다. =)
오고가는 Sync때문에 두개의 클라이언트에 뜨는 Object는 상이할 수 있습니다.

오늘은 MasterClient 개념을 설명하고 다음에 기회를 봐서 상세한 설명이 들어가는 대망의 4회차를 진행할께요.

Exitgames의 Photon제품군은 크게 2개로 나눠져 있습니다.
1. Photon Server : 자체적으로 개발팀에서 운영해야 하며 100 CCU가 무료입니다.
2. Photon Cloud : 20CCU가 무료이며 별도의 서버가 필요하지 않습니다.

다시말해, Photon Cloud와 Server는 기능상 약간 다르지만 가장 큰 차이가 바로 서버를 내가 사서 관리하느냐, ExitGame에서 빌려서 쓰냐의 차이입니다.

상황이 이렇다보니 원래 Photon Server에서 제공되던 Server Logic 제공이 불가능해졌습니다.
이 글을 읽는 개발자 모두에게서 위와 같은 느낌이 밀려올거라는 상상이 드는 군욤. ㅋㅋ

그래서, 우리의 Exitgames 창업자이자 CTO인 Christof Wegmann 아저씨께서 만든 개념이 MasterClient입니다.
P2P개념에서 Master Client와 혼동하시면 아니됩니다.


쉽게 정리하자면 전문용어로 "방장"입니다.
이미지화 해보면 이런 정도?
그럼 방장은 뭐하는 녀석이냐?
방을 만들고 처음 들어간 넘이 "방장(Master Client)" 권한을 획득하는데, 서버에서 하는 역할 모두를 할 수 있습니다.


즉, 기존에 서버프로그래머가 헉헉 대면서 개발했던 부분을 이제 클라이언트 개발자가 해야 한다는 불편한 진실을 담고 있죠. 네...
음... 뭐 원래 이바닥이 원래 이런 중요한 정보는 나중에 말해주죠.
실컷 정보줘서 공부시킨담에 빼도 박도 못할때...

그리고 또 하나의 문제가 있습니다. Photon Server의 경우는 서버를 자체운영하다보니 Database연동 기능도 담고 있지만, Photon Cloud는 그런거 없습니다.
진짜 없습니다.
이쯤 되면 아마도 짱돌 날라 올 듯 싶기도 하지만, 어쨌든 없으니 잇몸으로 씹어야 겠죠?
그래서, 저희 회사에서 동원하는건 Microsoft사의 Azure입니다.

일전에 Posting했던 "남벌" Prototype의 경우 Azure위에 올라간 ASP MVC에 MSSQL에 붙여서 사용했죠.

다시말해 Master Client는 방안에서 일어나는 모든 정보를 파악하고 있다가 게임이 종료되면 비동기 방식으로 서버에 저장합니다.
뭐... 이런건 서버개발자나 웹개발자에게 술사줘가며 시키던가...
아님 이제부터 공부하셔야 겠죠. ㅋㅋㅋ

아니면 사장님 졸라서 Photon Server 사시고, 서버 2대 이상 사신담에 Clustering하시어 L4장비에 방화벽 장비 붙여서 IDC센터에 입고하는 방법도 있습니다.
그리고, 서버관리자는 3교대 24시간 서버 확인하는 수 밖에 없으니 추가로 3명의 인력을 더 뽑으셔야 겠네욤 ㅜㅜ

오늘은 더 진행하면 짱돌날라올테니 일단 몸부터 피했다가,
차회에 마지막 소스 공개와 함깨 Photon Cloud의 주요 변수를 확인해 보도록 하겠습니다. =)

* 마지막 부분에 틀린 정보를 드렸습니다.
Photon Server의 모듈을 사용해서 서버부분을 개발하고나면 Azure, 아마존 등의 Cloud에 올려서 서비스 할 수 있다고 하는군욤!



2012/11/29 14:50 2012/11/29 14:50

트랙백 주소 :: 이 글에는 트랙백을 보낼 수 없습니다