2023. 12. 22. 21:19ㆍSKKU DT
이제 캐릭터끼리 죽일 수 있게 하기 위해 Damage 스크립트를 생성한다.
캐릭터를 Destroy하면 플레이어를 다시 생성하는 과정이 필요하기 때문에 다른 방식을 사용해야 한다. 렌더러를 끄고 리스폰 장소에서 다시 나오도록.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Player = Photon.Realtime.Player;
public class Damage : MonoBehaviourPunCallbacks
{
//MeshRenderer 컴포넌트 배열로 가져오기. 죽은 다음 투명 처리를 하기 위해서.
//배열로 가져오는 이유는 플레이어의 Skinned MeshRenderer, 총의 MeshRenderer 두 개 있기 때문.
Renderer[] renderers;
//초기 Hp
int initialHp = 100;
//현재 Hp
public int currentHp = 100;
//죽을 때 애니메이션
Animator anim;
//죽었을 때 CharacterController 막아야한다. 죽은 캐릭터가 움직이면 안되기 때문.
CharacterController cc;
private readonly int hashDie = Animator.StringToHash("Die"); //애니메이터 두 파라미터를 해시데이터 값을 만들어서 뒤에서 불러올 예정
private readonly int hashRespawn = Animator.StringToHash("Respawn");
private void Awake()
{
//자식 오브젝트의 렌더러를 가져온다.
renderers = GetComponentsInChildren<Renderer>();
anim = GetComponent<Animator>();
cc = GetComponent<CharacterController>();
//시작 전 체력 동일하게 맞추기
currentHp = initialHp;
}
private void OnCollisionEnter(Collision collision)
{
//총알과 닿으면 체력이 깎이게 한다. 특정 부위에 collider 넣고 Tag로 구별해서 해당 collider에 닿으면 데미지를 추가로 넣도록 응용할 수도 있다.
if(currentHp > 0 && collision.collider.CompareTag("BULLET")) //현재 hp가 양수이고 충돌체의 태그가 BULLET이면
{
currentHp -= 10;
if(currentHp <= 0) //만약 현재 hp가 음수이면 Die 코루틴 실행
{
StartCoroutine(PlayerDie());
}
Debug.Log($"현재 체력: {currentHp}");
}
}
IEnumerator PlayerDie()
{
//캐릭터 컨트롤러 컴포넌트 비활성화
cc.enabled = false;
//리스폰 애니메이션 비활성화. Respawn 파라미터의 타입은 bool
anim.SetBool(hashRespawn, false); //string 타입("Respawn")으로 가져와도 되고 hash 타입으로 가져와도 된다.
//사망 애니메이션 활성화
anim.SetTrigger(hashDie);
//죽고 나서 조금 대기
yield return new WaitForSeconds(3f);
//리스폰 애니메이션 활성화
anim.SetBool(hashRespawn, true);
//캐릭터 렌더러 비활성화 함수
SetPlayerVisible(false);
//리스폰까지 대기
yield return new WaitForSeconds(3f);
//플레이어를 안보이게만 해놓았으므로 위에서 비활성화 했던 것들을 반대로 바꿔야 한다.
//체력 재설정
currentHp = 100;
//캐릭터 렌더러 활성화
SetPlayerVisible(true);
//캐릭터 컨트롤러 활성화
cc.enabled = true;
}
//캐릭터 렌더러 컴포넌트 배열을 활성화/비활성화 함수
void SetPlayerVisible(bool isVisible)
{
for(int i = 0; i < renderers.Length; i++)
{
//렌더러 배열을 활성화 할지 여부
renderers[i].enabled = isVisible;
}
}
}
**특정 부위에 collider 넣고 Tag로 구별해서 해당 collider에 닿으면 데미지를 추가로 넣도록 응용할 수도 있다.
로비에서 유저 이름과 방 이름을 설정
먼저 로비에서 닉네임을 생성한다.
로비씬은 게임씬의 환경을 가져다 쓰기 위해 씬 복사한다. Virtual Camera 삭제, Main Camera 씨네머신 컴포넌트 삭제
로비 UI 구현
PhotonManager 스크립트 수정
유저 ID 설정, 룸ID 설정, 랜덤 룸 함수 끄기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Photon 네임스페이스 추가
using Photon.Pun;
using Photon.Realtime;
using TMPro;
public class PhotonManager : MonoBehaviourPunCallbacks //상속 클래스 변경
{
//변수 선언
private readonly string version = "1.0";//게임 버전 체크. 유저가 건드리지 못하게 private, readonly
private string userId = "Victor"; //아무거나 userId 생성
//유저 ID를 입력할 인풋 필드
public TMP_InputField userInputField;
//룸 ID를 입력할 인풋 필드
public TMP_InputField roomInputField;
//네트워크 접속은 Start()보다 먼저 실행되어야한다. Awake() 함수 사용
private void Awake()
{
//씬 동기화. 맨 처음 접속한 사람이 방장이 된다.
PhotonNetwork.AutomaticallySyncScene = true;
//버전 할당. 위에 string으로 만들었던 version을 쓴다.
PhotonNetwork.GameVersion = version;
//App ID 할당. 위에 userId로 만들었던 userId를 쓴다.
PhotonNetwork.NickName = userId;
//포톤 서버와의 통신 횟수를 로그로 찍기. 기본값 : 30
Debug.Log(PhotonNetwork.SendRate); //제대로 통신이 되었다면 30이 출력된다.
//포톤 서버에 접속
PhotonNetwork.ConnectUsingSettings();
}
//CallBack 함수
public override void OnConnectedToMaster() //정상적으로 마스터 서버에 접속이 되면 호출된다.
{
//마스터 서버에 접속이 되었는지 디버깅 한다.
Debug.Log("Connected to Master");
Debug.Log($"In Lobby = {PhotonNetwork.InLobby}"); //로비에 들어와 있으면 True, 아니면 False 반환. Master 서버에는 접속했지만 로비에는 아니므로 False 반환된다.
//로비 접속
PhotonNetwork.JoinLobby();
}
public override void OnJoinedLobby() //로비에 접속이 제대로 되었다면 해당 콜백함수 호출
{
Debug.Log($"In Lobby = {PhotonNetwork.InLobby}"); //로비에 접속이 되었다면 True가 반환 될 것이다.
//방 접속 방법은 두 가지. 1.랜덤 매치메이킹, 2.선택된 방 접속
//PhotonNetwork.JoinRandomRoom();
}
//방 생성이 되지 않았으면 오류 콜백 함수 실행
public override void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log($"JoinRandom Failed {returnCode}: {message}");
OnMakeRoomClick(); //오류 나는 것을 방지하기 위해서.
//룸 속성 설정
//RoomOptions roomOptions = new RoomOptions();
//룸의 접속할 수 있는 최대 접속자 수 최대 제한을 해놔야 CCU를 제한할 수 있다.
//roomOptions.MaxPlayers = 20;
//룸 오픈 여부
//roomOptions.IsOpen = true;
//로비에서 룸의 목록에 노출시킬지 여부. 공개방 생성
//roomOptions.IsVisible = true;
//룸 생성
//PhotonNetwork.CreateRoom("Room1", roomOptions); //룸 이름과 룸 설정. 우리는 roomOptions에 설정을 이미 해놓았다.
}
//제대로 룸이 있다면 다음의 콜백 함수를 호출한다.
public override void OnCreatedRoom()
{
Debug.Log("Created Room");
Debug.Log($"Room Name: {PhotonNetwork.CurrentRoom.Name}");
}
//룸에 들어왔을 때 콜백 함수
public override void OnJoinedRoom()
{
Debug.Log($"In Room = {PhotonNetwork.InRoom}");
Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
//접속한 사용자 닉네임 확인
foreach(var player in PhotonNetwork.CurrentRoom.Players)
{
//플레이어 닉네임, 유저의 고유값 가져오기
Debug.Log($"플레이어 닉네임: {player.Value.NickName}, 유저 고유값: {player.Value.ActorNumber}");
}
//플레이어 생성 포인트 그룹 배열을 받아오기. 포인트 그룹의 자식 오브젝트의 Transform 받아오기.
//Transform[] points = GameObject.Find("PointGroup").GetComponentsInChildren<Transform>();
//1부터 배열의 길이까지의 숫자 중 Random한 값을 추출
//int idx = Random.Range(1, points.Length);
//플레이어 프리팹을 추출한 idx 위치와 회전 값에 생성. 네트워크를 통해서.
//PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
//마스터 클라이언트인 경우 게임 씬 로딩
if(PhotonNetwork.IsMasterClient)
{
PhotonNetwork.LoadLevel("GameScene"); //씬 이름으로 불러오기
}
}
private void Start()
{
//유저 ID 랜덤 설정
userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1, 21):00}"); //20명까지 밖에 못들어오므로 1~21 설정. :00은 한 자리도 두 자리로 만들어주려고.
userInputField.text = userId;
//접속 닉네임 네트워크 등록
PhotonNetwork.NickName = userId;
}
//유저명을 설정하는 로직
public void SetUserId()
{
//인풋 필드가 비어있으면 랜덤한 값, 그렇지 않으면 유저가 생성한 값. 다시 접속했을 때 닉네임 보존
if (string.IsNullOrEmpty(userInputField.text))
{
userId = $"USER_{Random.Range(1, 21):00}";
}
else
{
userId = userInputField.text;
}
//유저명 저장. 로비에서 만든 개체를 메인에서도 쓸 수 있다.
PlayerPrefs.SetString("USER_ID", userId);
PhotonNetwork.NickName = userId; //네트워크에도 반영
}
string SetRoomName()
{
//비어있으면 랜덤한 룸 이름. 그렇지 않으면 가져오도록.
if (string.IsNullOrEmpty(roomInputField.text))
{
roomInputField.text = $"ROOM_{Random.Range(1, 101):000}";
}
return roomInputField.text;
}
public void OnLoginClick() //로그인 버튼 매핑 함수
{
//유저 ID 저장
SetUserId();
//무작위 룸으로 입장
PhotonNetwork.JoinRandomRoom();
}
public void OnMakeRoomClick() //방 생성 버튼 매핑 함수
{
//유저 ID 저장
SetUserId();
//룸 속성 정의
RoomOptions ro = new RoomOptions();
ro.MaxPlayers = 20;
ro.IsOpen = true;
//공개방 설정
ro.IsVisible = true;
//룸 생성
PhotonNetwork.CreateRoom(SetRoomName(), ro); //고정된 값이 아니라 유저가 타이핑한 값을 받아온다.
}
}
버튼 매핑을 한다.
바로 실행하면 User ID에 자동으로 이름이 생성되는 것을 볼 수 있다.
하지만 플레이어 생성은 되지 않는다.
GameScene에서 더이상 PhotonManager는 필요하지 않다. 삭제.
GameManager 빈 오브젝트 생성, GameManager 스크립트 생성
GameManager에서 플레이어를 생성할 예정이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
public class GameManager : MonoBehaviour
{
private void Awake()
{
CreatePlayer();
}
void CreatePlayer()
{
//PhotonManager 스크립트에서 주석처리 되어있던 줄을 가져왔다.
//플레이어 생성 포인트 그룹 배열을 받아오기. 포인트 그룹의 자식 오브젝트의 Transform 받아오기.
Transform[] points = GameObject.Find("PointGroup").GetComponentsInChildren<Transform>();
//1부터 배열의 길이까지의 숫자 중 Random한 값을 추출
int idx = Random.Range(1, points.Length);
//플레이어 프리팹을 추출한 idx 위치와 회전 값에 생성. 네트워크를 통해서.
PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
}
}
룸 목록 만들기
패널 하나, 스크롤 뷰 하나, 버튼 하나 만들어서 사이즈를 조절한다.
RoomItem 버튼 위치 수정
Content에 Grid Layout Group 컴포넌트 추가
Grid Layout Group 설정 수정
PhotonManage 스크립트 수정
OnRoomListUpdate(List<RoomInfo> roomList) 콜백 함수 추가
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
foreach(var room in roomList)
{
Debug.Log($"Room = {room.Name} ({room.PlayerCount}/{room.MaxPlayers}");
}
}
룸 정보를 Dictionary 형태로 저장
변수 부분 추가
//룸 목록에 대한 데이터 저장
Dictionary<string, GameObject> rooms = new Dictionary<string, GameObject>();
//룸 목록을 표시할 프리팹
GameObject roomItemPrefab;
//룸 목록이 표시될 scroll content
public Transform scrollContent;
Awake 함수도 수정
private void Awake()
{
//씬 동기화. 맨 처음 접속한 사람이 방장이 된다.
PhotonNetwork.AutomaticallySyncScene = true;
//버전 할당. 위에 string으로 만들었던 version을 쓴다.
PhotonNetwork.GameVersion = version;
//App ID 할당. 위에 userId로 만들었던 userId를 쓴다.
PhotonNetwork.NickName = userId;
//포톤 서버와의 통신 횟수를 로그로 찍기. 기본값 : 30
Debug.Log(PhotonNetwork.SendRate); //제대로 통신이 되었다면 30이 출력된다.
//RoomItem 프리팹 로드 Resources 폴더로부터...
roomItemPrefab = Resources.Load<GameObject>("RoomItem");
//포톤 서버에 접속
if (PhotonNetwork.IsConnected == false)
{
PhotonNetwork.ConnectUsingSettings();
}
}
OnRoomListUpdate 콜백 함수 수정
//방 리스트를 수신하는 콜백 함수 생성
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
// 삭제된 RoomItem 프리팹을 저장할 임시변수
GameObject tempRoom = null;
foreach (var roomInfo in roomList)
{
// 룸이 삭제된 경우
if (roomInfo.RemovedFromList == true)
{
// 딕셔너리에서 룸 이름으로 검색해 저장된 RoomItem 프리팹을 추출
rooms.TryGetValue(roomInfo.Name, out tempRoom);
// RoomItem 프리팹 삭제
Destroy(tempRoom);
// 딕셔너리에서 해당 룸 이름의 데이터를 삭제
rooms.Remove(roomInfo.Name);
}
else // 룸 정보가 변경된 경우
{
// 룸 이름이 딕셔너리에 없는 경우 새로 추가
if (rooms.ContainsKey(roomInfo.Name) == false)
{
// RoomInfo 프리팹을 scrollContent 하위에 생성
GameObject roomPrefab = Instantiate(roomItemPrefab, scrollContent);
// 룸 정보를 표시하기 위해 RoomInfo 정보 전달
roomPrefab.GetComponent<RoomData>().RoomInfo = roomInfo;
// 딕셔너리 자료형에 데이터 추가
rooms.Add(roomInfo.Name, roomPrefab);
}
else // 룸 이름이 딕셔너리에 없는 경우에 룸 정보를 갱신
{
rooms.TryGetValue(roomInfo.Name, out tempRoom);
tempRoom.GetComponent<RoomData>().RoomInfo = roomInfo;
}
}
Debug.Log($"Room={roomInfo.Name} ({roomInfo.PlayerCount}/{roomInfo.MaxPlayers})");
}
}
RoomData 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;
public class RoomData : MonoBehaviour
{
private RoomInfo _roomInfo;
// 하위에 있는 TMP_Text를 저장할 변수
private TMP_Text roomInfoText;
// PhotonManager 접근 변수
private PhotonManager photonManager;
// 프로퍼티 정의
public RoomInfo RoomInfo
{
get
{
return _roomInfo;
}
set
{
_roomInfo = value;
// 룸 정보 표시
roomInfoText.text = $"{_roomInfo.Name} ({_roomInfo.PlayerCount}/{_roomInfo.MaxPlayers})";
// 버튼 클릭 이벤트에 함수 연결
GetComponent<UnityEngine.UI.Button>().onClick.AddListener(() => OnEnterRoom(_roomInfo.Name));
}
}
void Awake()
{
roomInfoText = GetComponentInChildren<TMP_Text>();
photonManager = GameObject.Find("PhotonManager").GetComponent<PhotonManager>();
}
void OnEnterRoom(string roomName)
{
// 유저명 설정
photonManager.SetUserId();
// 룸 속성 정의
RoomOptions ro = new RoomOptions();
ro.MaxPlayers = 20; // 룸에 접속할 수 있는 최대 접속자 수
ro.IsOpen = true; // 룸의 오픈 여부
ro.IsVisible = true; // 로비에서 룸 목록에 노출시킬지 여부
// 룸 접속
PhotonNetwork.JoinOrCreateRoom(roomName, ro, TypedLobby.Default);
}
}
RoomData 스크립트를 프리팹에 추가
Content를 PhotonManager 빈 컴포넌트에 추가
왼쪽 플레이어가 들어간 방이 오른쪽 플레이어의 룸 리스트에 뜬다.