온라인 구현
This commit is contained in:
@@ -1,22 +1,117 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Photon.Pun;
|
||||
using UnityEngine.Networking;
|
||||
using Hashtable = ExitGames.Client.Photon.Hashtable;
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
public class GameManager : MonoBehaviourPunCallbacks
|
||||
{
|
||||
public TextMeshProUGUI text;
|
||||
public GameObject readyPanel;
|
||||
public GameObject ready;
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
|
||||
public static Dictionary<int, Sprite> AvatarCache = new Dictionary<int, Sprite>();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 씬 시작 시, 모두의 아바타를 다운로드하는 코루틴 시작
|
||||
StartCoroutine(DownloadAllAvatarsAndReady());
|
||||
}
|
||||
|
||||
private IEnumerator DownloadAllAvatarsAndReady()
|
||||
{
|
||||
AvatarCache.Clear(); // 게임 시작 전 딕셔너리 초기화
|
||||
|
||||
// 방에 있는 모든 플레이어를 한 명씩 확인
|
||||
foreach (var player in PhotonNetwork.PlayerList)
|
||||
{
|
||||
// 그 플레이어의 정보에 "AvatarUrl"이 있는지 확인
|
||||
if (player.CustomProperties.TryGetValue("AvatarUrl", out object urlObject))
|
||||
{
|
||||
string url = (string)urlObject;
|
||||
|
||||
// 해당 URL에서 이미지를 다운로드 (비동기 대기)
|
||||
yield return StartCoroutine(DownloadImage(player.ActorNumber, url));
|
||||
}
|
||||
}
|
||||
|
||||
// --- 다운로드가 모두 끝났음! ---
|
||||
|
||||
// 서버에 "나 이미지 다운로드 다 했고 준비 끝났어!" 라고 알림
|
||||
Hashtable props = new Hashtable() { { "IsLoaded", true } };
|
||||
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
|
||||
}
|
||||
|
||||
private IEnumerator DownloadImage(int actorNumber, string url)
|
||||
{
|
||||
// 빈 URL이면 무시
|
||||
if (string.IsNullOrEmpty(url)) yield break;
|
||||
|
||||
using UnityWebRequest request = UnityWebRequestTexture.GetTexture(url);
|
||||
yield return request.SendWebRequest(); // 다운로드 끝날 때까지 대기
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
Debug.LogError($"[다운로드 실패] 유저 번호: {actorNumber}, 에러: {request.error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 성공적으로 받아왔다면 Texture를 추출
|
||||
Texture2D texture = DownloadHandlerTexture.GetContent(request);
|
||||
|
||||
// UI(Image 컴포넌트)에 넣기 편하게 Sprite 형식으로 변환
|
||||
Sprite avatarSprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height),
|
||||
new Vector2(0.5f, 0.5f));
|
||||
|
||||
// 딕셔너리에 저장! (나중에 꺼내 쓸 목적)
|
||||
AvatarCache[actorNumber] = avatarSprite;
|
||||
|
||||
Debug.Log($"[다운로드 성공] 유저 번호 {actorNumber}의 아바타 저장 완료!");
|
||||
}
|
||||
}
|
||||
|
||||
// 누군가의 상태가 바뀔 때마다 실행됨
|
||||
public override void OnPlayerPropertiesUpdate(Photon.Realtime.Player targetPlayer, Hashtable changedProps)
|
||||
{
|
||||
// 내가 방장일 때만 모든 사람이 로딩을 마쳤는지 검사합니다.
|
||||
if (!PhotonNetwork.IsMasterClient || !changedProps.ContainsKey("IsLoaded")) return;
|
||||
if (CheckAllPlayersLoaded())
|
||||
{
|
||||
// 모두가 로딩을 마쳤다면, 방장이 모든 클라이언트에게 신호를 보냅니다!
|
||||
photonView.RPC("RpcStartCountdown", RpcTarget.All);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckAllPlayersLoaded()
|
||||
{
|
||||
// 방에 있는 모든 플레이어의 "IsLoaded"가 true인지 확인
|
||||
foreach (var player in PhotonNetwork.PlayerList)
|
||||
{
|
||||
if (player.CustomProperties.TryGetValue("IsLoaded", out object isLoaded))
|
||||
{
|
||||
if (!(bool)isLoaded) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false; // 아직 로딩 안 된 사람이 있음
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 전원 로딩 완료!
|
||||
}
|
||||
|
||||
// RPC: 방장이 호출하지만, 이 방에 있는 모든 사람의 컴퓨터에서 실행되는 마법의 함수!
|
||||
[PunRPC]
|
||||
private void RpcStartCountdown()
|
||||
{
|
||||
// 방장의 신호를 받고 다 같이 동시에 코루틴을 시작합니다.
|
||||
StartCoroutine(GameSet());
|
||||
}
|
||||
|
||||
private IEnumerator GameSet()
|
||||
{
|
||||
yield return new WaitForSeconds(3f);
|
||||
// 1. 카운트다운
|
||||
text.text = "3";
|
||||
yield return new WaitForSeconds(1f);
|
||||
text.text = "2";
|
||||
@@ -24,8 +119,91 @@ public class GameManager : MonoBehaviour
|
||||
text.text = "1";
|
||||
yield return new WaitForSeconds(1f);
|
||||
text.text = "GO!";
|
||||
|
||||
readyPanel.SetActive(false);
|
||||
yield return new WaitForSeconds(3f);
|
||||
ready.SetActive(false);
|
||||
|
||||
// --- 게임 시작 ---
|
||||
Gazuaa.isGameActive = true; // 스페이스바 입력 활성화!
|
||||
|
||||
// 2. 15초 동안 게임 진행 (대기)
|
||||
yield return new WaitForSeconds(15f);
|
||||
|
||||
// --- 게임 종료 ---
|
||||
Gazuaa.isGameActive = false; // 스페이스바 입력 차단!
|
||||
|
||||
readyPanel.SetActive(true);
|
||||
text.text = "STOP!";
|
||||
|
||||
yield return new WaitForSeconds(2f); // 결과 발표 전 약간의 뜸 들이기
|
||||
|
||||
// 3. 방장만 대표로 가챠를 돌려서 결과를 뽑습니다.
|
||||
if (PhotonNetwork.IsMasterClient)
|
||||
{
|
||||
DetermineWinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DetermineWinner()
|
||||
{
|
||||
Dictionary<int, int> finalCounts = new Dictionary<int, int>();
|
||||
int totalAvatars = 0;
|
||||
|
||||
// 방에 있는 모든 사람의 최종 아바타 개수를 수집합니다.
|
||||
foreach (var player in PhotonNetwork.PlayerList)
|
||||
{
|
||||
if (player.CustomProperties.TryGetValue("AvatarCount", out object countObj))
|
||||
{
|
||||
int count = (int)countObj;
|
||||
finalCounts[player.ActorNumber] = count;
|
||||
totalAvatars += count;
|
||||
}
|
||||
}
|
||||
|
||||
// 아무도 스페이스바를 누르지 않았다면?
|
||||
if (totalAvatars == 0)
|
||||
{
|
||||
photonView.RPC("RpcShowWinner", RpcTarget.All, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 가챠 뽑기 로직 (1 ~ 총 아바타 개수)
|
||||
int randomPick = Random.Range(1, totalAvatars + 1);
|
||||
int currentSum = 0;
|
||||
int winnerActorNumber = -1;
|
||||
|
||||
foreach (var kvp in finalCounts)
|
||||
{
|
||||
currentSum += kvp.Value;
|
||||
if (randomPick <= currentSum)
|
||||
{
|
||||
winnerActorNumber = kvp.Key; // 당첨자 식별 번호!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 모두에게 "이 사람이 당첨됐다!" 라고 쏩니다.
|
||||
photonView.RPC("RpcShowWinner", RpcTarget.All, winnerActorNumber);
|
||||
}
|
||||
|
||||
[PunRPC]
|
||||
private void RpcShowWinner(int winnerActorNumber)
|
||||
{
|
||||
if (winnerActorNumber == -1)
|
||||
{
|
||||
text.text = "아무도 참여하지 않았습니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
// 우승자 닉네임 찾기
|
||||
string winnerName = "알 수 없음";
|
||||
foreach (var p in PhotonNetwork.PlayerList)
|
||||
{
|
||||
if (p.ActorNumber == winnerActorNumber) winnerName = p.NickName;
|
||||
}
|
||||
|
||||
text.text = $"우승: {winnerName}!";
|
||||
|
||||
// 화면에서 우승자의 아바타만 빼고 전부 어둡게/투명하게 만드는 연출 실행!
|
||||
FindObjectOfType<Gazuaa>().HighlightWinner(winnerActorNumber);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI; // Image 컴포넌트를 조작하기 위해 필요!
|
||||
using UnityEngine.InputSystem;
|
||||
using Photon.Pun;
|
||||
using System.Collections.Generic;
|
||||
using TMPro; // Dictionary 사용
|
||||
using Hashtable = ExitGames.Client.Photon.Hashtable;
|
||||
|
||||
public class Gazuaa : MonoBehaviour
|
||||
public class Gazuaa : MonoBehaviourPunCallbacks
|
||||
{
|
||||
private GazuaInput _gazuaInput;
|
||||
|
||||
|
||||
public GameObject map;
|
||||
public GameObject avatarPrefab;
|
||||
public TextMeshProUGUI logText; // [추가] 화면 오른쪽 로그 텍스트박스 연결
|
||||
|
||||
private int myAvatarCount = 0;
|
||||
public static bool isGameActive = false;
|
||||
|
||||
// 핵심!: 각 플레이어(유저 번호)가 지금까지 '몇 개의 아바타를 화면에 그렸는지' 기억하는 사전
|
||||
private Dictionary<int, int> knownAvatarCounts = new Dictionary<int, int>();
|
||||
private List<string> logMessages = new List<string>();
|
||||
private int maxLogLines = 15; // 텍스트박스 높이에 맞춰서 조절하세요!
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -15,18 +28,109 @@ public class Gazuaa : MonoBehaviour
|
||||
_gazuaInput.Enable();
|
||||
|
||||
_gazuaInput.Player.Gacha.performed += Gacha;
|
||||
|
||||
if (logText != null) logText.text = ""; // 시작할 때 텍스트 비우기
|
||||
}
|
||||
|
||||
private void Gacha(InputAction.CallbackContext obj)
|
||||
{
|
||||
// 포톤 방에 없거나, 타이머가 끝나서 게임이 멈췄다면 입력 무시!
|
||||
if (!PhotonNetwork.InRoom || !isGameActive) return;
|
||||
|
||||
var result = Randomizer.GetResult();
|
||||
for(var i = 0; i < result; i++) Instantiate(avatarPrefab, map.transform);
|
||||
myAvatarCount += result;
|
||||
|
||||
Hashtable hash = new Hashtable();
|
||||
hash["AvatarCount"] = myAvatarCount;
|
||||
PhotonNetwork.LocalPlayer.SetCustomProperties(hash);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
// 서버가 "누군가의 정보가 바뀌었어!" 라고 알려줄 때 실행됨 (나의 변경 사항도 포함됨)
|
||||
public override void OnPlayerPropertiesUpdate(Photon.Realtime.Player targetPlayer, Hashtable changedProps)
|
||||
{
|
||||
if (changedProps.ContainsKey("AvatarCount"))
|
||||
{
|
||||
int newTotalCount = (int)changedProps["AvatarCount"];
|
||||
int actorNumber = targetPlayer.ActorNumber;
|
||||
string playerName = targetPlayer.NickName; // [핵심] 아까 로비에서 적은 닉네임 가져오기!
|
||||
|
||||
int oldCount = 0;
|
||||
if (knownAvatarCounts.ContainsKey(actorNumber))
|
||||
{
|
||||
oldCount = knownAvatarCounts[actorNumber];
|
||||
}
|
||||
|
||||
int avatarsToSpawn = newTotalCount - oldCount;
|
||||
|
||||
// [추가] 새로 뽑힌 개수가 0개보다 크다면 로그 띄우기
|
||||
if (avatarsToSpawn > 0)
|
||||
{
|
||||
AddLog($"<b>{playerName}</b>님이 <color=#FFD700>{avatarsToSpawn}</color>개 가챠에 성공했습니다!");
|
||||
}
|
||||
|
||||
for (int i = 0; i < avatarsToSpawn; i++)
|
||||
{
|
||||
GameObject newAvatar = Instantiate(avatarPrefab, map.transform);
|
||||
newAvatar.name = actorNumber.ToString();
|
||||
|
||||
if (GameManager.AvatarCache.ContainsKey(actorNumber))
|
||||
{
|
||||
Image avatarImage = newAvatar.GetComponent<Image>();
|
||||
if (avatarImage != null)
|
||||
{
|
||||
avatarImage.sprite = GameManager.AvatarCache[actorNumber];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
knownAvatarCounts[actorNumber] = newTotalCount;
|
||||
}
|
||||
}
|
||||
|
||||
// [추가] 로그를 관리하고 텍스트박스에 띄워주는 함수
|
||||
private void AddLog(string message)
|
||||
{
|
||||
if (logText == null) return;
|
||||
|
||||
logMessages.Add(message);
|
||||
|
||||
// 정해진 줄 수를 넘어가면 가장 오래된(맨 위의) 로그를 지움
|
||||
if (logMessages.Count > maxLogLines)
|
||||
{
|
||||
logMessages.RemoveAt(0);
|
||||
}
|
||||
|
||||
// 리스트에 있는 텍스트들을 줄바꿈(\n)으로 합쳐서 텍스트박스에 쏙!
|
||||
logText.text = string.Join("\n", logMessages);
|
||||
}
|
||||
|
||||
public void HighlightWinner(int winnerActorNumber)
|
||||
{
|
||||
string winnerNameStr = winnerActorNumber.ToString();
|
||||
|
||||
// Map 밑에 생성된 모든 아바타를 순회합니다.
|
||||
foreach (Transform child in map.transform)
|
||||
{
|
||||
Image img = child.GetComponent<Image>();
|
||||
if (img == null) continue;
|
||||
|
||||
if (child.name == winnerNameStr)
|
||||
{
|
||||
// 당첨자의 아바타: 원래 색상 유지 및 살짝 크게 만들기
|
||||
child.localScale = new Vector3(1.2f, 1.2f, 1.2f);
|
||||
|
||||
// (응용: 당첨자 아바타들 중 하나만 랜덤으로 골라 파티클을 터뜨려도 멋집니다!)
|
||||
}
|
||||
else
|
||||
{
|
||||
// 패배자의 아바타: 까맣게 만들고 반투명하게(Alpha 값 조절)
|
||||
img.color = new Color(0.3f, 0.3f, 0.3f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private new void OnDisable()
|
||||
{
|
||||
_gazuaInput.Disable();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
96
Assets/Scripts/LobbyManager.cs
Normal file
96
Assets/Scripts/LobbyManager.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using Photon.Pun;
|
||||
using Photon.Realtime;
|
||||
using TMPro; // UI 텍스트용
|
||||
using UnityEngine.UI;
|
||||
using Hashtable = ExitGames.Client.Photon.Hashtable; // UI 버튼용
|
||||
|
||||
public class LobbyManager : MonoBehaviourPunCallbacks
|
||||
{
|
||||
public TMP_InputField roomCodeInput; // 방 코드 입력칸
|
||||
public Button joinButton; // 접속 버튼
|
||||
public Button startButton; // 게임 시작 버튼 (방장 전용)
|
||||
public TextMeshProUGUI statusText; // 현재 상태 텍스트
|
||||
public TMP_InputField nicknameInput; // [추가] 닉네임 입력칸
|
||||
public TMP_InputField urlInput;
|
||||
public GameObject loading;
|
||||
public Button afterLoadButton;
|
||||
public GameObject roomPanel;
|
||||
|
||||
void Start()
|
||||
{
|
||||
afterLoadButton.interactable = false;
|
||||
// 핵심: 방장이 씬을 로드하면 나머지 인원도 자동으로 따라가게 설정
|
||||
PhotonNetwork.AutomaticallySyncScene = true;
|
||||
PhotonNetwork.ConnectUsingSettings();
|
||||
}
|
||||
|
||||
public override void OnConnectedToMaster()
|
||||
{
|
||||
loading.SetActive(false);
|
||||
afterLoadButton.interactable = true;
|
||||
PhotonNetwork.JoinLobby();
|
||||
}
|
||||
|
||||
// UI 버튼 클릭 시 호출할 함수
|
||||
public void ClickJoinRoom()
|
||||
{
|
||||
// 방 코드나 닉네임이 비어있으면 안 넘어감
|
||||
if (string.IsNullOrEmpty(roomCodeInput.text) || string.IsNullOrEmpty(nicknameInput.text)) return;
|
||||
|
||||
// 1. [핵심] 포톤 서버에 내 닉네임을 공식적으로 등록합니다!
|
||||
PhotonNetwork.NickName = nicknameInput.text;
|
||||
|
||||
// 2. 아바타 URL 저장 (기존과 동일)
|
||||
string myUrl = urlInput.text;
|
||||
Hashtable myProps = new Hashtable() { { "AvatarUrl", myUrl } };
|
||||
PhotonNetwork.LocalPlayer.SetCustomProperties(myProps);
|
||||
|
||||
// 3. 방 접속
|
||||
RoomOptions roomOptions = new RoomOptions { MaxPlayers = 10 };
|
||||
PhotonNetwork.JoinOrCreateRoom(roomCodeInput.text, roomOptions, TypedLobby.Default);
|
||||
statusText.text = "방 접속 중...";
|
||||
}
|
||||
|
||||
public override void OnJoinedRoom()
|
||||
{
|
||||
statusText.text = $"방 입장 완료! (코드: {PhotonNetwork.CurrentRoom.Name})";
|
||||
StartCoroutine(RoomPanel());
|
||||
}
|
||||
|
||||
private IEnumerator RoomPanel()
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(2f);
|
||||
roomPanel.SetActive(true);
|
||||
startButton.gameObject.SetActive(false); // 일단 게임 시작 버튼은 숨겨놓기
|
||||
// 내가 방장(처음 방을 만든 사람)이라면 게임 시작 버튼 활성화
|
||||
if (PhotonNetwork.IsMasterClient)
|
||||
{
|
||||
startButton.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 방장만 누를 수 있는 '게임 시작' 버튼에 연결할 함수
|
||||
public void ClickStartGame()
|
||||
{
|
||||
// "GameScene"이라는 이름의 씬을 불러옴 (모두가 다 같이 넘어갑니다!)
|
||||
PhotonNetwork.LoadLevel("GameScene");
|
||||
}
|
||||
|
||||
public void ClickLeaveRoom()
|
||||
{
|
||||
// 포톤 서버에 "나 이 방에서 나갈래!" 라고 요청합니다.
|
||||
PhotonNetwork.LeaveRoom();
|
||||
statusText.text = "방에서 나가는 중...";
|
||||
}
|
||||
|
||||
// [추가 2] 방에서 완전히 빠져나왔을 때 포톤이 자동으로 실행해 주는 콜백 함수
|
||||
public override void OnLeftRoom()
|
||||
{
|
||||
// 방에서 나왔으니, UI를 다시 처음 상태(로그인 화면)로 되돌립니다.
|
||||
roomPanel.SetActive(false);
|
||||
|
||||
statusText.text = "무사히 도망쳤다!";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/LobbyManager.cs.meta
Normal file
2
Assets/Scripts/LobbyManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc444b467b04d1a4e85fb02c928fa74f
|
||||
18
Assets/Scripts/TextColorManager.cs
Normal file
18
Assets/Scripts/TextColorManager.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class TextColorManager : MonoBehaviour
|
||||
{
|
||||
private TextMeshProUGUI _text;
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
_text = GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
_text.color = Color.HSVToRGB(Mathf.PingPong(Time.time, 1f), 1f, 1f);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TextColorManager.cs.meta
Normal file
2
Assets/Scripts/TextColorManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce0f98ad994074447bac7927e022f02e
|
||||
Reference in New Issue
Block a user