유니티 포톤 플레이어 - yuniti poton peulleieo

Photon Pun 클라우드에서 탱크 동기화 작업을 위해 몇가지 스크립트 세팅을 더 해주었다.

1. TankMove 스크립트

using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; using UnityStandardAssets.Utility; public class TankMove : MonoBehaviourPun, IPunObservable { //탱크의 이동과 속도 public float moveSpeed = 20.0f; public float rotSpeed = 50.0f; //참조할 컴포넌트 private Rigidbody rbody; private Transform tr; //키보드 입력값 변수 private float h, v; private PhotonView pv = null; //포톤뷰 컴포넌트 public Transform camPivot; //메인 카메라가 추적할 CamPovot 게임 오브젝트 private Vector3 currPos = Vector3.zero; private Quaternion currRot = Quaternion.identity; // Start is called before the first frame update void Awake() { rbody = GetComponent<Rigidbody>(); tr = GetComponent<Transform>(); rbody.centerOfMass = new Vector3(0.0f, -0.5f, 0.0f); //Rigidbody의 무게 중심을 낮게 설정 pv = GetComponent<PhotonView>(); //PhotonView 컴포넌트 할당 pv.Synchronization = ViewSynchronization.UnreliableOnChange; //데이터 전송 타입 설정 pv.ObservedComponents[0] = this; //PhotonView Observed Components 속성에 TankMove 스크립트를 연결 if (pv.IsMine) //로컬이라면 { Camera.main.GetComponent<SmoothFollow>().target = camPivot; rbody.centerOfMass = new Vector3(0.0f, -0.5f, 0.0f); } else //원격 플레이어의 탱크는 물리력을 이용하지 않음 { rbody.isKinematic = true; } //원격 탱크의 위치, 회전 값을 처리할 변수의 초기값 설정 currPos = tr.position; currRot = tr.rotation; } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if(stream.IsWriting) //로컬 플레이어의 위치 정보 송신 { stream.SendNext(tr.position); stream.SendNext(tr.rotation); } else // 원격 플레이어의 위치 정보 수신 { currPos = (Vector3)stream.ReceiveNext(); currRot = (Quaternion)stream.ReceiveNext(); } } // Update is called once per frame void Update() { if (pv.IsMine) //로컬인 경우에 { h = Input.GetAxis("Horizontal"); v = Input.GetAxis("Vertical"); //회전과 이동 처리 tr.Rotate(Vector3.up * rotSpeed * h * Time.deltaTime); tr.Translate(Vector3.forward * v * moveSpeed * Time.deltaTime); } else //원격 플레이인 경우에 { //원격 플레이어의 탱크를 수신받은 위치까지 부드럽게 이동(선형 보간값을 처리할 때 Lerp) tr.position = Vector3.Lerp(tr.position, currPos, Time.deltaTime * 3.0f); //원격 플레이어의 탱크를 수신받은 각도까지 부드럽게 회전(각도 보간값을 처리할 때 Slerp) tr.rotation = Quaternion.Slerp(tr.rotation, currRot, Time.deltaTime * 3.0f); } } }

 IPunObservable 인터페이스를 상속 받으면 public void OnPhotonSerializeView 메서드가 추가로 생성된다. 기본적으로는 throw new System.NotImplementedException(); 예외를 던지는 코드가 생성된다.

2. Turret & Cannon Photon 동기화

Turret과 Cannon도 동기화를 시켜주기 위해 각각오브젝트에 Photon view 컴포넌트를 추가해주었다. Observed Components에 Transform을 드래그앤 드롭해주면 Photon Transform View 컴포넌트가 자동으로 추가된다. 동기화를 해주면 멀티 플레이 환경에서 이동, 조작 등이 서로 영향을 받지 않고 독립적으로 실행된다.

using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; public class TurretCtrl : MonoBehaviourPun, IPunObservable { private Transform tr; private RaycastHit hit; //Ray가 지면에 맞은 위치를 저장할 변수 public float rotSpeed = 5.0f; private PhotonView pv = null; private Quaternion currRot = Quaternion.identity; // Start is called before the first frame update void Awake() { tr = GetComponent<Transform>(); pv = GetComponent<PhotonView>(); //PhotonView 컴포넌트에 할당 pv.ObservedComponents[0] = this; //PhotonView의 Observed 속성을 이 스크립트로 지정 pv.Synchronization = ViewSynchronization.UnreliableOnChange; //Photon View의 동기화 속성을 설정 currRot = tr.localRotation; //초기 회전 값 설정 } // Update is called once per frame void Update() { if(pv.IsMine) { //메인 카메라에서 마우스 커서의 위치로 캐스팅되는 Ray를 생성 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //생선된 Ray를 Scene뷰에 녹색 광선으로 표현 Debug.DrawRay(ray.origin, ray.direction * 100.0f, Color.green); if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << 8)) { //Ray에 맞은 위치를 로컬좌표로 변환 Vector3 relative = tr.InverseTransformPoint(hit.point); //역탄젠트 함수인 Atan2로 두 점 간의 각도를 계산 float angle = Mathf.Atan2(relative.x, relative.z) * Mathf.Rad2Deg; //rotSpeed 변수에 지정된 속도로 회전 tr.Rotate(0, angle * Time.deltaTime * rotSpeed, 0); } } else { tr.localRotation = Quaternion.Slerp(tr.localRotation, currRot, Time.deltaTime * 3.0f); } } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if(stream.IsWriting) { stream.SendNext(tr.localRotation); } else { currRot = (Quaternion)stream.ReceiveNext(); } } }using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; public class CannonCtrl : MonoBehaviourPun, IPunObservable { private Transform tr; public float rotSpeed = 100.0f; private PhotonView pv = null; private Quaternion currRot = Quaternion.identity; //원격 플레이어 탱크의 포신 회전 각도를 저장할 변수 // Start is called before the first frame update void Awake() { tr = GetComponent<Transform>(); pv = GetComponent<PhotonView>(); pv.ObservedComponents[0] = this; pv.Synchronization = ViewSynchronization.UnreliableOnChange; currRot = tr.localRotation; } // Update is called once per frame void Update() { if(pv.IsMine) //자신의 탱크일 때만 조정 { float angle = -Input.GetAxis("Mouse ScrollWheel") * Time.deltaTime * rotSpeed; tr.Rotate(angle, 0, 0); } else //원격 플레이어인 경우 { //현재 각도에서 수신 받은 실시간 회전 각도로 부드럽게 회전 tr.localRotation = Quaternion.Slerp(tr.localRotation, currRot, Time.deltaTime * 3.0f); } } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if(stream.IsWriting) { stream.SendNext(tr.localRotation); } else { currRot = (Quaternion)stream.ReceiveNext(); } } }

3. 포 발사 동기화

using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; public class FireCannon : MonoBehaviourPun { public GameObject cannon = null; //Cannon 프리팹 public Transform firePos; //Cannon 발사 지점 private AudioClip fireSfx = null; private AudioSource sfx = null; private PhotonView pv = null; // Start is called before the first frame update void Awake() { cannon = (GameObject)Resources.Load("Cannon"); fireSfx = Resources.Load<AudioClip>("CannonFire"); sfx = GetComponent<AudioSource>(); pv = GetComponent<PhotonView>(); //Photon View 컴포넌트 할당 } // Update is called once per frame void Update() { //PhotonView가 자신의 것이고 마우스 왼쪽 버튼 클릭 시 발사 로직 수행 if (pv.IsMine && Input.GetMouseButtonDown(0)) { Fire(); //원격 네트워크 플레이어의 탱크에 RPC 원격으로 Fire 함수를 호출 pv.RPC("Fire", RpcTarget.Others, null); } } [PunRPC] void Fire() { sfx.PlayOneShot(fireSfx, 1.0f); Instantiate(cannon, firePos.position, firePos.rotation); } }

포 발사 스크립트의 경우 IPunObservable 인터페이스를 상속하지 않고 [PunRPC] 어트리뷰트를 가져와주었다.

4. 탱크 Health Bar 설정

적 포탄을 맞았을 때 탱크 체력이 깎이고 결국 Destroy되게 Head Up Display 세팅하였다. UI에서 캔버스를 가져오고 Panel 아래 Text와 체력바 이미지를 넣어주었다. Text는 초기에 플레이어가 지정한 User ID가 들어가고 HPbar는 Filled / Horizontal / Right 타입으로 세팅하여 피격 시 수평 초록색 바가 오른쪽에서 왼쪽으로 깎이도록 설정했다. 이미지는 World Space로 보이고 Scale과 Position을 한눈에 볼 수 있도록 세팅했다.

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class TankDamage : MonoBehaviour { private MeshRenderer[] renderers; private GameObject expEffect = null; //탱크 폭발효과 프리팹 private int initHp = 100; private int currHp = 0; public Canvas hudCanvas; //탱크 밑에 Canvas public Image hpBar; //Filled 타입의 Image UI 항목을 연결할 변수 // Start is called before the first frame update void Awake() { //탱크 폭파 후 투명 처리를 위한 메쉬렌더러 컴포넌트 배열 renderers = GetComponentsInChildren<MeshRenderer>(); currHp = initHp; //현재 생명치를 초기 생명치로 초기값 설정 expEffect = Resources.Load<GameObject>("Large Explosion"); //탱크 폭발 시 생성시킬 폭발 효과를 로드 hpBar.color = Color.green; //Filled 이미지 색상을 녹색으로 설정 } private void OnTriggerEnter(Collider other) { if (currHp > 0 && other.tag == "CANNON") { currHp -= 20; hpBar.fillAmount = (float)currHp / (float)initHp;//현재 생명치 백분율 //생명 수치에 따라 Filled 이미지의 색상을 변경 if (hpBar.fillAmount <= 4.0f) hpBar.color = Color.red; else if (hpBar.fillAmount <= 0.6f) hpBar.color = Color.yellow; if (currHp <= 0) { StartCoroutine(this.ExplosionTank()); } } } IEnumerator ExplosionTank() { //폭발효과 생성 Object effect = GameObject.Instantiate(expEffect, transform.position, Quaternion.identity); Destroy(effect, 3.0f); hudCanvas.enabled = false; //HUD 비활성화 SetTankVisible(false);//탱크 투명 처리 yield return new WaitForSeconds(3.0f); //3초 기다림 hpBar.fillAmount = 1.0f; hpBar.color = Color.green; hudCanvas.enabled = true; currHp = initHp; //리스폰 시 생명 초기값 설정 SetTankVisible(true); //탱크를 다시 보이게 처리 } void SetTankVisible(bool isVisible) //MeshRenderer를 활성/비활성화 하는 함수 { foreach (MeshRenderer _renderer in renderers) { _renderer.enabled = isVisible; } } }

탱크 오브젝트에 적용된 Tank Damage 스크립트이다. 기본 탱크 체력이 100이고 한 대 맞을 때 마다 20씩 감소하도록 설정했다. 5대 피격 후 Destroy되고 3초 후 리스폰 되도록 세팅했다. 또한 탱크 피격 시 Large Explosion 이펙트가 발생하도록 연출했다. 포탄 Cannon에 Tag를 CANNON으로 설정해 피격 오브젝트를 스크립트가 인식하도록 구현했다.

using System.Collections; using System.Collections.Generic; using UnityEngine; public class BillboardCanvas : MonoBehaviour { private Transform tr; private Transform mainCameraTr; // Start is called before the first frame update void Start() { tr = GetComponent<Transform>(); mainCameraTr = Camera.main.transform; } // Update is called once per frame void Update() { tr.LookAt(mainCameraTr); } }

HPbar가 탱크에 자식으로 연결되어 있기 때문에 탱크가 바라보는 방향에 따라 체력바 이미지가 돌아가는 현상이 있었다. 플레이 화면에서는 어느 방향으로 움직이든 플레이어가 아군, 적군의 체력 바를 일직선에서 바라보아야 체력 확인이 용이하기 때문에 HP Canvas가 플레이 내내 플레이어를 바라보도록 하는 함수를 적용했다. 해당 스크립트는 Canvas에 연결해주면 된다.

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Photon.Pun; public class DisplayUserId : MonoBehaviour { public Text userId; private PhotonView pv = null; // Start is called before the first frame update void Start() { pv = GetComponent<PhotonView>(); userId.text = pv.Owner.NickName; } // Update is called once per frame void Update() { } }

해당 스크립트는 Tank 오브젝트와 연결된 내용으로 체력 Canvas의 Text부분에 User ID가 보이도록 구현하였다.

5. 로비 Scene 구현

메인 게임이 시작되기 전 User ID를 입력하고 Room 입장을 하는 Lobby Scene을 구성했다. 기본적인 UI는 반투명 Panel 아래에 TEXT(USER ID)와 외부 키입력을 받는 InputFiled(유저 아이디 입력값을 받음), 방으로 입장하는 Button(Join Random Room)을 배치했다. 여기서 Button의 On Click() 이벤트에 PhotonInit 스크립트를 연결해주었고 해당 스크립트에서 선언한 메서드들 중 JoinRandomRoom() 함수를 설정했다. On Click() 이벤트를 사용해 버튼처럼 클릭해 함수를 호출하여 방으로 이동할 수 있게 된다.

using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; using Photon.Realtime; using UnityEngine.UI; public class PhotonInit : MonoBehaviourPunCallbacks { public string version = "v1.0"; public InputField userId; //플레이어 이름을 입력하는 UI 항목 연결 // Start is called before the first frame update void Awake() { PhotonNetwork.ConnectUsingSettings(); } private void OnGUI() { GUILayout.Label(PhotonNetwork.NetworkClientState.ToString()); } public override void OnConnectedToMaster() //포톤 클라우드에 접속이 잘되면 호출되는 콜백함수 { base.OnConnectedToMaster(); Debug.Log("Entered Lobby"); userId.text = GetUserId(); //PhotonNetwork.JoinRandomRoom(); } string GetUserId() //로컬에 저장된 플레이어 이름을 반환하거나 생성하는 함수 { string userId = PlayerPrefs.GetString("USER_ID"); if (string.IsNullOrEmpty(userId)) { userId = "USER_" + Random.Range(0, 999).ToString("000"); } return userId; } public override void OnJoinRandomFailed(short returnCode, string message) //방 입장이 실패했을 때 { base.OnJoinRandomFailed(returnCode, message); Debug.Log("No Room!!"); PhotonNetwork.CreateRoom("MyRoom", new RoomOptions { MaxPlayers = 20 }); //방을 만들어줌 (최대 20명) } public void OnClickJoinRandomRoom() { PhotonNetwork.NickName = userId.text; //로컬 플레이어 이름을 설정 PlayerPrefs.SetString("USER_ID", userId.text); //플레이어 이름을 저장 PhotonNetwork.JoinRandomRoom(); //무작위로 추출된 룸으로 입장 } public override void OnJoinedRoom() { base.OnJoinedRoom(); Debug.Log("Enter Room"); StartCoroutine(this.LoadBattleField()); //CreateTank(); } IEnumerator LoadBattleField() { //씬을 이동하는 동안 포톤 클라우드 서버로부터 네트워크 메시지 수신 중단 PhotonNetwork.IsMessageQueueRunning = false; //백그라운드로 씬 로딩 AsyncOperation ao = Application.LoadLevelAsync("scBattleField"); yield return ao; } /* void CreateTank() { float pos = Random.Range(-100.0f, 100.0f); PhotonNetwork.Instantiate("Tank", new Vector3(pos, 20.0f, pos), Quaternion.identity, 0); } */ }

6. Main Scene 입장

using System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; public class GameMgr : MonoBehaviourPun { // Start is called before the first frame update void Awake() { CreateTank(); PhotonNetwork.IsMessageQueueRunning = true; //포톤 클라우드의 네트워크 메시지 수신을 다시 연결 } void CreateTank() //탱크를 PhotonNetwork로 생성 { float pos = Random.Range(-100.0f, 100.0f); PhotonNetwork.Instantiate("Tank", new Vector3(pos, 20.0f, pos), Quaternion.identity, 0); } }

메인 씬에서 기존의 PhotonInit 오브젝트(스크립트)는 삭제하고, 빈 오브젝트 생성 - GameManger를 만들고 해당 스크립트를 연결해주었다. 로비에서 Join Random Room 버튼을 누르면 메인 씬으로 이동하고 Awake() 함수를 통해 Tank가 생성, 플레이 할 수 있게 만들었다. 

로비 씬과 메인 씬 두개를 빌드해스 플레이해보면 다음과 같이 플레이할 수 있다. 유저 아이디를 확인할 수 있으며, 피격 시 체력이 깎임과 동기화가 잘 된 것을 확인 할 수 있다.

Toplist

최신 우편물

태그